<!-- Generated from llms-txt-cms/index.html. The canonical document is the HTML page. -->

- [ Home ](/) 
/
- [ How to create ](/how-to-create/) 
/
- Headless CMS guide            
# llms.txt for headless CMS

Headless CMS platforms (Contentful, Sanity, Strapi, Directus) don't serve files directly, you generate llms.txt at build time from CMS data, or fetch it at request time via a server route.

Last updated: April 22, 2026

Using a hosted website builder instead of a headless CMS? See the dedicated guides for
[Shopify](/llms-txt-shopify/), [Webflow](/llms-txt-webflow/),
[Wix](/llms-txt-wix/) and [Squarespace](/llms-txt-squarespace/).

## The core pattern: CMS API → build step → static file

Headless CMS platforms manage content but do not serve arbitrary files at arbitrary paths. To
publish `llms.txt`, you need to pull content from your CMS and write the file
yourself, either at **build time** (static output) or at **request time** (server route).

- **Query your CMS**, fetch published pages with title, slug, and summary fields. 
- **Map to Markdown links**, format each entry as `- [Title](https://url/): description.` 
- **Write or return the file**, write to `public/llms.txt` at build time,
or return from a server route at request time.     
ℹ Static vs dynamic

Static generation is simpler and faster, the file is cached by your CDN. Dynamic generation
(server route) ensures the file is always up to date without a rebuild. For most CMS-backed
sites, static generation with a daily or per-deploy rebuild is the right choice.

## Contentful example

