
I recently built my portfolio site, and while figuring out the design and what content to put on it, an idea struck me: I wanted to include a blog. Not the daily-journal kind, but a place to write about the technical things I learn during my coding sessions, reading docs, picking up new libraries, and so on.
Coding is something that really fascinates me. So instead of going with the first solution I find on the internet or get from an LLM, I usually go round and round, trying out different approaches, just to understand the pros and cons of each before picking the one that fits what I actually need. This blog post is about one of those detours, the one I took while setting up the blogging system itself.
The trending way to add a blog to a Next.js portfolio is pretty simple: write your posts as MDX files (which support React components too), drop them into a folder in your source code, and let Next.js generate the pages with a bit of boilerplate. It's fast, it gives you statically generated pages, and it's great for SEO.
I knew all this. I still didn't go that route. You might ask why, since engineering is supposed to be about finding the most efficient solution, and this clearly was one. Fair question. I had two reasons, and neither of them was really a "problem" with the MDX approach so much as a deliberate detour I chose to take:
These days, my first move for any unfamiliar problem is to ask an LLM, then go deep on the actual docs. So once I'd decided I wanted a blog but didn't want to touch source code to publish a post, I went to Gemini and asked what I should be doing.
I already had a rough idea of what CMS platforms were, and since I was using Next.js, I knew there'd be some way to pre-render pages statically. That mattered to me for two reasons: my blog posts are just write-ups of technical things I've learned, so they're static content by nature, and rendering them on every request would be wasteful, and It would also be bad for SEO.
What I didn't know was how CMS platforms actually worked under the hood, whether a headless CMS was the only sensible option, or how Incremental Static Regeneration (ISR) and revalidation worked in Next.js, since I'd never touched that part of the framework before.
Gemini told me what I needed was a "headless CMS + ISR" setup. I asked it to explain how that would actually work, and the answer was roughly this: I'd write content in a text editor on a separate portal (which could also be hosted on my own site), and once I hit "publish," a webhook on my site would catch the new content. From there, Next.js would handle the SSG + ISR side of things, regenerating the static pages without me ever touching my source code.
Then it suggested something else: since I clearly wanted to learn this deeply, why not build my own headless CMS? It even offered to help.
By now, most people have learned not to take an LLM's suggestion at face value, and after sitting with the idea for a bit, building my own headless CMS stopped making sense, for a few reasons:
So, I decided to use an existing headless CMS instead. The plan in my head was simple: make an account on one of these platforms, write content with a proper text editor, point it at my site with a webhook, hit publish, and I'd have a clean content pipeline set up in no time.
That's not quite how it went.
I went back to Gemini, described what I wanted to do, and it told me Sanity was the most popular choice among developers and small startups. I didn't research too many alternatives, since I wasn't planning to write especially complex content, so I just went with it.
I made an account on Sanity, and to my surprise, the first thing it gave me wasn't a text editor, it was a snippet of code to run in my terminal to spin up something called a "studio." My first reaction was something like, what is this, I just wanted a text editor.
It turned out the text editor was coming, just not in the form I expected. What I was actually setting up was the "Sanity Studio," a portal that would be entirely my own. I followed the setup steps and got it running. Then it asked me to define a "schema," and that's when it clicked: I was building my own headless CMS after all, just with the boilerplate (the editor, the infrastructure) handled by Sanity, while I built the parts that actually mattered for learning.
It wasn't exactly what I'd pictured going in, but I was happy with it. I was learning more about how content management systems work than I expected to.
After talking it through with an LLM, I decided to restructure my project into a pnpm workspace monorepo, with a shared package for types and schemas that both the portfolio app and the Sanity Studio could use. The alternative would have been to host the studio inside the portfolio app itself, at something like `/admin`, but Sanity specifically recommends against that.
Before getting into how I actually configured and wired up Sanity in my portfolio, it's worth covering some general background on content management, since it helped me understand why headless CMS platforms exist in the first place.
If you think about it, there are really only a handful of fundamentally different ways to manage content, with variations on each. At its core, content management comes down to two questions: how is content added, and how is it delivered?
There's no single "best" answer here. It depends on why you need content management in the first place: how complex the content is, how many people are writing it, whether there's a hierarchy of writers and editors, whether your writers even know HTML or Markdown, and on the performance side, how fast you need pages to load and how much dynamic behavior you actually need.
Roughly, the history looks like this:
Today, you'll still find everything in active use, from raw source-code editing to advanced CMS platforms with full role-based access control for writers, senior editors, and publishers. What you should use really just depends on what you need.
With the background out of the way, the last step was actually wiring all this into my project.
I went to Claude, explained the setup, and it generated a working implementation that matched what I needed. It defined schemas for "posts" and "tags," where each post had a title, slug, excerpt, cover image, related tags, and the content body itself, which included support for YouTube embeds, callout blocks, code blocks, and basic font styling. It also generated the corresponding TypeScript types, a few utility functions like readingTime, and I fed the schema into Sanity Studio.
From there, it built out the components to render the content on the page, set up a webhook, and created the /blog/[slug] page using static generation with Next.js ISR for revalidation.
By the end I had a mostly working setup, with two outstanding problems:
I spent a day deep in Sanity's docs, going back and forth with both Claude and Gemini, and once I felt like I had a real handle on it, I rebuilt parts of the code myself, without LLM help, partly to fix the remaining bugs and partly just to test what I'd actually learned.
A few of the things I changed:
sanity.cli.ts file that normally lives in the Sanity Studio app. Since my shared sanity package was the single source of truth for types and schemas, I couldn't generate types from inside the studio app, so I had to restructure things to make typegen work from the shared package.After a few hours of debugging, everything came together and started working the way it was supposed to.
Once it was all running, I figured this entire experience, the detours, the wrong turns, the things I learned along the way, was worth writing about. It seemed like a fitting topic for my first blog post.
If you want to see exactly how I set up the monorepo, schemas, and everything else, the full code is up on GitHub here, along with READMEs covering the important parts. The rest should make sense from reading the code itself.
Thanks for reading. If you're working through something similar, I'd say: let yourself take the detour. I learned a lot more from the wrong turns here than I would have from just shipping the obvious solution.
Thanks for reading. More posts on the blog.