/ astro-integrations / How to Use Strapi with Astro: Complete Guide
astro-integrations 4 min read

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 18+
  • An Astro project (npm create astro@latest)
  • A running Strapi instance (local or cloud). You can spin one up with npx create-strapi-app@latest my-project --quickstart
  • Strapi 4.x or 5.x

Installation

Strapi exposes a REST API by default, so you do not need a special SDK. However, installing a fetch wrapper makes things cleaner:

npm install qs

The qs package helps build query strings for Strapi's filtering and population parameters.

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

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:

---
// 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.attributes.slug}`}>
          <h2>{post.attributes.title}</h2>
          <time>{new Date(post.attributes.publishedAt).toLocaleDateString()}</time>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>

For individual post pages, use getStaticPaths:

---
// 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.attributes.slug },
  }));
}

const { slug } = Astro.params;
const posts = await fetchFromStrapi("posts", {
  filters: { slug: { $eq: slug } },
  populate: "*",
});
const post = posts[0];
---

<BaseLayout title={post.attributes.title}>
  <article>
    <h1>{post.attributes.title}</h1>
    <div set:html={post.attributes.content} />
  </article>
</BaseLayout>

Production Tips

  1. Use the populate parameter carefully. Strapi does not populate relations by default. Use populate: "*" 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 static mode, the data is fetched once at build time. For SSR mode, 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.

    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.