Why Astro + Strapi Is My Default Stack for Content-Driven Sites
After shipping a dozen marketing sites, two product launches, and this portfolio on the same stack, here's why I keep coming back to Astro and Strapi. And where I wouldn't reach for them.
Every few months I get the same question from a small team or a solo founder: "What stack should we use for our marketing site?" My answer for the last two years has been the same. Astro on the frontend, Strapi as the headless CMS. Here's why. And just as important, where the seams show.
The shape of the problem
A "content-driven site" usually means a homepage, a few feature pages, a blog, a contact form, occasional case studies or service pages. Most of it is content that needs to be edited by someone non-technical. None of it needs full SPA interactivity on every route. Some of it needs to feel premium and considered.
Traditional WordPress works. Next.js works. Both are wrong for this shape, in different ways. WordPress because it makes you fight a 20-year-old admin to do anything modern, and the frontend conventions reward template thinking over component thinking. Next.js because you're shipping a React framework, with all the JS, hydration, and complexity that comes with it, for what is functionally a static site.
Astro is the framework that finally said "static by default, interactive when you actually need it." Strapi is the headless CMS that finally said "your schema lives in code, not in a UI you click through." Together they hit a sweet spot the boring options miss.
Why Astro
The selling point isn't speed (though it's fast). It's that Astro forces an architectural discipline most other frameworks let you skip: decide which components need JavaScript and which don't, every time.
In Astro an .astro component renders to HTML at build time and ships zero JS. You opt into interactivity by importing a Svelte (or React, Vue, Solid, whatever) component and adding a client:load or client:visible directive. That single decision, "does this need to hydrate?", gets made consciously for every island. The result is sites that feel snappy because they're actually static, with surgical interactivity where it matters.
The other thing Astro gets right: it doesn't fight you. The tooling is small. The mental model fits in your head. The build is fast. When something breaks, the error usually points at the actual line. Compared to debugging a Next.js hydration mismatch, Astro feels like a vacation.
For this portfolio every section is a static .astro component except the contact form (Svelte 5 with runes), the theme toggle, and the marquee. The total client JS budget is under 30KB gzipped. Try hitting that with Next.js and a comparable feature set.
Why Strapi
I tried the alternatives. Sanity has a beautiful editor but its content modeling is opinionated in ways that fight me. Contentful is genuinely good and genuinely expensive. Payload CMS is promising but ties you to its admin tightly. Decap and Tina are fine for tiny sites and frustrating beyond that.
Strapi 5 hits the boring sweet spot:
- Schema is committed code.
apps/cms/src/api/<name>/content-types/<name>/schema.jsonlives in git. Code review on schema changes. Reproducible across machines. No clicking through admin to wire up a new field. - GraphQL out of the box. The
@strapi/plugin-graphqlexposes everything the REST API does, with i18n built in. You write fragments once and reuse them across queries. - i18n is real. EN + BS, side by side, with proper locale-aware queries. Most CMSes try to bolt this on. Strapi treats it as a primary concern.
- Self-host or Strapi Cloud. I run mine on Strapi Cloud for production (managed Postgres, automatic backups), SQLite locally for dev. The data model is identical.
The thing nobody tells you: Strapi 5 dropped the deprecated entityService API and moved to a documents API that's actually pleasant to write seed scripts against. That detail matters enormously when you're reproducing content across environments.
The integration that makes it sing
Here's the pattern I use on every project now:
// apps/web/src/api-strapi/queries/blog-posts.ts
export async function getAllBlogPosts(locale: Locale = DEFAULT_LOCALE): Promise<BlogPost[]> {
const data = await strapiFetch<{ blogPosts: BlogPost[] }>(
`query GetAllBlogPosts($locale: I18NLocaleCode!, $limit: Int!) {
blogPosts(locale: $locale, sort: ["publishedAt:desc"], pagination: { limit: $limit }) {
...BlogPostFields
}
}
${BLOG_POST_BUNDLE}`,
{ locale, limit: FETCH_ALL_LIMIT },
);
return data.blogPosts;
}That pagination: { limit: N } line looks like an implementation detail. It isn't. Strapi 5's GraphQL plugin silently caps unbounded queries at 10 results. The first time you hit that in production with a "list all" query, you'll spend 90 minutes debugging a missing-data ghost. Now I write FETCH_ALL_LIMIT = 100 once at the top of every queries file and never think about it again.
The build flow is then: Strapi Cloud serves data over GraphQL, Astro fetches at build time, Cloudflare Workers Builds deploys the static output. The contact form is a single Astro endpoint with prerender = false, served by the same Worker. End-to-end Time-to-First-Byte is around 80ms globally. Pages feel instant because they are.
Where I wouldn't use this stack
I'd reach for something else when:
- The product is a real application. Dashboards, CRMs, anything authenticated and interactive on every page. Astro's static-first model fights you here. Use Next.js, SvelteKit, or Remix.
- The team already speaks WordPress fluently and the editorial workflow is the bottleneck. Don't impose a new CMS on a working team.
- You need real-time collaboration in the editor (Notion-style multiplayer). Strapi is single-user. You'll want Sanity or a custom solution.
What I'd build differently next time
A few things I've learned the hard way and would do differently on the next project:
- Set up the build-time SEO audit on day one. I added it later for this portfolio (
apps/web/src/plugins/seo-audit.ts). Having it gate every PR from the start would have saved me from a few late-stage SEO scrambles. - Pick the i18n locale list before designing components. Components that didn't anticipate BS rendering had to be reworked. Doing locale-aware design from the first sketch is cheaper.
- Decide the deploy target before the build. Cloudflare Workers Builds, Vercel, and Netlify each have small but real differences in how they handle headers, redirects, and edge functions. Picking late means refactoring config later.
The compounding return
The boring win of this stack is that it stops fighting you about month three. After the initial schema design and the GraphQL fragment plumbing, the marginal cost of adding a new content type, a new page, or a new locale is small. Sites I built two years ago on Astro + Strapi are still on the same major versions, still fast, still maintainable. The technology choices that age well are usually the ones that didn't try to be exciting.
Astro and Strapi are not exciting. They are correct.