---
title: "How to publish a valid llms.txt on Ghost"
slug: llms-txt-ghost
published: 2026-08-30T13:18:44.120311+00:00
updated: 2026-08-30T13:18:44.120311+00:00
author: "Asif Rahman"
author_url: https://masifrahman.com
category: "AI Readiness"
tags: check:C2, platform:ghost, llms.txt, Ghost, AI readiness, routes.yaml, content
description: "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."
url: https://aiscan.site/blog/llms-txt-ghost
---

**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 this | Where the file ends up | Time |
|---|---|---|---|
| Ghost(Pro) | Add a template route in `routes.yaml`, upload it under **Settings > Labs** | `/llms.txt/` (with the slash) | ~15 min |
| Self-hosted Ghost | Same route, or an alias in nginx or Caddy | `/llms.txt/`, or `/llms.txt` via the proxy | ~10 min |
| Any Ghost site | Check the final URL and content type, not the status code | n/a | 1 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](https://llmstxt.org/): 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](https://aiscan.site/blog/llms-txt-shopify), Astro [builds it as an endpoint](https://aiscan.site/blog/llms-txt-astro), and Webflow [stores an artifact you uploaded](https://aiscan.site/blog/llms-txt-webflow). 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:

```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 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:

```handlebars
# {{@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:

```nginx
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](https://www.platformer.news) and [404 Media](https://www.404media.co). 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:

```bash
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 saw | What it means | Fix |
|---|---|---|
| 302 to the homepage | The route never loaded | Re-upload `routes.yaml`; confirm the key reads `/llms.txt/` with both slashes |
| Renders, but `text/html` | `content_type` missing or misindented | YAML nesting is two spaces, and Ghost is strict about it |
| Renders as text, nothing under `## Posts` | Wrong template filename | `routes.yaml` drops the `.hbs`, so `template: llms-txt` needs `llms-txt.hbs` |
| 200 with content, C2 still fails | A Ghost page slug collides with the route | Ghost'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:

```bash
npx aiscan-cli yoursite.com
```

Row **C2** is this file. [AIScan](https://aiscan.site/) 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](https://aiscan.site/llms-txt-generator) will produce a starting file if you would rather paste one than write the Handlebars.

| C2 can tell you | C2 cannot tell you |
|---|---|
| The file returns 200 at your root | Whether the links inside it resolve |
| It has an H1, a `##` section and markdown links | Whether your summaries describe the pages honestly |
| It is reachable without JavaScript | Whether any model has actually read it |

On that last question, the [review of the evidence](https://aiscan.site/blog/does-llms-txt-actually-work-2026) 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](https://aiscan.site/docs/checks/content), and every platform guide published so far is indexed at [aiscan.site/guides](https://aiscan.site/guides).

