Do I need a CMS for a portfolio only I edit?
I'm the only person who touches my portfolio, so why did I put a headless CMS behind it instead of a couple of files? Here's what Sanity actually buys a solo site, and when plain files would have been the smarter call.
I'm the only person who edits my portfolio. No editorial team, no marketing department, nobody asking me to change a headline at 5pm on a Friday. So when I sat down to rebuild it, the obvious question was: why not just keep my projects in a JSON file and write my blog posts as markdown? That's what most developer portfolios do, and it works fine.
Instead I put everything behind Sanity, a headless CMS. Projects, blog posts, my bio, the site settings: all of it lives in Sanity's database and gets fetched at request time. The short answer to "did I need that?" is no, I didn't need it. The longer answer is that a few specific things got easier once the content had a real shape, and one of them I didn't see coming. This post is about which parts earned their keep and which parts were me overbuilding for an audience of one.
What is a headless CMS, exactly?
A regular CMS like WordPress owns both your content and the HTML it turns into. A headless CMS only owns the content. It hands you structured data through an API and stays completely out of how you render it. Think of it like a fridge: it stores the ingredients in a tidy way, but it has no opinion about what you cook.
Concretely, in my case the ingredients are documents. A blog post is a document with a title, a slug, a published date, an array of tags, and a body. The frontend is a separate Next.js app that asks Sanity for those documents and decides what the page looks like. Here is the actual function my blog post page calls:
export async function getBlogPostBySlug(slug: string): Promise<BlogPost | null> {
const { data } = await sanityFetch({
query: BLOG_POST_BY_SLUG_QUERY,
params: { slug },
})
return data as BlogPost | null
}The page hands in a slug, gets back a typed object, and renders it. Sanity never sees the markup. That separation is the whole idea behind "headless," and it's the thing that makes the rest of this worth talking about.
What does it actually buy me over a JSON file?
The first thing is that I can edit content without shipping code. If I want to fix a typo in my bio or add a project, I open Sanity Studio, change the field, and the live site updates. No git commit, no Vercel rebuild, no waiting. For a JSON file that change is a code change: edit, commit, push, wait for the deploy. None of that is hard, but it's friction, and it's the kind of friction that quietly stops you from fixing small things.
The second thing is the one that surprised me, and it's about shape. In a JSON file, a tag is a string. Two posts tagged "react" only match because the strings happen to be identical. In Sanity, a tag is its own document, and a post holds a reference to it. That sounds like bureaucracy until you try to build a feature on top of it.
Here's the concrete one. The "related posts" section at the bottom of every article finds other posts that share tags with the one you're reading. With references and Sanity's query language (called GROQ), that's a single query:
*[_type == "blogPost" && slug.current != $slug && count(tags[]._ref[@ in $tagIds]) > 0]
| order(publishedAt desc)[0...10] {
_id, title, slug, publishedAt, heroImage,
tags[]->{ _id }
}That reads as: find every blog post that isn't this one, where at least one of its tag references is in the current post's tag list, newest first. The tags[]-> bit follows each reference and pulls back the real tag document. With a JSON file I'd be loading every post into memory and writing the matching logic by hand. Because the data has a shape the database understands, the database does the work, and it does it in one round trip. That's the moment the CMS stopped feeling like overkill and started feeling like the right tool.
The third thing is the one I genuinely didn't plan for. Sanity ships an MCP server, which is just a standard way for an AI agent to read and write content through the API. Because my content is structured documents behind that interface, an agent can draft a blog post or add a project entry without ever touching the codebase. The post you're reading was written that way. With a JSON file in the repo, the same workflow would need filesystem access or a custom API I'd have to build and secure myself.
What does it cost me?
It isn't free, and the costs are real even if none of them are dramatic. The setup is a couple of hours before you write a single word: you define schemas, deploy them to Sanity, wire sanityFetch into Next.js, and drop in a <SanityLive /> component so the cache knows when content changed. A JSON file needs none of that. You just import it.
There's also a mindset shift that nobody warns you about. With a JSON file you can scribble the data however you like and clean it up later. With a CMS you design the content model first, before you've built the UI. That's good discipline, but it's slower at the very start, when you mostly want to see something on the screen.
Things that surprised me
A few things I only learned by living with it:
- Deploy your schema before you write any content. I did it the other way around, and changing a schema while documents already exist is fiddly and occasionally confusing. The empty-database version of this job takes thirty seconds.
- Local development gets a touch slower. Every page that reads from Sanity makes a network call. In production the CDN and cache tagging make that disappear, but in dev you feel it next to a local file read. It never blocked me, but I noticed.
- Your content now lives somewhere you don't own. Sanity is generous on the free tier and I've had no trouble, but if they change pricing or disappear, I'm exporting and migrating. That's a worry a git repo full of files simply doesn't have.
- The MCP angle made the CMS pay for itself in a way I never anticipated when I chose it. I picked Sanity for the editing and the structure. The fact that an agent can manage the whole site through the same API was a bonus I stumbled into.
So, was it worth it?
Here's the line I'd draw, and it has nothing to do with being a solo developer. It's about whether your content has relationships. If you're building features that lean on shape (filtering posts by tag, finding related articles, ordering projects, querying across documents) then a CMS pays for itself, because the database does that work instead of you. My portfolio does all of those things, so for me it was the right call, and I'd choose it again.
If your content is just text and images with no relationships between the pieces, the answer flips. A static "here are my three projects" site with no blog and no plans to update more than once a year does not need any of this. A couple of markdown files and a JSON array would have been the smarter, faster, more honest choice, and you'd never miss what you skipped.
So the question I started with, do I need a CMS for a portfolio only I edit, turns out to be the wrong question. Being the only editor was never the deciding factor. The deciding factor was that I wanted to build things on top of my content, and structured content is what makes that possible. If you want to see the schemas or the Next.js wiring, the source is on GitHub.


