---
title: "How to publish a valid llms.txt on Hugo"
slug: llms-txt-hugo
published: 2026-09-03T13:22:16.3766+00:00
updated: 2026-09-03T13:22:16.3766+00:00
author: "Asif Rahman"
author_url: https://masifrahman.com
category: "AI Readiness"
tags: check:C2, platform:hugo, llms.txt, Hugo, AI readiness, static site generator, output formats
description: "Hugo ships no llms.txt setting. Two working routes: a static/llms.txt file, or a custom output format with a layouts/home.llms.txt template. Verified steps."
url: https://aiscan.site/blog/llms-txt-hugo
---

Hugo has no llms.txt setting, no plugin and no checkbox. What it has instead is an output format system that already builds `robots.txt` and `sitemap.xml` the same way, which means the file can be generated from your content on every build rather than hand-maintained. Two routes get you a working `/llms.txt`. One takes thirty seconds and goes stale. The other takes ten minutes and never does.

## Quick summary

| Question | Answer for Hugo |
|---|---|
| Is there a built-in llms.txt? | No. Hugo ships `robots`, `sitemap` and `rss` output formats. There is no `llms` format. |
| Fastest working route | Put a file at `static/llms.txt`. Hugo copies it to `public/llms.txt` on build. |
| Route that stays current | Define a custom output format and a `layouts/home.llms.txt` template that ranges over your pages. |
| Media type to use | `text/plain`, already registered in Hugo with the `txt` suffix. No `mediaTypes` block needed. |
| Template filename | `layouts/home.llms.txt` on Hugo v0.146.0 and later. Older tutorials say `layouts/index.llms.txt`. |
| Verify with | `npx aiscan-cli yoursite.com`, then read row **C2**. |
| Hugo's own site | `gohugo.io/llms.txt` returns **404**, verified on 3 September 2026. |

## Two routes, and the honest trade between them

| | `static/llms.txt` | Custom output format |
|---|---|---|
| Setup time | Under a minute | Five to ten minutes |
| Updates when you publish a post | No | Yes |
| Needs a template | No | Yes, one file |
| Breaks if you rename a section | Silently | Loudly, at build time |
| Good for | Small sites, a hand-curated index | Blogs, docs, anything with more than ~20 pages |

Hugo's directory-structure documentation describes `static` in its own words as the directory that "contains files that will be copied to the public directory when you build your project", and names `robots.txt` as one of its examples. So `static/llms.txt` works, today, with no configuration at all. It is also a file that nobody will remember to edit six months from now.

## Route one: drop the file in static/

Create `static/llms.txt` in your project root:

```
# Example Site

> Short description of what this site covers.

## Guides

- [Getting started](https://example.com/guides/getting-started/): First-run setup
- [Deploying](https://example.com/guides/deploying/): Build and ship

## Reference

- [Configuration](https://example.com/reference/config/): Every setting
```

Run `hugo` and the file lands at `public/llms.txt` untouched. That is the whole route.

## Route two: make llms.txt something Hugo builds

Hugo's output format table already contains the exact shape you want. According to Hugo's configuration documentation, the built-in `robots` format is `mediaType: text/plain`, `baseName: robots`, `isPlainText: true`. Copy that shape and change the base name.

Add to `hugo.toml`:

```toml
[outputFormats.llms]
mediaType = 'text/plain'
baseName = 'llms'
isPlainText = true

[outputs]
home = ['html', 'rss', 'llms']
```

Two things worth knowing before you paste that. First, `text/plain` is already a registered media type in Hugo with `txt` as its only suffix, fetched from the default media-type table, so you do not need a `[mediaTypes]` block. Second, keep `html` first in the `home` list. Hugo's outputs documentation says the order matters and that "the first element will be the primary output format for that page kind, and in most cases that should be `html`", which is also what its own worked example does when it adds a third format.

Now the template. Create `layouts/home.llms.txt`:

```go-html-template
# {{ site.Title }}

> {{ site.Params.description }}

## Pages

{{ range .Site.RegularPages }}
- [{{ .LinkTitle }}]({{ .Permalink }}): {{ .Summary | plainify | truncate 120 }}
{{- end }}
```