Use the [Contentful JavaScript SDK](https://github.com/contentful/contentful.js) 
to fetch entries via the Content Delivery API. Run this script during your build step (e.g., in `package.json` scripts or your CI pipeline) before the framework build:
scripts/generate-llms-txt.mjs (Contentful)   
Copy

```
// scripts/generate-llms-txt.mjs
// Contentful: fetch published doc entries and generate llms.txt at build time

import { createClient } from 'contentful';
import { writeFileSync } from 'fs';

const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});

async function generateLlmsTxt() {
// Fetch entries of content type 'docPage' sorted by display order
const entries = await client.getEntries({
content_type: 'docPage',
order: 'fields.order',
select: 'fields.title,fields.slug,fields.summary',
limit: 100,
});

const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

const links = entries.items
.map((entry) => {
const { title, slug, summary } = entry.fields;
return `- [${title}](${SITE_URL}/docs/${slug}/): ${summary ?? ''}`;
})
.join('\n');

const content = [
'# Your Site',
'',
'> One-sentence description of your product.',
'',
'## Documentation',
'',
links,
'',
'## Optional',
'',
`- [Changelog](${SITE_URL}/changelog/): Release history.`,
].join('\n');

writeFileSync('public/llms.txt', content, 'utf-8');
console.log(`Generated llms.txt with ${entries.items.length} entries.`);
}

generateLlmsTxt().catch(console.error);

```

Add the script to your build pipeline:
package.json   
Copy

```
{
"scripts": {
"prebuild": "node scripts/generate-llms-txt.mjs",
"build": "next build"
}
}
```

## Sanity example

Use GROQ, Sanity&rsquo;s query language, to fetch exactly the fields you need. The `!(_id in path("drafts.**"))` filter ensures only published documents are included:
scripts/generate-llms-txt.mjs (Sanity)   
Copy

```
// scripts/generate-llms-txt.mjs
// Sanity: use GROQ to query published docs and generate llms.txt

import { createClient } from '@sanity/client';
import { writeFileSync } from 'fs';

const client = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: process.env.SANITY_DATASET || 'production',
useCdn: false, // always fetch fresh data at build time
apiVersion: '2024-01-01',
});

async function generateLlmsTxt() {
// GROQ query: fetch all published doc entries with title, slug, and summary
const docs = await client.fetch(
`*[_type == "doc" && !(_id in path("drafts.**"))] | order(order asc) {
title,
"slug": slug.current,
summary
}`
);

const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

const links = docs
.map((doc) => `- [${doc.title}](${SITE_URL}/docs/${doc.slug}/): ${doc.summary ?? ''}`)
.join('\n');

const content = [
'# Your Site',
'',
'> One-sentence description of your product.',
'',
'## Documentation',
'',
links,
].join('\n');

writeFileSync('public/llms.txt', content, 'utf-8');
console.log(`Generated llms.txt with ${docs.length} docs.`);
}

generateLlmsTxt().catch(console.error);

```

## Strapi example

Strapi exposes a REST API at `/api/:collection`. Use the `fields` and `filters` query parameters to fetch only published documents with
the fields you need:
scripts/generate-llms-txt.mjs (Strapi)   
Copy

```
// scripts/generate-llms-txt.mjs
// Strapi v4/v5: fetch published articles via REST API and generate llms.txt

import { writeFileSync } from 'fs';

const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN;
const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

async function generateLlmsTxt() {
// Fetch published docs, adjust the collection slug and fields as needed
const res = await fetch(
`${STRAPI_URL}/api/docs?fields[0]=title&fields[1]=slug&fields[2]=summary&filters[publishedAt][$notNull]=true&pagination[limit]=100`,
{
headers: STRAPI_TOKEN ? { Authorization: `Bearer ${STRAPI_TOKEN}` } : {},
}
);

if (!res.ok) throw new Error(`Strapi API error: ${res.status}`);
const { data } = await res.json();

const links = data
.map((item) => {
const { title, slug, summary } = item.attributes ?? item; // v4 vs v5
return `- [${title}](${SITE_URL}/docs/${slug}/): ${summary ?? ''}`;
})
.join('\n');

const content = [
'# Your Site',
'',
'> One-sentence description of your product.',
'',
'## Documentation',
'',
links,
].join('\n');

writeFileSync('public/llms.txt', content, 'utf-8');
console.log(`Generated llms.txt with ${data.length} entries.`);
}

generateLlmsTxt().catch(console.error);

```

Strapi v4 wraps response data in an `attributes` object; Strapi v5 returns fields at the
top level. The example handles both with a fallback.

## Integration with Next.js route handler

If you want the file to be always in sync without a full rebuild, use a Next.js App Router route
handler with `revalidate` set to your desired TTL. Next.js will cache the response and
regenerate it in the background:
app/llms.txt/route.ts (Next.js + Contentful)   
Copy

```
// app/llms.txt/route.ts
// Next.js App Router, fetch from CMS at request time (or cache with revalidate)

import { NextResponse } from 'next/server';
import { createClient } from 'contentful';

// Cache for 1 hour (Next.js incremental static regeneration)
export const revalidate = 3600;

const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});

export async function GET() {
const entries = await client.getEntries({
content_type: 'docPage',
order: 'fields.order',
select: 'fields.title,fields.slug,fields.summary',
limit: 100,
});

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yoursite.com';

const links = entries.items
.map((e: any) => `- [${e.fields.title}](${SITE_URL}/docs/${e.fields.slug}/): ${e.fields.summary ?? ''}`)
.join('\n');

const body = [
'# Your Site',
'',
'> One-sentence description.',
'',
'## Documentation',
'',
links,
].join('\n');

return new NextResponse(body, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
},
});
}

```

## Integration with Astro endpoint

In Astro, create a `src/pages/llms.txt.ts` endpoint with `export const prerender = true` to generate the file at build time. Astro will call the
CMS API during `astro build` and write the static file to `dist/llms.txt`:
src/pages/llms.txt.ts (Astro + Sanity)   
Copy

```
// src/pages/llms.txt.ts
// Astro endpoint, fetch from CMS at build time (static generation)

import type { APIRoute } from 'astro';
import { createClient } from '@sanity/client';

// This endpoint is pre-rendered at build time
export const prerender = true;

const sanity = createClient({
projectId: import.meta.env.SANITY_PROJECT_ID,
dataset: import.meta.env.SANITY_DATASET || 'production',
useCdn: false,
apiVersion: '2024-01-01',
});

export const GET: APIRoute = async () => {
const docs = await sanity.fetch(
`*[_type == "doc" && !(_id in path("drafts.**"))] | order(order asc) {
title, "slug": slug.current, summary
}`
);

const SITE_URL = import.meta.env.SITE_URL || 'https://yoursite.com';

const links = docs
.map((doc: any) => `- [${doc.title}](${SITE_URL}/docs/${doc.slug}/): ${doc.summary ?? ''}`)
.join('\n');

const body = [
'# Your Site',
'',
'> One-sentence description of your product.',
'',
'## Documentation',
'',
links,
].join('\n');

return new Response(body, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
};

```

## Static vs dynamic generation

- **Static (build time)**, faster (CDN-cached), simpler, no runtime CMS dependency.
Best when content changes infrequently or you deploy on every content change. 
- **Dynamic (server route)**, always reflects the latest CMS content. Best when
content changes frequently between deployments, or when you can&rsquo;t trigger a rebuild on
content change. Add a `Cache-Control` header to avoid hammering the CMS API on every
request.   
## Checklist

- Script or endpoint fetches only **published** content (not drafts). 
- All generated URLs are **absolute** (`https://`). 
- File starts with exactly one H1. 
- Blockquote summary immediately follows the H1. 
- Each section has at least one link. 
- File is under 20 KB (curate, don&rsquo;t dump every entry). 
- Build step runs before the framework build (`prebuild` script or CI step). 
- Validated with [llmtxt.info/validator/](/validator/) after each deploy.   
## Related guides

- [How to create llms.txt](/how-to-create/), templates and deployment guide. 
- [Next.js guide](/llms-txt-nextjs/), App Router and static generation. 
- [Astro guide](/llms-txt-astro/), content collections and endpoints. 
- [llms.txt format reference](/llms-txt-format/), spec details. 
- [Validator](/validator/) · [Generator](/generator/).        
## Sources

- [ llmstxt.org, official spec ](https://llmstxt.org/)
- [ Contentful JavaScript SDK ](https://github.com/contentful/contentful.js)
- [ Sanity GROQ reference ](https://www.sanity.io/docs/groq)
- [ Strapi REST API docs ](https://docs.strapi.io/dev-docs/api/rest)           
On this page

- [ The core pattern: CMS API → build step → static file ](#pattern)
- [ Contentful example ](#contentful)
- [ Sanity example ](#sanity)
- [ Strapi example ](#strapi)
- [ Integration with Next.js route handler ](#nextjs)
- [ Integration with Astro endpoint ](#astro)
- [ Static vs dynamic generation ](#static-vs-dynamic)
- [ Checklist ](#checklist)
