How to Use Strapi with Astro: Complete Guide
Step-by-step guide to integrating Strapi with your Astro website.
Strapi is an open-source headless CMS that gives you full control over your content API. Unlike hosted CMS platforms, you can self-host Strapi on your own server, customize the admin panel, and own your data completely. Pairing it with Astro means you get a flexible backend for content management and a fast static frontend for your visitors.
I have used this combo on a few projects now, and the developer experience is genuinely good. Strapi handles content modeling and the admin UI while Astro builds the pages at compile time. The result is a site that loads fast and costs almost nothing to host.
Prerequisites
- Node.js 20, 22, or 24 (Strapi 5 only supports Active LTS or Maintenance LTS releases, so 18 is no longer supported)
- An Astro project (
npm create astro@latest). This guide targets Astro 6.x, the current major (6.4.2 at time of writing) - A running Strapi instance (local or cloud). You can spin one up with
npx create-strapi-app@latest my-project. The interactive CLI now asks you to log in or skip; pick Skip to create a local project on the free plan. The old--quickstartflag was deprecated in Strapi 5; use--non-interactiveif you want to bypass the prompts instead - Strapi 5.x (current is 5.47.0). If you are still on Strapi 4 the data shape differs, see the response-format note in Basic Usage below
A Note on Astro Rendering Modes (Astro 5 and 6)
Older Astro tutorials reference output: 'hybrid'. That option was removed in Astro 5 and does not exist in Astro 6. The current model is simpler:
staticis the default. Every page is prerendered to HTML at build time. You do not set anything for this.serverserver-renders every page on demand and requires an adapter.- To mix the two, leave the default
staticoutput and opt individual routes into on-demand rendering withexport const prerender = false. That still requires an installed adapter (for example@astrojs/node, current 10.1.2).
For a content site backed by Strapi, the default static output with getStaticPaths is usually what you want: pages are built once and served as plain HTML. Reach for an adapter and prerender = false only when you need request-time data such as previews or per-user content.
Installation
Strapi exposes a REST API by default, so you do not need a special SDK. However, installing a query-string helper makes filtering and population cleaner:
npm install qs
The qs package (current 6.15.2) helps build query strings for Strapi's filtering and population parameters. If you prefer zero dependencies, the official Astro Strapi guide builds query strings with the native URL and URLSearchParams instead, so qs is optional.
Configuration
Add your Strapi URL to a .env file:
STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=your_api_token_here
Create the API token in your Strapi admin panel under Settings > API Tokens. Use a "Read-only" token for your frontend.
Now set up a helper to fetch from Strapi:
// src/lib/strapi.ts
import qs from "qs";
const STRAPI_URL = import.meta.env.STRAPI_URL || "http://localhost:1337";
const STRAPI_TOKEN = import.meta.env.STRAPI_API_TOKEN;
export async function fetchFromStrapi(endpoint: string, params?: Record<string, any>) {
const query = params ? `?${qs.stringify(params, { encodeValuesOnly: true })}` : "";
const res = await fetch(`${STRAPI_URL}/api/${endpoint}${query}`, {
headers: {
Authorization: `Bearer ${STRAPI_TOKEN}`,
},
});
if (!res.ok) {
throw new Error(`Strapi error: ${res.status} ${res.statusText}`);
}
const json = await res.json();
return json.data;
}
Basic Usage
Important: the Strapi 5 response shape changed
This is the single most common thing that trips people up moving from old tutorials. Strapi 5 flattened the REST response. Fields are no longer wrapped in an attributes object, and each entry is identified by a documentId string rather than a numeric id.
Strapi 4 returned:
{ "data": [ { "id": 14, "attributes": { "title": "Article A", "slug": "article-a" } } ] }
Strapi 5 returns:
{ "data": [ { "documentId": "clkgylmcc000008lcdd868feh", "title": "Article A", "slug": "article-a" } ] }
So you read post.title, not post.attributes.title. If you have a large old codebase you cannot refactor immediately, Strapi 5 lets you send a Strapi-Response-Format: v4 request header to temporarily restore the old data.attributes.* wrapping while you migrate. Note that even with that header documentId stays the canonical identifier.
Assuming you have a "Post" collection type in Strapi with fields like title, slug, content, and publishedAt, here is how to fetch and display posts against Strapi 5:
---
// src/pages/blog/index.astro
import { fetchFromStrapi } from "../../lib/strapi";
import BaseLayout from "../../layouts/BaseLayout.astro";
const posts = await fetchFromStrapi("posts", {
sort: ["publishedAt:desc"],
populate: ["cover"],
fields: ["title", "slug", "publishedAt", "description"],
});
---
<BaseLayout title="Blog">
<h1>Blog</h1>
<ul>
{posts.map((post: any) => (
<li>
<a href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</a>
</li>
))}
</ul>
</BaseLayout>
For individual post pages, use getStaticPaths so each page is prerendered at build time (the default static output):
---
// src/pages/blog/[slug].astro
import { fetchFromStrapi } from "../../lib/strapi";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const posts = await fetchFromStrapi("posts", {
fields: ["slug"],
});
return posts.map((post: any) => ({
params: { slug: post.slug },
}));
}
const { slug } = Astro.params;
const posts = await fetchFromStrapi("posts", {
filters: { slug: { $eq: slug } },
populate: "*",
});
const post = posts[0];
---
<BaseLayout title={post.title}>
<article>
<h1>{post.title}</h1>
<div set:html={post.content} />
</article>
</BaseLayout>
Production Tips
Use the
populateparameter carefully. Strapi does not populate relations by default. Usepopulate: "*"for quick prototyping, but in production specify exactly which fields you need to keep response sizes small.Set up webhooks for automatic rebuilds. In Strapi, go to Settings > Webhooks and add a URL that triggers your hosting platform's build hook. This way, every time content is published, your Astro site rebuilds automatically.
Cache your API responses. If you are using Astro in the default
staticoutput, the data is fetched once at build time, so there is nothing to cache at runtime. For routes you opt into on-demand rendering (output: 'server'orprerender = falsewith an adapter), consider adding a caching layer (in-memory or Redis) to avoid hitting Strapi on every request.
Use Strapi's built-in image optimization. Strapi generates multiple image sizes automatically. Reference the formats object in your image responses to serve responsive images without a third-party service.
Run Strapi behind a reverse proxy in production. Put Nginx or Caddy in front of Strapi for SSL termination, rate limiting, and security headers. Never expose port 1337 directly to the internet.
Alternatives to Consider
- Contentful if you want a fully hosted solution with no server management. The tradeoff is less customization and vendor lock-in.
- Keystatic if your content team is comfortable with Git workflows. It stores everything in your repository, so there is no external service to manage.
- DatoCMS if you need built-in image optimization and localization out of the box with a cleaner API.
Common Errors and Fixes
Cannot read properties of undefined (reading 'title') after upgrading to Strapi 5. Your code is reading post.attributes.title. Strapi 5 removed the attributes wrapper, so read post.title directly. See the new response format breaking change. If you cannot refactor right away, send the Strapi-Response-Format: v4 request header to keep the old wrapping temporarily.
output: 'hybrid' is reported as an invalid option. That mode was removed in Astro 5 and does not exist in Astro 6. Use the default static output and add export const prerender = false to the specific routes you want rendered on demand, after installing an adapter.
On-demand rendering throws because no adapter is configured. Any route with prerender = false (or output: 'server') needs a server adapter. Add one with npx astro add node (or netlify/vercel/cloudflare) before building.
Empty data array or missing relations. Strapi does not populate relations by default. Add an explicit populate parameter. Use populate: "*" for prototyping, then narrow to the exact fields you need for production.
401 Unauthorized from the Strapi API. Your collection is not public and you are missing a token, or the token lacks read permission. Create a read-only token under Settings then API Tokens, and either make the find/findOne actions public under Settings then Roles then Public, or send the token as a Bearer header.
Node version error from create-strapi-app. Strapi 5 only supports Active LTS or Maintenance LTS Node releases (currently 20, 22, 24). Running it on Node 18 or an odd-numbered release will fail. Switch with nvm before retrying.
Official Docs and Examples
- Astro Strapi guide: https://docs.astro.build/en/guides/cms/strapi/
- Astro on-demand rendering (adapters,
prerender): https://docs.astro.build/en/guides/on-demand-rendering/ - Strapi 5 Quick Start: https://docs.strapi.io/cms/quick-start
- Strapi 5 REST API reference: https://docs.strapi.io/cms/api/rest
- Strapi 5 new response format breaking change: https://docs.strapi.io/cms/migration/v4-to-v5/breaking-changes/new-response-format
- Example repo (Astro 6 + Strapi 5, landing page + blog): https://github.com/PaulBratslavsky/astro-strapi-example-project
Wrapping Up
Strapi and Astro is one of the best self-hosted CMS combinations available. You get complete ownership of your content, a customizable admin panel, and the performance benefits of static site generation. The setup takes about 30 minutes, and from there you have a content pipeline that scales with your needs.
Sources
All versions and facts below were checked on 2026-05-29.
- Astro current version (6.4.2), from npm registry: https://registry.npmjs.org/astro/latest
- Strapi current version (5.47.0), from npm registry: https://registry.npmjs.org/@strapi/strapi/latest
- create-strapi-app current version (5.47.0): https://registry.npmjs.org/create-strapi-app/latest
- qs current version (6.15.2): https://registry.npmjs.org/qs/latest
- @astrojs/node current version (10.1.2): https://registry.npmjs.org/@astrojs/node/latest
- Astro on-demand rendering, output modes (
staticdefault,server,prerender): https://docs.astro.build/en/guides/on-demand-rendering/ - Official Astro Strapi guide (fetch wrapper, env setup): https://docs.astro.build/en/guides/cms/strapi/
- Strapi 5 flattened response format and
documentId(and theStrapi-Response-Format: v4header): https://docs.strapi.io/cms/migration/v4-to-v5/breaking-changes/new-response-format - Strapi 5 Quick Start and supported Node LTS versions: https://docs.strapi.io/cms/quick-start
- Strapi 5 REST API reference: https://docs.strapi.io/cms/api/rest
- Example repo confirming Astro 6 + Strapi 5 pairing: https://github.com/PaulBratslavsky/astro-strapi-example-project
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.