llms.txt setup guide for every platform, AIScan cover
llms.txt setup guide for every platform, AIScan cover
AI Readiness

llms.txt on Astro, Docusaurus, Framer and Ghost (and More)

Publish a valid llms.txt on Astro, Docusaurus, Framer and Ghost: exact paths, plan gates, the baseUrl and redirect traps, and the curl check that proves it.

24 Oct 2026 19 min read
#llms.txt#AI readiness#content#AI crawlers

This guide covers C2 · Content.

Table of contents

Updated on 24 October 2026. Sections for Astro, Docusaurus, Framer and Ghost were checked on the dates shown inside each one. The other seven platforms are being folded into this guide and keep their own posts until they arrive.

A valid llms.txt is one file at /llms.txt that returns HTTP 200 and contains an H1, at least one ## section and Markdown links. That is the whole of AIScan's C2 check. What changes from platform to platform is not the file but where you are allowed to put it: a folder, a code endpoint, a paid upload panel or a template route. This guide holds the platform-specific steps in one place, so you can jump to your stack.

TL;DR

  • The pass bar is the same everywhere. C2 wants a 200, one H1, one ## section and Markdown links. It does not check the content type.
  • Astro: drop public/llms.txt, or build src/pages/llms.txt.ts from your content collections. Starlight sites can use the starlight-llms-txt plugin.
  • Docusaurus: static/llms.txt is served under your baseUrl. On a GitHub Pages project site that means /project/llms.txt, and the origin root returns 404.
  • Framer: an upload under Site Settings, Hosting, Files, on the Pro or Enterprise plan only. Use a .txt extension and the root path.
  • Ghost: no setting and no file root. Add a routes.yaml template route with content_type: text/plain, and judge success by the final URL, not the status code.
  • Google does not use the file. Publish it for the agents and tools that fetch it. Whether any do is a separate question, measured in does llms.txt actually work in 2026.

Contents

PlatformWhere the file comes fromSection
Astrostatic file, build-time endpoint, or Starlight pluginAstro
Docusaurusstatic/ folder, a postBuild hook, or an npm pluginDocusaurus
Framera Pro-plan file uploadFramer
Ghosta routes.yaml template routeGhost
Hugostill its own guide: llms.txt on Hugocoming
Next.jsstill its own guide: llms.txt on Next.jscoming
Replitstill its own guide: llms.txt on Replitcoming
Shopifystill its own guide: llms.txt on Shopifycoming
Squarespacestill its own guide: llms.txt on Squarespacecoming
Webflowstill its own guide: llms.txt on Webflowcoming
Wixstill its own guide: llms.txt on Wixcoming

What every platform's file has to contain

The llms.txt spec was proposed by Jeremy Howard of Answer.AI on 3 September 2024. An H1 carrying the project name is the only required section. Version 2, published 10 August 2026, added rel="alternate" and rel="describedby" link relations for discovery. No standards body owns it, and Google's AI optimization guide says Google Search ignores these files. The page was re-fetched on 24 October 2026 and still answers 200.

A minimum passing file:

# Example

> Guides and API reference for Example.

## Docs

