Why I Migrated from Keystatic to Strapi (and Kept Astro Despite the Lighthouse Score)

22 Feb 2026

Why I Migrated from Keystatic to Strapi (and Kept Astro Despite the Lighthouse Score)

Keystatic was working perfectly. File-based CMS, powered by Git, zero cost. I'd write in Markdown, commit, deploy. No database, no backend, just files. Clean and simple.

But I kept noticing small frictions that added up.

Every time I wanted to add an image to a blog post, I'd go through the same routine: export from Lightroom, resize for web, convert to WebP, move to 
/public/images/blog/
, reference it in Markdown with the exact path. It worked, but it was manual. Each image took maybe five minutes to prep, and when you're writing a technical post with eight screenshots, that's 40 minutes just handling media.

Then there was the editing experience itself. I'm comfortable in VS Code and I like Git-based workflows, but sometimes I just wanted to fix a typo or update a paragraph without opening my editor, staging changes, and pushing commits. For quick edits, the ceremony felt heavier than it needed to be.

And I started thinking about where I wanted this site to go. Right now it's a blog. But I've been meaning to add case studies for freelance projects, maybe a portfolio section with filterable categories, possibly a newsletter integration. Keystatic's file-based structure is elegant for simple content, but I could see the edges of what it was designed for.

The real moment that shifted my thinking came when I was reading about headless CMS architecture. I'd worked with traditional CMS platforms before, WordPress, some Ghost, but I'd never built anything with a true API-first backend. As someone with 10+ years of experience, that felt like a gap worth closing. Not because file-based systems are wrong, but because understanding headless architecture would make me better at evaluating tools and building systems. So this wasn't about Keystatic failing. It was about recognizing I'd outgrown it for what I wanted to build next.

