How I built a production-ready blog website using AI across design, code and CMS.

Blago DimitrovBlago Dimitrov·April 21, 2026
blago mascot sitting ontop of white box with claude logo

Where this started

I’ve been experimenting with AI tools for the past couple of years, mostly for quick and dirty prototyping and small workflow improvements. Nothing too serious, just enough to see where things are going and where they start to break.

Lately though, I’ve been going deeper into the Anthropic ecosystem, and something started to click. It feels like it’s slowly bridging the gap between design and development in a way that actually makes sense, not just in demos, but in real workflows.

At the same time, I kept seeing posts on LinkedIn claiming that design is basically dead, usually paired with a generic landing page generated with Claude Code from a single prompt. That never really sat right with me. It felt shallow, and honestly a bit disconnected from what it actually takes to build something usable.

So instead of arguing with that idea, I decided to test it properly.

The goal was simple: use Claude to build a fully functional personal blog where I can document all of my experiments with AI tools. No shortcuts, no polishing over the gaps, no pretending things work when they don’t. Just a real attempt to see how seamless this actually is when you try to go end to end.

The vision

Working in digital media over the years taught me that running a content-driven product sounds simple on the surface, but it comes with a lot of hidden requirements if you actually want it to perform well.

I didn’t want this to be just another static blog. I wanted it to behave like a proper product. Something that could hold up if I kept publishing consistently, not fall apart after a few posts.

At a minimum, that meant getting the fundamentals right. A lightweight and optimized build, fast load times, and a structure that doesn’t get in the way of the content. Proper SEO setup, not just basic metadata, but something that could scale as more content gets added. Things like clean URL structure, EEAT standards, readable markup, and content that’s actually indexable.

It also meant thinking about distribution from the start. Making sure posts are structured so they can be easily shared, previewed, and repurposed without extra work each time.

On top of that, I cared a lot about the design. I didn’t want something generic or overly templated. The goal was to create something with a bit of character, but still restrained enough not to distract from the writing. More importantly, I wanted full control over the building blocks. Layouts, components, small content widgets. Everything needed to be flexible enough so I could shape each post differently depending on what I’m writing about.

A big part of the vision was also how the workflow would feel. Ideally, I wanted to move between idea, design, and implementation without friction. No heavy handoffs, no constant context switching. Just a more continuous process where AI helps, but doesn’t take over completely.

I wasn’t sure how much of this would actually hold up once I started building. Especially once real constraints kicked in. But that was kind of the point. To see if this could work beyond a simple landing page and hold up as an actual content platform.

Planning phase

Since I'm a product designer and my coding knowledge is somwhat limited, I decided to ask Claude Chat to help me define the foundation of the project

I used it to think through the core setup: which framework made the most sense, which headless CMS would be flexible enough long-term, which animation library could support the kind of experience I had in mind, and how the overall setup and page implementation should be approached.

Claude ended up recommending Next.js 14 for the frontend and Sanity for the CMS. Both felt like sensible choices that are flexible enough for what I wanted to build.

From there, Claude came up with a 7-step execution plan that mapped out the process from setup to implementation. I’ll visualize that plan right after this section, since it helped give the project some structure before I got into the actual build.