Build with `hugo` and read `public/llms.txt`. Every new post appears in it on the next build, which is the entire point of choosing this route.

## The filename changed in v0.146.0, and Hugo's own docs disagree

This is the single most common way route two fails. Hugo's template lookup page says, verbatim, "We did a complete overhaul of Hugo's template system in v0.146.0", and that page's own examples still use the pre-overhaul names such as `index.amp.html`. The current pages use the new ones: the RSS templates page shows custom feeds living at `layouts/home.rss.xml`, `layouts/section.rss.xml` and `layouts/term.rss.xml`.

The pattern is `<kind>.<output format name>.<suffix>`. For a home-page llms.txt built by an output format named `llms` with a `txt` suffix, that is **`layouts/home.llms.txt`**. A tutorial written before March 2025 will tell you `layouts/index.llms.txt`, and on a current Hugo you will get a build that produces nothing while reporting no error.

## Why baseof.html does not wrap the file

The other silent failure is a `/llms.txt` that arrives full of your site's HTML shell. Hugo's template-types documentation gives the rule in its own words: a base template is applied only if the template being parsed "must include at least one define action" and "can only contain define actions, whitespace, and template comments". A plain-text template containing raw text and `range` blocks meets neither condition, so Hugo "executes it exactly as provided, without applying a base template".

If you see `<!DOCTYPE html>` at the top of your generated file, you have wrapped the body in `{{ define "main" }}`. Remove it.

## Verify the build, then verify the live site

Local check first, because a build failure and a deploy failure look identical from outside:

```bash
hugo --cleanDestinationDir
head -5 public/llms.txt
```

Then check what agents actually receive. Lead with the scan, because it answers the question the file exists to answer:

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

Read row **C2**. C2 passes when the file returns 200, carries an H1, has at least one `##` section, and contains markdown links. It does not require a particular content type, which is why Hugo serving `text/plain` is fine.

By hand, if you would rather:

```bash
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' https://yoursite.com/llms.txt
# expect: 200 text/plain; charset=utf-8
```

Hugo sites are unusually trustworthy under a plain status check. Probing `gohugo.io` on 3 September 2026 returned a hard **404** for `/llms.txt`, `/robots.txt` and an invented page path alike, with `/sitemap.xml` returning 200 `application/xml`. Hugo does not answer a missing file with the app shell the way several JavaScript-first hosts do, so a 200 here means a real file. Hugo's own site, incidentally, does not publish an llms.txt at all.

## Where AIScan fits, and where it doesn't

| C2 can tell you | C2 cannot tell you |
|---|---|
| The file resolves and returns 200 | Whether the links inside it resolve |
| It has an H1 and `##` sections | Whether the summaries describe the pages honestly |
| It contains markdown links | Whether a stale `static/llms.txt` is missing half your posts |
| Which dimension score it moved | Whether your build ran at all |

Our [llms.txt generator](https://aiscan.site/llms-txt-generator) writes a spec-shaped file you can paste into `static/llms.txt` as a starting point, and [the mistakes that break a valid file](https://aiscan.site/blog/llms-txt-validator-common-mistakes) covers what goes wrong inside the content itself. If you are weighing whether this is worth the ten minutes at all, [the evidence on llms.txt](https://aiscan.site/blog/does-llms-txt-actually-work-2026) is the honest version.

## After the deploy

Rebuild, deploy, then scan the live domain rather than a preview URL. Run [`npx aiscan-cli`](https://aiscan.site/) against your production host and read C2 in the `content` dimension. The full rubric for that dimension, including what sits alongside C2, is on the [content checks page](https://aiscan.site/docs/checks/content).

Two neighbouring platforms solve this differently and are worth a look if you run more than one stack: [llms.txt on Astro](https://aiscan.site/blog/llms-txt-astro) uses a file-extension endpoint rather than an output format, and [llms.txt on Framer](https://aiscan.site/blog/llms-txt-framer) is a plan-gated upload with no template involved. Every fix guide we publish is indexed at [aiscan.site/guides](https://aiscan.site/guides).

