Dark green editorial cover for the AIScan guide on publishing a valid llms.txt file on Ghost, check C2.
Dark green editorial cover for the AIScan guide on publishing a valid llms.txt file on Ghost, check C2.
AI Readiness

How to publish a valid llms.txt on Ghost

Ghost has no llms.txt setting and no static file root. Add a routes.yaml template route with content_type text/plain, then verify it with curl and a C2 scan.

AAsif Rahman August 30, 2026 6 min read
#llms.txt#Ghost#AI readiness#routes.yaml#content

This guide covers C2 · Content.

Table of contents

Verified 30 August 2026. 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. This guide covers both the Ghost(Pro) path and the self-hosted path, and it explains why the obvious way of checking your work reports success on a site that has no file at all.

Quick summary

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

Ghost's routing documentation says, verbatim, that routes render HTML unless you override it "by specifying a content_type property with a custom mime-type". That property is the whole trick. AIScan grades this file as C2, in the content dimension, and the pass bar comes from the llms.txt proposal: the file returns 200 and contains an H1, at least one ## section, and markdown links.

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.

That makes Ghost the odd one out among the platforms documented here. Shopify generates llms.txt for you, Astro builds it as an endpoint, and Webflow stores an artifact you uploaded. Ghost renders it on request, from your posts, every time an agent asks.

Add the route

Ghost keeps its routing config at content/settings/routes.yaml. Add a template route with a plain-text content type:

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 same routing page, "Trailing slashes are required" for dynamic routing, and in Ghost's own words, "Ghost automatically forces trailing slashes". Writing the route as /llms.txt/ is what makes it resolve.

Write the 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 on it automatically 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 directly on a self-hosted box works too, but Ghost has to be restarted; uploading through admin applies right away.

Self-hosted shortcut

If you run Ghost behind nginx or Caddy, you can skip the template and serve a static file at the bare path, which is the one thing routes.yaml cannot give you:

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

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

Read the response, not the status code

Here is the part that catches people. 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.

So check the destination, not the code:

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.

When the scan still reports C2 missing

Branch on what the response actually showed you:

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 routeGhost's docs warn that routes and slugs know nothing about each other, so one of the two wins. Rename the page

Where AIScan fits, and where it doesn't

Run the scan first. It fetches the file the way an agent would and tells you which row to read:

npx aiscan-cli yoursite.com

Row C2 is this file. AIScan also runs C1, C3 and E3 on the same fetch, which matters because a Ghost site that passes C2 can still fail E3 if its theme renders posts client-side. The llms.txt generator will produce a starting file if you would rather paste one than write the Handlebars.

C2 can tell youC2 cannot tell you
The file returns 200 at your rootWhether the links inside it resolve
It has an H1, a ## section and markdown linksWhether your summaries describe the pages honestly
It is reachable without JavaScriptWhether any model has actually read it

On that last question, the review of the evidence fetched from real crawler logs is more useful than any scan.

Ship it, then scan it

  1. Add the route, upload the theme, upload routes.yaml.
  2. Run npx aiscan-cli yoursite.com and read row C2.
  3. Confirm the redirect chain ends on text/plain.
  4. Re-run the scan after your next theme change, because a theme upload replaces llms-txt.hbs.

The rest of the checks in this dimension are documented at /docs/checks/content, and every platform guide published so far is indexed at aiscan.site/guides.

Frequently asked questions

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 be produced by a template route in routes.yaml or served by the reverse proxy in front of a self-hosted install.

I added the route but /llms.txt redirects to my homepage. What went wrong?

The route is not loading. Confirm the routes.yaml key is written as /llms.txt/ with slashes at both ends, since Ghost's routing docs state that trailing slashes are required for dynamic routing. Re-upload the file under Settings > Labs, which applies changes immediately, and re-test with curl -sIL.

My file renders but the response is text/html instead of text/plain. How do I fix it?

The content_type property is missing or indented wrongly. It sits at the same level as template, nested two spaces under the route key. YAML only accepts two-space indentation, and a tab or a four-space indent silently changes the structure.

The page loads as plain text but nothing appears under ## Posts. Why?

Ghost cannot find the template, so it fell back to another one. The template value in routes.yaml drops the .hbs extension, so template: llms-txt requires a file named llms-txt.hbs in your theme root. Re-zip the theme with that file included and upload it again.

Can Ghost(Pro) users do this, or is it self-hosted only?

Ghost(Pro) users can do all of it. routes.yaml is uploaded through Ghost admin under Settings > Labs, and the template ships inside your theme zip. The only step Ghost(Pro) cannot do is the nginx or Caddy alias, which needs server access.

Will the file update automatically when I publish a new post?

Yes, if you used the routes.yaml method. The {{#get}} helper queries the Content API server-side on each request, so a new post appears in the file as soon as it is published. A static file served by nginx does not update itself.

Why does my llms.txt disappear after I change themes?

The template lives inside the theme. Uploading a different theme, or re-uploading an older zip, removes llms-txt.hbs and the route then has nothing to render. Keep the file in your theme repository and re-add it to any theme you switch to.

Does serving the file at /llms.txt/ instead of /llms.txt still pass check C2?

Ghost forces trailing slashes on its own routes, so a request to /llms.txt resolves to /llms.txt/ on the same host. Confirm the redirect chain yourself with curl -sIL and check that it ends on your own path with content-type: text/plain rather than on your homepage. If you need the bare path to answer with no redirect at all, use the reverse-proxy method.

Related guides