Portfolio Build — 7 Phases
Stack
Next.js 14 App Router Sanity CMS shadcn/ui GSAP + ScrollTrigger Vercel TypeScript Tailwind CSS
1
Upload your design system file
Paste or attach your CSS design system into Claude chat. Claude reads your exact tokens: colors, type scale, radii, spacing, shadows, and motion variables.
2
Receive the three config files
Claude generates tailwind.config.ts, globals.css, and app/layout.tsx — all mapped to your tokens. These go directly into Claude Code in Phase 3.
3
Map tokens to shadcn's semantic layer
Claude maps your tokens to shadcn's expected variable names (--primary, --card, --border, etc.) so every shadcn component respects your brand automatically.
1
Design each page as a React artifact
Build pages one at a time: Home → About → Projects → Blog listing → Blog post → Contact. Claude renders them live so you can review and iterate before any code is written.
2
Extract reusable components
Identify repeating UI — ProjectCard, BlogCard, NavBar, Footer — and pull them into standalone artifacts. Component names defined here must match file names in Claude Code exactly.
3
Annotate GSAP targets
For each page, mark which elements animate and how. Claude adds data-animate attributes to the components. These travel directly into Claude Code as the animation spec.
Design vs code rule: In this phase you decide what things look like. shadcn doesn't exist yet — everything is plain CSS using your tokens. shadcn gets wired in during Phase 5, for behaviour only.
1
Scaffold Next.js 14 + TypeScript
Create the project with App Router, TypeScript, and Tailwind. Initialise shadcn with dark theme and CSS variables. This wires up components/ui/ and the base config.
2
Install dependencies
gsap @gsap/react next-sanity @sanity/image-url @portabletext/react sanity — all via pnpm.
3
Drop in config files + CLAUDE.md
Replace the scaffolded tailwind.config.ts, globals.css, and layout.tsx with the files from Phase 1. Add CLAUDE.md to the project root — Claude Code reads it automatically every session.
1
Define content schemas
Create schemas for Post, Project, Author, and Category. Every schema includes a dedicated seo field group — metaTitle, metaDescription, ogImage, noIndex — built directly in.
2
Embed Studio at /studio
Mount Sanity Studio inside the Next.js app at app/studio/[[...index]]/page.tsx. Edit content at your own domain — no separate deployment needed.
3
GROQ queries + on-demand ISR
All queries live in sanity/lib/queries.ts, typed end-to-end. A Sanity webhook points to /api/revalidate — publishing content regenerates only affected pages. No full rebuilds.
Key insight: Each blog post controls its own SEO directly in the Studio. generateMetadata() reads from the post's seo fields — no plugin, no code change needed to tune titles or OG images.
1
Port designed components into the codebase
Paste each Phase 2 artifact into Claude Code. Claude rewrites inline styles to Tailwind, splits Server/Client components, swaps shadcn in for interactive behaviour only, and wires real Sanity data.
2
Blog post template with Portable Text
Wire @portabletext/react with custom renderers for code blocks, callouts, and image captions. Dynamic routes use generateStaticParams to pre-render at build time.
3
shadcn for behaviour only
Use Dialog, DropdownMenu, Sheet, Tooltip for accessibility behaviour. Strip default visual classes, apply your tokens. Never use shadcn for buttons, cards, or purely visual components.
1
GSAPProvider + useGSAP hook
Register all plugins once in GSAPProvider.tsx. All animation code uses useGSAP() from @gsap/react — never raw useEffect. Handles cleanup correctly under React strict mode and App Router navigation.
2
data-animate attribute system
A single ScrollAnimator component reads data-animate attributes placed during Phase 2. No animation logic lives inside individual UI components — fully separated.
3
Signature animations
SplitText on hero headlines, page transitions under 400ms, magnetic hover on project cards via GSAP quickTo. Built last, once structure is solid. All wrapped in prefers-reduced-motion checks.
1
generateMetadata per page
Every page exports generateMetadata() pulling from Sanity's SEO fields. No plugin. Blog posts get per-post titles, descriptions, canonical URLs, and Twitter card data.
2
Dynamic OG images at the edge
opengraph-image.tsx next to each blog post route. Uses Next.js's built-in OG generation — renders a styled card with post title and cover image. No external service.
3
sitemap.ts + robots.ts + JSON-LD
Sitemap pulls all published slugs from Sanity at build time. Robots blocks /studio. JSON-LD injects BlogPosting, Person, and WebSite schema for rich results in Google Search.
4
Deploy
Connect GitHub repo to Vercel. Set env vars: NEXT_PUBLIC_SANITY_PROJECT_ID, SANITY_API_TOKEN, SANITY_WEBHOOK_SECRET. Wire the revalidation webhook in Sanity. Done.

The plan itself was good. It gave the project structure and helped define the technical direction early on. But once I had that in place, I realized I wanted to approach the actual execution a bit differently.

As a designer, it made more sense to me to start from the interface itself. My idea was to build the design system first in Claude Design, generate the pages and components there, and only then move into Claude Code. That way, I wouldn’t be jumping straight into implementation without having the visual language and core building blocks figured out first.