Why Keystatic Worked (Until It Didn't)

Keystatic is genuinely great for what it does. When you're building a personal blog and you're comfortable with Git and Markdown, it's hard to beat. The content lives in your repo, version-controlled by default. Your schema is defined in code, type-safe, predictable. There's no database to maintain, no backend to deploy, no monthly bills.

For a solo developer writing technical posts, it's a perfect fit. I could open VS Code, write in Markdown with proper syntax highlighting, commit, and deploy. The entire workflow matched how I already think about code.

But as I started planning beyond just blog posts, I noticed the constraints. Keystatic stores everything as 
.mdoc
 files, which is fine for straightforward content. But when I thought about adding case studies with multiple images, client testimonials, tech stack tags, and project categories, the file structure started to feel rigid. I could make it work, but I'd be fighting the tool instead of using it.

Media management was the clearest pain point. Each image meant a manual workflow: resize, optimize, move to the right folder, reference the exact path. If I wanted to change an image later, I'd have to track down the file, replace it, make sure the path still matched. There was no media library, no preview, no way to see all images across posts. Just files in folders.

And while I appreciated the Git-based workflow for major updates, sometimes I just wanted to fix a typo on my phone or make a quick edit without pulling out my laptop. Keystatic doesn't have a mobile-friendly admin panel. The editing experience assumes you're at your desk with VS Code open.

None of this made Keystatic bad. It just meant I was ready for something more flexible.

Why Strapi?

I needed something that could handle where I wanted to go, not just where I was. That meant a headless CMS with an API-first architecture, proper media management, and room to grow.

Strapi kept showing up in conversations. Open source, self-hosted, active community, and actual PostgreSQL support in v5. The previous versions used SQLite by default, which is fine for small projects but felt limiting for anything production-ready. v5 switching to PostgreSQL meant I could treat this like a real database, run queries, add indexes, scale if needed.

Strapi 5
Strapi 5
The admin panel was a big draw. Instead of editing Markdown files in VS Code, I could write in a proper rich-text editor with live previews. Upload images directly to Cloudinary through the interface and they'd auto-optimize to WebP/AVIF, delivered via CDN, all handled automatically. No more manual resizing, no more moving files to 
/public
, no more worrying about paths.

And because it's API-first, I could fetch content at build time in Astro for static generation, but I'd still have the option to query it dynamically later. Want to build a mobile app? The API's there. Want to integrate with a newsletter service? The data's structured and queryable. Want to add a portfolio section with filtering? Just create a new content type.

Workflow Diagram
Workflow Diagram

The cost was reasonable. Railway at $5/month and Cloudinary on the free tier. $5 total compared to Keystatic's $0, but I was gaining proper media management, an admin panel, and architectural flexibility. That felt worth it.

Migrating meant learning how Strapi v5's new API worked (the populate syntax changed completely), figuring out how to parse Keystatic's 
.mdoc
 format, and writing a script to normalize three different image reference formats into Cloudinary URLs. It took about 18 hours spread across three days, but the script ran in 2 minutes and migrated 12 posts, 165 images, and 8 tags without issues.

More importantly, I now understood headless CMS architecture from actually building with it. That's knowledge I can apply to client projects, technical decisions, and future systems.

Why Keep Astro?

When you're migrating the backend, it's tempting to redo the frontend too. I could've switched to Next.js for React Server Components, or Nuxt for Vue-based SSR, or SvelteKit for modern reactive patterns.

But here's the thing: my blog doesn't need server-side rendering. The content changes maybe once a week. There's no real-time data, no user sessions, no personalized feeds. It's a blog. Static pages work perfectly.

Astro's entire design philosophy is ship less JavaScript. By default, components render to HTML at build time. No hydration, no client-side framework, just static markup. If you need interactivity, you opt into it explicitly with client directives. That's the opposite of Next.js or Nuxt, where you start with full JavaScript and have to work to reduce it.

For a blog, that default makes sense. Readers don't need React to read text. They need fast-loading pages with good SEO and minimal layout shift. Astro delivers that without me having to think about it.

The integration with Strapi was straightforward. At build time, Astro fetches all posts from the Strapi API, generates static HTML for each one, and deploys. Users get instant page loads because they're just serving pre-rendered files. No API latency, no loading spinners, no "content is being fetched" states.

I also get to keep the Lighthouse performance scores that static sites naturally achieve. No framework overhead, no unnecessary JavaScript, just clean HTML and CSS. When I did add the Cloudinary images, the score dropped from 100 to 77 because of third-party cookies, but the actual performance stayed excellent because images were still loading from a global CDN.

Astro also has a great developer experience. The component syntax is intuitive, basically HTML with JavaScript expressions. The build process is fast. The documentation is clear. I can focus on writing content instead of wrestling with the framework.

So while I could've migrated to a full-stack framework, I didn't need to. Astro + Strapi gives me a static frontend with a dynamic backend. That split makes sense for this use case.

The Lighthouse Trade-off

After deploying with Strapi and Cloudinary, I ran Lighthouse. The Best Practices score dropped from 100 to 77.

The reason: there're few third-party cookies from 
res.cloudinary.com
. I had two options. I could proxy images through Astro's 
Image
 component. It would fetch from Cloudinary at build time, process them, serve them from my own domain. That would bring the score back to 100 because there'd be no third-party cookies.
Lighthouse Score
Lighthouse Score

But there was a UX cost. When I tested it, images showed a blank placeholder first, then loaded. It was subtle, maybe 200-400ms, but noticeable. Users would see empty boxes before images appeared.

The alternative was to serve images directly from Cloudinary. The URLs would include 
res.cloudinary.com
, which meant third-party cookies for CDN routing. Lighthouse would flag it. But users would see images instantly because they're loaded from the nearest edge location with no proxy overhead.

I chose instant loading.

The cookies Lighthouse was flagging aren't tracking cookies. They're functional cookies used by Cloudinary's CDN for edge routing and session management. No cross-site tracking, no data selling, just CDN optimization. GDPR considers these legitimate interest because they're necessary for the service.

Lighthouse flags all third-party cookies equally because it can't distinguish between tracking and functional. But context matters. These cookies enable faster image delivery. That's a trade I'm comfortable making.

The other benefit of direct URLs is simpler builds. Proxying 165 images through Astro at build time added processing overhead and increased build time by a couple seconds. Direct URLs mean zero image processing at build time. Faster builds, faster deploys, simpler pipeline.

So I kept the 77 score. The site loads fast, images appear instantly, and the architecture is straightforward. That matters more than a perfect audit score.

What I Learned

This migration taught me more about architectural decisions than I expected.

Free isn't always optimal. Keystatic's $0 cost was great, but $5/month for Strapi bought me better media management, an actual admin panel, and flexibility to expand. Sometimes paying a small amount gets you meaningful improvements.

Not every project needs server-side rendering. I could've migrated to Next.js and had access to React Server Components, incremental static regeneration, all the modern patterns. But my blog doesn't need any of that. Static pages generated at build time work perfectly. Choosing tools based on actual requirements instead of trends saves complexity.

Lighthouse scores are useful guides, not absolute targets. A 77 with instant image loading serves users better than a 100 with blank loading states. Metrics should support user experience, not replace it.

Understanding headless CMS architecture from building with it is different from just reading about it. I now know how populate syntax works in Strapi v5, how to structure relations, how to optimize API calls for build-time fetching. That knowledge applies beyond this blog.

Migration is mostly about understanding data structure. The actual code to migrate, parsing 
.mdoc
 files, normalizing image references, uploading to Cloudinary, took time to write but was conceptually straightforward. The harder part was mapping Keystatic's structure to Strapi's, understanding what gets lost in translation, making sure nothing broke. That's true for any migration, not just CMS changes.

When This Makes Sense

Keystatic is excellent if you're comfortable with Git, you're writing straightforward blog posts, and you want zero hosting costs. The file-based approach is elegant and the version control is built-in.

Strapi makes sense when you need structured content with relations, better media management, or you're planning to expand beyond basic blogging. If you're building a portfolio with case studies, or you want an admin panel for easier editing, or you're considering a mobile app that needs an API, the headless CMS architecture fits.

The question isn't which tool is better. It's which tool matches what you're building. For where I was a year ago, Keystatic was the right choice. For where I'm going now, Strapi makes more sense.

Closing: The Right Tool for the Job

Keystatic didn't fail. I just outgrew what it was designed for. That's fine. Tools should match your constraints, not the other way around.

I still have a 77 Lighthouse score. My blog still loads in under a second. I can now edit posts from my phone if I want. I have a proper media library. The architecture is ready for portfolio sections, case studies, or whatever I build next.

That's what success looks like.

What's Next: The Lighthouse Dilemma

After getting the blog live with Strapi and Cloudinary, I spent more time looking at performance optimization.

I experimented with Astro's Image component to see if I could get back to that perfect 100 score. The results were interesting, and not what I expected.

In the next post, I'll break down the technical details: why Astro's image proxy caused blank loading states, how Cloudinary's transformations work at the CDN level, and why direct URLs ended up being faster than build-time optimization. There's also the hybrid approach I tried (mixing direct URLs for hero images and proxied URLs for galleries) and why that added complexity without real benefits.

The core lesson: Lighthouse is a guide, not gospel. Real users care about instant visual feedback, not cookie warnings they never see.