- [Quickstart](https://example.com/docs/quickstart): install and first request
- [API reference](https://example.com/docs/api): every endpoint, with examples

If you would rather not hand-roll it, the llms.txt generator drafts one from a URL. The mistakes that break a file from the inside are catalogued in llms.txt validator: the common mistakes, and how it differs from your other root files is in llms.txt vs robots.txt vs sitemap.

How the platforms differ, from the guides folded in so far:

PlatformCost of a valid fileStays current on its ownPlan gateChecked
Astro2 minutes (static) or 15 minutes (endpoint)Yes, with the endpointNone28 August 2026
Docusaurus5 minutes (static), 20 minutes (hook)Yes, with a hook or pluginNone4 September 2026
Framerabout 10 minutesNoPro or Enterprise2 September 2026
Ghostabout 15 minutesYes, the template reads your postsNone30 August 2026

Astro

Verified 28 August 2026. Plugin version re-checked 24 October 2026.

A valid llms.txt on Astro is either a two-minute file drop or a twenty-line endpoint that rebuilds itself from your content collections every time you run astro build. Astro's own documentation site does not publish one (checked 28 August 2026), so there is no upstream example to copy.

If you want toDo thisWhere it livesTime
Ship something valid todayHand-write the file as a static assetpublic/llms.txt2 minutes
Keep it current without editing itGenerate it from getCollection()src/pages/llms.txt.ts15 minutes
Cover a Starlight docs siteInstall starlight-llms-txtastro.config.mjs5 minutes

The endpoint is the version that survives a real blog. A hand-written file is stale the day you publish your next post.

Astro path A: a static file in public/

Astro's project structure guide describes public/ as the home for "non-code, unprocessed assets", and its own example tree puts robots.txt there. Everything in public/ is copied to the build root untouched.

  1. Create public/llms.txt at the top level of your project, beside src/ and astro.config.mjs.
  2. Paste the Markdown above and swap in real absolute URLs.
  3. Run npx astro build and confirm the file appears at dist/llms.txt.
  4. Deploy.

That is a correct answer for a marketing site with eight pages. On anything with a content collection behind it, you will forget to update it.

Astro path B: an endpoint built from your content collections

Astro's endpoints guide says a .js or .ts file in src/pages becomes a route with the extension stripped, so src/pages/data.json.ts builds /data.json. The same rule gives you /llms.txt from src/pages/llms.txt.ts. In a statically generated site, custom endpoints "are called at build time to produce static files", so this costs nothing at runtime.

Set site in astro.config.mjs first, because the spec wants absolute URLs:

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({ site: 'https://example.com' });

Then create the endpoint:

// src/pages/llms.txt.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';

export const GET: APIRoute = async ({ site }) => {
  const posts = (await getCollection('blog', ({ data }) => !data.draft))
    .sort((a, b) => +b.data.pubDate - +a.data.pubDate);

  const body = [
    '# Example',
    '',
    '> Guides, API reference and engineering notes.',
    '',
    '## Blog',
    '',
    ...posts.map(
      (p) => `- [${p.data.title}](${new URL(`/blog/${p.id}/`, site)}): ${p.data.description}`,
    ),
    '',
  ].join('\n');

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

getCollection() takes an optional filter function, which is how drafts stay out. Your src/content.config.ts schema already guarantees that title and description exist on every entry, and that is what makes the generated file reliable rather than hopeful.

Astro path C: Starlight documentation sites

If your docs run on Starlight, starlight-llms-txt does the whole job. It is written by Chris Swithinbank, a Starlight maintainer. Version 0.11.0 was published on 1 July 2026, and npm listed 0.12.0 as the latest on 24 October 2026. Install it with npm i starlight-llms-txt, add starlightLlmsTxt() to the plugins array inside starlight() in astro.config.mjs, and set site. It generates llms.txt, llms-full.txt and llms-small.txt, and the plugin docs tell you to preview at localhost:4321/llms.txt.

Three Astro traps

A trailing slash returns 404. Astro's endpoints guide is explicit: routes whose URLs include a file extension "can only be accessed without a trailing slash", whatever your build.trailingSlash setting says. So /llms.txt/ fails while /llms.txt works. Always test the bare path.

On-demand rendering regenerates the file on every request. If you run an adapter with server output, add export const prerender = true; to the endpoint so it is written once at build time.

Do not chase the content type. C2 does not check it. Both nextjs.org/docs/llms.txt and vercel.com/llms.txt serve text/plain; charset=utf-8, and both pass.

Docusaurus

Verified 4 September 2026. Package versions re-checked 24 October 2026.

A Docusaurus site can publish a completely valid llms.txt and still score zero on it. The reason is one config value. On GitHub Pages, the most common deployment target, a project site lives at https://your-org.github.io/your-project/, so baseUrl is /your-project/ and the file you dropped into static/ answers at /your-project/llms.txt. Nothing answers at the origin root. The build was fine. The URL was wrong.

If you want to…Do thisTimeStays current on its own
Ship a file todayPut llms.txt in static/5 minNo, you edit it by hand
Generate it from your real routesAdd an inline plugin with a postBuild hook20 minYes, every build
Skip writing codeInstall a published plugin from npm10 minYes, every build
Serve it at the origin rootDeploy at baseUrl: '/', or add a host-level rewriteVariesN/A

Why the file lands at a URL agents will not ask for

Docusaurus copies static/ straight into the build output. Its Static Assets page says: "Every file you put into that directory will be copied into the root of the generated build folder with the directory hierarchy preserved." The next paragraph is the one that matters: "for site baseUrl: '/subpath/', the image /static/img/docusaurus.png will be served at /subpath/img/docusaurus.png." According to the deployment guide, "For a site deployed at https://my-org.com/my-project/, baseUrl is /my-project/."

DeploymentbaseUrlstatic/llms.txt is served atOrigin root /llms.txt
docs.example.com (own domain)/https://docs.example.com/llms.txt200
example.com/docs/ (subpath)/docs/https://example.com/docs/llms.txt404
org.github.io/project/ (GitHub Pages project site)/project/https://org.github.io/project/llms.txt404

A subpath file is not invalid. The spec, fetched from llmstxt.org on 4 September 2026, allows it: "The llms.txt file spec is for files named llms.txt, at the root path /llms.txt of a website or at any subpath (e.g. /docs/llms.txt). A file covers the URLs under its path, and where more than one file applies, agents should use the most specific one." So /docs/llms.txt is spec-legal and useful. It is also invisible to any scanner that probes only the origin root, ours included. Know which one you published before you go looking for the score.

Docusaurus route 1: the static folder

  1. Create static/llms.txt at the root of your Docusaurus project, beside docusaurus.config.js.
  2. Write the file, listing pages that answer questions (see below).
  3. Run npm run build, then npm run serve, and open http://localhost:3000 plus your baseUrl and llms.txt.
  4. Deploy.

If your team keeps generated assets somewhere other than static/, the staticDirectories option in docusaurus.config.js takes an array of paths, all copied to the build output as-is.

Docusaurus route 2: a postBuild hook that reads your real routes

Hand-written indexes drift the moment somebody adds a page. According to the lifecycle API reference, postBuild(props) is "Called when a (production) build finishes" and receives siteDir, outDir, baseUrl, siteConfig, routesPaths and routesBuildMetadata, which carries a noIndex flag per location. Add this to docusaurus.config.js as an inline plugin:

plugins: [
  function llmsTxtPlugin() {
    return {
      name: 'llms-txt',
      async postBuild({siteConfig, routesPaths, routesBuildMetadata, outDir}) {
        const fs = await import('node:fs/promises');
        const path = await import('node:path');
        const lines = [
          `# ${siteConfig.title}`,
          '',
          `> ${siteConfig.tagline}`,
          '',
          '## Docs',
        ];
        for (const route of routesPaths) {
          if (routesBuildMetadata?.[route]?.noIndex) continue;
          if (route === '/404.html') continue;
          lines.push(`- [${route}](${siteConfig.url}${route})`);
        }
        await fs.writeFile(
          path.join(outDir, 'llms.txt'),
          lines.join('\n') + '\n',
        );
      },
    };
  },
],

Writing into outDir puts the file at the build root, the same place static/ lands, so the baseUrl rule above still applies. Before you ship it, replace the raw route path with a real title, and group routes under separate ## headings rather than one flat list. Agents read the headings.

Docusaurus route 3: an npm plugin

PackageVersionWhat it produces
docusaurus-plugin-llms0.6.0 on 4 September 2026; 0.6.1 on 24 October 2026An llms.txt index following the llmstxt.org format
@signalwire/docusaurus-plugin-llms-txt1.2.2 on both datesMarkdown twins of your HTML pages, plus an llms.txt index

The second solves a problem the first does not touch: your links can point at Markdown rather than HTML, which is what an agent wants at the other end. Both are community packages, not part of @docusaurus/core. Read the README and pin the version.

Choosing which Docusaurus pages to list

A docs site's route list includes tag pages, archive pages and pagination, and none of those help a model. Start with the H1 naming the project, follow it with a blockquote summary, group links under ## headings such as ## Getting started and ## API reference, give every link a short note, and leave out anything carrying noIndex.

Confirm which Docusaurus URL you shipped

curl -sI https://yoursite.com/llms.txt | head -1
curl -sI https://yoursite.com/your-baseurl/llms.txt | head -1

Expect HTTP/2 200 from whichever one matches your baseUrl. A 404 from the origin root while the subpath returns 200 is the failure this section exists for. Status codes can be trusted on Docusaurus: verified on 4 September 2026, docusaurus.io/llms.txt returns a real 404 with a text/html body of 20,038 bytes, and an invented path returns the identical 404. Then read the body with curl -s https://yoursite.com/llms.txt | head -20. The first non-blank line must be a single # heading. If you see HTML, you are looking at your 404 page through a redirect.

AIScan's C2 check probes /llms.txt at the origin of the URL you give it. Give it the subpath URL and it still asks the origin root, so a spec-legal /docs/llms.txt reads as absent. That is a limit of the check rather than a fault in your file.

Framer

Verified 2 September 2026. Framer's help articles are script-rendered and were not re-fetched on 24 October 2026, so the plan and path details below carry their September date.

On Framer, llms.txt is not a file you commit and not a text box you fill in. It is an upload in a hosting panel, and that panel is switched off below a certain price.

QuestionAnswer
Where does the file live?Uploaded under Site Settings → Hosting → Files, with the path set to the domain root
Which plans can do it?Pro and Enterprise only. Free and Basic sites cannot host a static file at all
What formats are accepted for llms.txt?TXT for a standard file, JSON for structured data
Does it work before publishing?No. The file appears only after the site is published
Does Framer generate it for you?No. Framer auto-generates robots.txt and sitemap.xml, not llms.txt
Time to fixAbout ten minutes, once you are on a plan that allows it

The Framer plan gate comes first

Framer's static files article says, in its own words, "This feature is available on Pro and Enterprise plans." There is no workaround.

SourceWhat it says about plansLast updated
Framer help, "Static files""available on Pro and Enterprise plans"10 August 2026
Framer help, "Add an llms.txt file to your site"Pro and Enterprise in the body, and again in its FAQ10 August 2026
Framer pricing, snapshot in Framer's own llms.txtPro is $30 per month, billed monthly30 August 2026

According to the same article, "These settings used to live in the Domains dashboard and are now in your project's Site Settings, under Hosting." Older tutorials still point at the Domains dashboard, so ignore them.

Upload the file in Site Settings

  1. Open your dashboard.
  2. Open Site Settings.
  3. Go to Hosting → Files.
  4. Click the Add button.
  5. Upload your file.
  6. Set the file path. Framer's step says "do not include the file name", and its worked example is a path of /pdfs producing example.com/pdfs/example.pdf. Its FAQ answers the root case directly: "You can upload a file and assign it to a root-level path such as /ads.txt or /llms.txt." So upload a file named llms.txt and give it the root path.
  7. Publish the site.

Delivery depends on the MIME type. text/plain, text/xml, application/json, application/pdf and the image, audio and video families render in a browser, and in Framer's phrasing "all other file types will automatically download instead of rendering." Upload Markdown with an .md extension and a crawler gets a download prompt rather than a body. Use .txt. The same panel handles .well-known files: set the path to /.well-known for security.txt, assetlinks.json or an AI plugin manifest.

Framer serves a Markdown version of every optimised page at the same URL, through an Accept: text/markdown header or by appending ?md. Measured on 2 September 2026, www.framer.com/pricing with that header returned 200, text/markdown; charset=utf-8, 11,651 bytes, opening with YAML front matter. Framer's own file, fetched from www.framer.com/llms.txt that day, was 5,900 bytes and points at https://www.framer.com/ai/?md and its siblings rather than the HTML pages. It still answered 200 on 24 October 2026.

The caveat comes from Framer's AI agents article: "Markdown is only generated for optimized pages." That is not theoretical. Framer's own help articles are not optimised, and Accept: text/markdown on /help/articles/static-files/ returns text/html. Test any URL before you put its ?md form in your index.

Verify the Framer file landed

What you seeWhat it means
200 and text/plainWorking. Framer serves it from the edge with server: Framer
404, tiny bodyThe upload is missing, the path is wrong, or the site has not been published since
A download prompt in the browserWrong extension. Re-upload as .txt
200 but empty contentThe file uploaded is empty; C2 will still fail

Framer returns a real 404 for a missing root file. Verified on 2 September 2026, www.framer.com/llms-full.txt returned 404 with a ten-byte body, while an unknown page path returned the site's own 404 page. Framer does not answer 200 with the homepage the way Ghost does, so a status check on Framer is trustworthy.

Ghost

Verified 30 August 2026. The routing page was re-fetched on 24 October 2026 and still documents content_type.

Ghost has no setting for llms.txt and no folder you can drop a file into that lands on the site root. The fix takes about fifteen minutes: one route in routes.yaml, one Handlebars template, one upload. Ghost's routing documentation says routes render HTML unless you override it "by specifying a content_type property with a custom mime-type". That property is the whole trick.

If you are on…Do thisWhere the file ends upTime
Ghost(Pro)Add a template route in routes.yaml, upload it under Settings > Labs/llms.txt/ (with the slash)~15 min
Self-hosted GhostSame route, or an alias in nginx or Caddy/llms.txt/, or /llms.txt via the proxy~10 min
Any Ghost siteCheck the final URL and content type, not the status coden/a1 min

Why Ghost gives you nowhere to put the file

Ghost serves theme files under /assets/ and uploaded media under /content/images/. Neither is the site root, and Ghost admin has no file manager. So on Ghost the file cannot be a file. It has to be a route that renders text, from your posts, every time an agent asks.

Add the Ghost route

Ghost keeps its routing config at content/settings/routes.yaml:

routes:
  /llms.txt/:
    template: llms-txt
    content_type: text/plain

collections:
  /:
    permalink: /{slug}/
    template: index

taxonomies:
  tag: /tag/{slug}/
  author: /author/{slug}/

Keep the collections and taxonomies blocks. Uploading a routes file that omits them replaces your whole URL structure. The trailing slash is not optional. According to the routing page, "Trailing slashes are required" for dynamic routing, and Ghost "automatically forces trailing slashes". Writing the route as /llms.txt/ is what makes it resolve.

Write the Ghost template

Create llms-txt.hbs in your theme root, alongside index.hbs. The {{#get}} helper queries the Content API server-side before the template renders:

# {{@site.title}}
> {{@site.description}}

## Posts
{{#get "posts" limit="all" order="published_at desc"}}{{#foreach posts}}- [{{title}}]({{url absolute="true"}}): {{excerpt words="20"}}
{{/foreach}}{{/get}}

## Start here
- [About]({{@site.url}}/about/): who publishes this

The {{#get}} helper reads posts, tags, authors, tiers and newsletters. Pages are not one of those resources, so list your important pages by hand in a second ## section, as above. Zip the theme and upload it in Ghost admin. The uploader runs GScan, and according to Ghost's theme docs "any fatal errors will prevent the theme from being used", so a broken Handlebars block is caught before it reaches the site. Then upload routes.yaml under Settings > Labs. Editing the file on a self-hosted box works too, but Ghost has to be restarted.

Self-hosted Ghost shortcut

Behind nginx or Caddy you can skip the template and serve a static file at the bare path, which routes.yaml cannot give you:

location = /llms.txt {
    alias /var/www/llms.txt;
    default_type text/plain;
}

The trade-off: the proxy version never updates itself, while the template version regenerates from your posts on every request.

Read the Ghost response, not the status code

Verified on 30 August 2026, we requested /llms.txt from two Ghost publications that do not publish one, Platformer and 404 Media. Both returned 302 to the homepage, which then returned 200 with text/html. A follow-redirects status check therefore reports 200 on a Ghost site with no llms.txt at all. Check the destination:

curl -sIL https://example.com/llms.txt | grep -iE '^(HTTP|location|content-type)'

You want the chain to end on your own /llms.txt/ with content-type: text/plain. If the final location is your homepage, the route is not live.

What you sawWhat it meansFix
302 to the homepageThe route never loadedRe-upload routes.yaml; confirm the key reads /llms.txt/ with both slashes
Renders, but text/htmlcontent_type missing or misindentedYAML nesting is two spaces, and Ghost is strict about it
Renders as text, nothing under ## PostsWrong template filenameroutes.yaml drops the .hbs, so template: llms-txt needs llms-txt.hbs
200 with content, C2 still failsA Ghost page slug collides with the routeRoutes and slugs know nothing about each other, so one wins. Rename the page

Re-run the scan after your next theme change, because a theme upload replaces llms-txt.hbs.

Confirm any platform's file passes C2

Scan first. npx aiscan-cli yoursite.com returns the whole content dimension, and row C2 names which of the four conditions failed rather than telling you something is wrong. It is free and needs no account. To check by hand:

curl -sSL -o llms.txt -w '%{http_code}\n' https://example.com/llms.txt  # expect 200
head -1 llms.txt                                                    # expect "# Example"
grep -c '^## ' llms.txt                                             # expect >= 1
grep -cF '](http' llms.txt                                          # expect >= 1
C2 can tell youC2 cannot tell you
The file exists and returns 200Whether the URLs inside it resolve
It has an H1 and a ## sectionWhether the descriptions are accurate
It carries Markdown linksWhether any assistant has ever fetched it

Server logs answer the last one. Filter for requests to /llms.txt and see who turns up. Read C2 next to C1 (Markdown for agents) and C3 (structured HTML), because the same build step usually moves all three. Every other fix guide sits at aiscan.site/guides.

Frequently asked questions

What does a valid llms.txt need to pass AIScan check C2?

Four things: the file returns HTTP 200, it has an H1, it has at least one ## section, and it contains Markdown links. C2 does not check the content type, so text/plain is fine. A file that loads in the browser but opens with a paragraph instead of a # heading still fails.

Does Google use llms.txt?

No. Google's AI optimization guide states that Google Search ignores these files and that keeping one neither helps nor harms rankings. Publish it for the agents and directed tools that do fetch it.

Where does llms.txt go in an Astro project?

A hand-written file belongs at public/llms.txt, which Astro copies to the build root untouched. A generated one belongs at src/pages/llms.txt.ts, because Astro strips the .ts extension and serves the route as /llms.txt. Test the bare path, since routes with a file extension cannot be reached with a trailing slash.

Why does my Docusaurus llms.txt return 404 at the site root?

Almost certainly baseUrl. Docusaurus copies static/ to the build root, and the build root is served under your baseUrl. If baseUrl is /my-project/, the file answers at /my-project/llms.txt and the origin root has nothing. Deploy at baseUrl: '/' on its own domain or add a host-level rewrite.

Is a file at /docs/llms.txt valid?

Yes. The llms.txt specification allows the file at the root path or at any subpath, and says agents should use the most specific file that applies. Scanners that probe only the origin root, AIScan's C2 included, will report it as absent.

Which Framer plans can host an llms.txt file?

Pro and Enterprise. Framer's static files article says the feature is available on those plans, so Free and Basic sites cannot host a file at the domain root. Upload it under Site Settings, Hosting, Files with the root path, as a .txt file, and publish the site. This was checked on 2 September 2026.

Why does my Framer llms.txt download instead of displaying?

Framer renders only certain MIME types in the browser, including text/plain, and downloads everything else. A file uploaded with an .md extension falls outside that list. Re-upload the same content as llms.txt with a .txt extension.

Does Ghost have a built-in llms.txt setting?

No. As of 30 August 2026 Ghost admin has no llms.txt option and no file manager, so the file has to come from a template route in routes.yaml with content_type: text/plain, or from the reverse proxy in front of a self-hosted install.

Why does a status check say my Ghost site has llms.txt when it does not?

Ghost answers a missing /llms.txt with a 302 to the homepage, which returns 200 with text/html. A check that follows redirects reports 200. Run curl -sIL and confirm the chain ends on your own /llms.txt/ with content-type: text/plain.

M Asif Rahman

About the author

Entrepreneur, curious about AI

I have been building companies for a long time, and lately most of my curiosity goes to AI. AIScan is a small personal project I built on a flight from Dubai to Narita. I use it to understand how ready the web is for AI agents, and I share what the scans teach me.

Related guides

Browse every guide by check and platform