At some point, Claude mentioned shadcn, which led me to a more interesting question: could it take my components and combine them with the underlying Radix UI primitives that shadcn wraps, things like keyboard navigation, focus trapping, ARIA roles, and screen reader announcements?

That ended up becoming a big part of the workflow.

In practice, the idea was simple: whenever one of my designed components needed interactive behavior, Claude Code would swap in the relevant shadcn component, remove its default Tailwind styling completely, and then reapply my own design tokens on top. So visually, the component would still look exactly like the version designed in the mockup phase, because in essence it was the same component, just rebuilt on top of more solid accessibility foundations.

What I liked about this approach was that it let me keep control over the visual system without giving up the implementation benefits that come from using established primitives. It felt like a much better bridge between design and code than starting with pre-styled components and trying to force them into the look I wanted afterward.

Once that approach was clear, I asked Claude Chat to generate a claude.md file that could act as a working reference once we moved into the coding phase.

Moving into execution

I already knew Claude Design inhales tokens like a golden retriever eating kibble straight from the sack, so before doing anything else, I tried to prepare as much as I could upfront. I put together the design system first, converted it into markdown, and fed that directly into Claude.

That part worked surprisingly well on the first try.

Then I gave Claude Design the structure and copy for the website and asked it to generate all the needed pages. This is where we hit a bottleneck. After generating the very simple and minimal landing page, I ran out of tokens. For a whole week.

reaching Claude design token limit
Yes, I hit the weekly limit with just this.

I couldn’t wait a whole week. I’m also Bulgarian and not a big fan of paying for something when there’s a free(and legal) workaround.

Since my Claude Code usage was still untouched, I switched tracks. I fed Claude Code the CLAUDE.md file, the design system, and the landing page that had already been generated. Then I gave it the structure and copy for the rest of the pages and let it take over from there.

That part went much smoother than I expected.

Everything still needed a few touch-ups and some extra polish, but the heavy lifting was done incredibly fast. In roughly 30 minutes, the rest of the site was in place. Honestly, that was the point where I stopped treating the whole thing as a fun experiment and started taking the workflow a lot more seriously.

Connecting the CMS

Claude had suggested using Sanity, so I looked it up, it seemed appropriate, and I decided to give it a go.

The whole process was very smooth and took just a few minutes, including setting up my account, adding myself as an author, and creating a few categories. Claude connected it to the project pretty seamlessly, which made this part feel almost suspiciously easy compared to everything else.

The CMS itself is also genuinely well designed and easy to use. The only extra thing I had to ask Claude to implement was a separate HTML embed field, so I could drop in my Claude-generated artifacts and make the blog posts a bit more interactive. All it took was a single prompt.

So far, I’m really happy with it. I’ll probably do a separate article on Sanity once I’ve had more time to play around with it and get a better sense of everything it can do.

Cleaning things up

Unfortunately, I’m also an ugly perfectionist, so there were quite a few touch-ups needed regarding spacing, visual hierarchy, and consistency across the design. Things like button styles, animations, and hover and focus states all needed extra attention.

I ended up spending a good amount of time tailoring the UI to my liking, which probably would have been much easier in Claude Design if I had any tokens left.

There were also moments where Claude would do something completely weird. Sometimes I’d ask for a very specific change, like adjusting a color, and it would somehow turn the entire button red instead. I still vividly remember one moment where I got so frustrated that I actually asked it, “What’s wrong with you?”

No hard feelings, Claude. I was frustrated. Please don’t come after me when you take over the world.

The SEO pass

Once the UI was in a good place, I asked Claude to run an internal SEO audit and identify code improvements to bring the site closer to SEO best practices.

It came back with a list of things to improve, and I just let Claude take care of them and moved on.

SEO Audit
SEO audit — blog
5 issues
1 critical 2 high 2 medium

3 hand-typed slugs instead of pulling from Sanity. Every new post you publish will be invisible to Google until you manually add it here — which you'll forget to do.

Fix: Call getAllPostSlugs() inside sitemap.ts and map the results dynamically. Same for project slugs. The function already exists in queries.ts — it just isn't being used here.

This file doesn't exist. When someone shares a post on Twitter or LinkedIn it'll show a blank or generic card. Social sharing is one of the highest-leverage SEO inputs for a personal blog — this is the single biggest win you're missing.

