Skip to content
Astro Strapi Headless CMS Web Architecture

Why Astro + Strapi Is My Default Stack for Content-Driven Sites

Sanin Mulić 8 min read

I've shipped a dozen marketing sites on this stack, plus two product launches and the portfolio you're reading. 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. This post is the long version of that answer, including the places 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 someone non-technical needs to edit. None of it needs full SPA interactivity on every route, though the whole thing still has to feel considered and expensive.

Traditional WordPress and Next.js both work, and both fit this shape badly. WordPress makes you fight a 20-year-old admin to do anything modern, and its frontend conventions reward template thinking over component thinking. Next.js means shipping a React framework, with all the JS and hydration overhead that comes with it, for what is functionally a static site.

Astro is the framework that finally said “static by default, interactive only where needed.” Strapi takes the same attitude to content modeling: the schema lives in code and gets reviewed like code, instead of accumulating in an admin UI you click through. Together they cover this shape of project better than anything else I’ve tried.

Why Astro

Speed gets the headlines, and it is fast. The real selling point is the architectural discipline Astro refuses to let you skip: you 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 are static, with interactivity only where it’s needed.

Astro also stays out of your way. The tooling is small, and the whole mental model fits in your head. Builds are quick, and when something breaks the error usually points at the actual line. Compared to debugging a Next.js hydration mismatch, it 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 excellent and priced like it. 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 gets the unglamorous parts right:

  • Schema is committed code. apps/cms/src/api/<name>/content-types/<name>/schema.json lives 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-graphql exposes everything the REST API does, with i18n built in. You write fragments once and reuse them across queries.
  • i18n is first-class. 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.

One change the docs undersell: Strapi 5 dropped the deprecated entityService API and moved to a documents API that’s pleasant to write seed scripts against. That detail matters a lot once you’re reproducing content across environments.

How the two connect

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 matters more than it looks. Strapi 5’s GraphQL plugin silently caps unbounded queries at 10 results, so 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.

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:

  1. 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.
  2. 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.
  3. 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 quiet win of this stack is that it stops fighting you about month three. Once the schema design and the GraphQL fragment plumbing are in place, adding a new content type or a new locale costs very little. Sites I built two years ago on Astro + Strapi are still on the same major versions and still fast, and I can hand any of them to another developer without a week of archaeology.

Two years in, I have yet to regret the choice on a single project.