
By Sensinum
Integration of Astro with Strapi CMS that enables easy data loading from the Strapi Content API
released November 3, 2026
npm install @sensinum/astro-strapi-loader
Integration of Astro with Strapi CMS that enables easy data loading from Strapi Content API
npm install @sensinum/astro-strapi-loader
# or
yarn add @sensinum/astro-strapi-loadermarkdown option uses Astro renderMarkdown() so render() / <Content /> inherit your project markdown config (closes #6)This repository includes optional, model-agnostic guidance for coding agents and AI-assisted workflows (Strapi REST query objects, populate / dynamic zones, locale, qs serialization, and Astro Collections usage).
| Location | Purpose |
|---|---|
.ai/AGENTS.md | Short agent instructions—non-negotiables and pointers to the skill. |
.ai/astro-strapi-loader/SKILL.md | Full skill (YAML frontmatter + markdown); copy or adapt for your global or project skills directory. |
.cursor/rules/astro-strapi-loader.mdc | Cursor rules for files such as content.config.ts (.mdc with optional globs). |
You can reuse or fork these files in downstream projects that consume @sensinum/astro-strapi-loader so assistants stay aligned with Strapi 5 REST parameters and this package’s behavior. The published npm package contains only dist/; AI assets live in the GitHub tree alongside the source.
src/content.config.ts:import { generateCollections } from '@sensinum/astro-strapi-loader';
let strapiCollections: any = {};
try {
strapiCollections = await generateCollections({
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
headers: {
// ...
},
}, [{ // leave empty [] to fetch all the collections based on default Strapi settings
name: "homepage",
query: {
populate: { seo: true },
},
}, {
name: "layout",
query: {
populate: { header: true, footer: true },
},
}, 'simple-collection-name']); // Can also pass just strings
} catch (error) {
console.error(error);
}
export const collections = {
...strapiCollections,
};✅ Backward Compatible: Existing code works without any changes!
---
import { getCollection } from 'astro:content';
import { fetchContent } from '@sensinum/astro-strapi-loader';
// Basic usage with Astro Collections
const myCollection = await getCollection('my-collection');
// Basic usage with direct Strapi Content API fetch
const myCollection = await fetchContent({
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
contentType: 'my-collection',
queryParams: {
populate: {
// ...
},
filters: {
// ...
},
sort: ['publishedAt:desc'],
},
});
---
<div>
{ myCollection.map(item => (
<article>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
)) }
</div>| Option | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Strapi API URL |
token | string | No | Strapi API access token |
collectionName | string | No | Custom collection name (for multiple collections from same endpoint) |
idGenerator | function | No | Custom function to generate IDs from item data |
locale | string | string[] | No | Single locale or array of locales for i18n support |
markdown | StrapiMarkdownOptions | No | Opt-in body rendering for Astro render() (see Native Markdown) |
headers | Record<string, string> | No | Additional headers for API request |
⚠️ Note: The token must have read access to both the Content API and the Content-Type Builder API (ONLY to the "Get Content Types" endpoint).
You can mix old (simple) and new (extended) format in a single generateCollections call:
strapiCollections = await generateCollections({
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
}, [
// ✅ Old format - works as before
{
name: "homepage",
query: { populate: { seo: true } }
},
{
name: "layout",
query: { populate: { header: true, footer: true } }
},
// ✅ 1.1.0+ - with locale support
{
name: "pages",
collectionName: "pagesEN",
locale: "en",
query: { sort: ['publishedAt:desc'] }
},
{
name: "pages", // Same endpoint, different config!
collectionName: "pagesDE",
locale: "de",
query: { sort: ['publishedAt:desc'] }
},
// ✅ 1.1.0+ - with custom ID
{
name: "blog-posts",
idGenerator: (data) => data.slug as string,
query: { filters: { published: true } }
},
// ✅ 1.1.0+ - combining all features
{
name: "articles",
collectionName: "articlesMultilang",
locale: ["en", "de", "fr"],
idGenerator: (data) => data.slug as string
}
]);
// Result collections:
// - homepage (old format)
// - layout (old format)
// - pagesEN (1.1.0+)
// - pagesDE (1.1.0+)
// - blog-posts (1.1.0+)
// - articlesMultilang (1.1.0+)Use slugs or custom fields as collection IDs instead of Strapi's documentId:
Option A: Using generateCollections:
strapiCollections = await generateCollections({
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
}, [{
name: "pages",
idGenerator: (data) => data.slug as string,
query: { populate: { seo: true } }
}]);
// Now you can use: getEntry('pages', 'about-us')Option B: Using strapiLoader directly:
import { strapiLoader } from '@sensinum/astro-strapi-loader';
import { defineCollection, z } from 'astro:content';
const pages = defineCollection({
loader: strapiLoader('pages', {
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
idGenerator: (data) => data.slug as string
}),
schema: z.object({
slug: z.string(),
title: z.string(),
content: z.string()
})
});Option A: Using generateCollections (recommended for multiple collections):
strapiCollections = await generateCollections({
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
}, [{
name: "pages",
collectionName: "pagesEN",
locale: "en",
query: { sort: ['publishedAt:desc'] }
}, {
name: "pages", // Same endpoint!
collectionName: "pagesDE",
locale: "de",
query: { sort: ['publishedAt:desc'] }
}]);
// Now you have both 'pagesEN' and 'pagesDE' collectionsOption B: Using strapiLoader directly:
const pagesEN = defineCollection({
loader: strapiLoader('pages', {
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
collectionName: 'pagesEN',
locale: 'en'
}),
schema: pageSchema
});
const pagesDE = defineCollection({
loader: strapiLoader('pages', {
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
collectionName: 'pagesDE',
locale: 'de'
}),
schema: pageSchema
});
export const collections = { pagesEN, pagesDE };Fetch all language versions in one collection:
const pagesMultilang = defineCollection({
loader: strapiLoader('pages', {
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
locale: ['en', 'de', 'fr'] // Array of locales
}),
schema: z.object({
title: z.string(),
content: z.string(),
_locale: z.string() // Automatically added by loader
})
});
// Access by locale-prefixed ID
const page = await getEntry('pagesMultilang', 'en:documentId');
// Or filter by locale
const allPages = await getCollection('pagesMultilang');
const enPages = allPages.filter(p => p.data._locale === 'en');const blogMultilang = defineCollection({
loader: strapiLoader('blog-posts', {
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
collectionName: 'blogAllLanguages',
locale: ['en', 'de', 'fr'],
idGenerator: (data) => data.slug as string
}, {
sort: ['publishedAt:desc'],
filters: { status: { $eq: 'published' } }
}),
schema: blogSchema
});
// Access: getEntry('blogAllLanguages', 'en:my-post-slug')Opt-in support for Astro’s Content Layer render() / <Content />, using the same markdown pipeline as local .md files (Loader renderMarkdown). When markdown is omitted, the loader behavior is unchanged.
StrapiMarkdownOptions
| Option | Type | Default | Description |
|---|---|---|---|
field | string | — | Dot-path to a markdown string on the entry (e.g. content, seo.description) |
getMarkdown | (data) => string | null | undefined | Promise<…> | — | Custom extractor (wins over field) |
includeBody | boolean | true | Also store raw source on entry.body |
format | "markdown" | "html" | "markdown" | markdown → renderMarkdown(); html → pass-through for already-rendered HTML |
Helpers exported from the package: getValueByPath, resolveMarkdownSource, createHtmlRenderedContent, buildEntryRenderedContent.
With generateCollections:
strapiCollections = await generateCollections({
url: import.meta.env.STRAPI_URL,
token: import.meta.env.STRAPI_TOKEN,
}, [{
name: "articles",
idGenerator: (data) => data.slug as string,
markdown: { field: "content" },
query: { populate: { seo: true } },
}]);With getMarkdown (compose / nested fields):
markdown: {
getMarkdown: (data) => {
const blocks = data.sections as Array<{ body?: string }> | undefined;
return blocks?.map((b) => b.body).filter(Boolean).join("\n\n");
},
}In a page:
---
import { getEntry, render } from 'astro:content';
const entry = await getEntry('articles', Astro.params.slug);
const { Content } = await render(entry);
---
<Content />Note: This targets string Markdown (or HTML with
format: "html"). Strapi Blocks remain a separate concern (e.g. a blocks renderer package).
| Option | Type | Description | Documentation |
|---|---|---|---|
filters | object | Strapi Query Builder filters | open |
populate | object / string[] | Populating & selecting relations | open |
fields | string[] | Selecting fields | open |
sort | string[] | Result sorting | open |
pagination | object | Result pagination | open |
yarnyarn devyarn checkWe welcome contributions to this project! Here's how you can help:
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)Please make sure to:
Copyright © Sensinum
This project is licensed under the MIT License - see the LICENSE.md file for details.
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