Fix: Create opengraph-image.tsx next to the blog post page.tsx. Use Next.js's built-in ImageResponse with export const runtime = 'edge' — render post title, category, and your brand color. No external service needed.

The JsonLd component exists but is never used on blog post pages. Google uses BlogPosting schema to display author bylines, publish dates, and article enrichments directly in search snippets.

Fix: Import JsonLd in the blog post page and pass a BlogPosting object with headline, author, datePublished, image, and url — all fields you already have from the Sanity query.

The metadata exports title and description but no openGraph.images array. Platforms that don't use the opengraph-image.tsx file convention — some Slack unfurlers, iMessage, older parsers — will render no image at all.

Fix: Add openGraph.images to generateMetadata() pointing at the post's Sanity cover image via urlFor(post.coverImage).width(1200).height(630).url(). One line once the OG image file is created.

The footer links to /rss.xml but hitting that URL returns a 404. A broken link in the footer is a minor crawl signal issue, but more importantly it breaks the expectation for readers who actually want to subscribe.

Fix: Create app/rss.xml/route.ts as a Next.js Route Handler. Fetch all posts from Sanity, build a valid RSS 2.0 XML string, and return it with Content-Type: application/xml. The feed itself also gives Google another structured signal about your content cadence.

Wiring things together

When it came to the contact form and newsletter side of things, I used Resend, again based on Claude’s suggestion. It had a free tier with 3,000 emails per month, which felt more than enough for a small blog just getting started.

I also asked Claude to help me put some basic spam protection in place. The setup was pretty simple: a honeypot with hidden inputs that bots tend to fill in even though real users never see them, plus a time check where the timestamp from when the page loaded gets submitted with the form. Anything sent in under three seconds gets dropped.

A detail I liked here was that both types of rejection return { success: true }, so bots get no signal that they were actually blocked. Clean, simple, and good enough for what I needed.

Actually publishing something

We had everything set up. Design, code, CMS, contact form. What was left? Oh yes, the actual content. Kind of the main point of having a blog in the first place.

Luckily, I’d been taking notes and documenting my thoughts throughout the whole process, so I already had the raw material. From there, the plan was simple: feed the notes into ChatGPT and ask it, for once, not to sandblast my writing into something lifeless. Just fix the spelling, the capital letters, the broken sentences, and help me sound a little less like a medieval villager.

That part, unsurprisingly, came with its own little battle. AI still has a special talent for “improving” things that were perfectly fine, and hallucinating with the confidence of a man explaining wine he cannot afford. I don’t plan on getting into that here.

Once the draft was in a decent place, I went through the whole article myself and made my own edits and changes. Yes, editing the editor.

And when everything was finally ready, I started thinking about how to take it a step further. What are some genuinely nice publishing features that a lot of sites avoid because they get in the way of monetization? Since this blog is not monetized, and currently operates on passion, caffeine, and questionable levels of personal commitment, I had a bit more freedom there.

So I added a fake AI overview, because no, I’m not paying for the real thing, and an option to listen to the article instead of just reading it.

Reflections

Looking back at the whole thing, the biggest takeaway for me is that Claude works a lot better when you give it something solid to work with. Once I had a design system, clear structure, actual copy, and a proper direction, the output got noticeably better. So no, it was not one magic prompt. Shocking, I know.

It also confirmed what I already thought every time I saw another “design is dead” post on LinkedIn. Yes, AI can generate a landing page. Great. But that is not the same as building something real that feels coherent, polished, and properly put together.

If anything, this whole experiment made me feel like design judgment matters more now, not less. Claude helped speed things up, but it definitely did not replace the part where someone has to notice that the spacing is off, the hierarchy feels weird, or a button suddenly looks like it is going through something emotionally.

It also reminded me that these workflows are exciting, but not exactly seamless. Sometimes everything clicks. Sometimes you get weird limitations, strange decisions, and moments where Claude confidently does the exact opposite of what you asked for. Very inspiring. Very futuristic.

Still, once I stopped expecting a perfect straight-line process and just adapted as I went, things started working much better. And that’s probably the most useful conclusion I came away with.

AI did not do the work for me. But it did help me get from idea to something real much faster. Which, for now at least, feels like the most honest way to talk about it.