Skip to content

Add llms.txt support: auto-updating indexes + markdown page twins - #1145

Open
Red5d wants to merge 5 commits into
JupiterBroadcasting:developfrom
Red5d:develop
Open

Red5d wants to merge 5 commits into
JupiterBroadcasting:developfrom
Red5d:develop

Conversation

@Red5d

@Red5d Red5d commented Oct 28, 2026

Copy link
Copy Markdown

(Developed in collaboration with my Hermes agent. Reviewed and verified working in a local docker container by me, the human)

Implements the llmstxt.org v2 proposal so LLM agents can navigate the site:

File Purpose
/llms.txt 4 KB site map: shows (with episode counts), core pages, per-show index links
/show/<slug>/llms.txt × 8 Full episode indexes (1,961 episodes): date · title (guest) - description, newest-first, each linking its markdown twin
/llms-full.txt Core (non-episode) pages concatenated (~124 KB). Episodes deliberately excluded — the full corpus is ~15 MB, a bit large in a context window
<url>index.md twins Markdown version of every page/section (spec form for extensionless URLs). Episode twins include show notes, hosts, guests, duration, audio/video links

HTML pages get spec discovery links: rel="describedby" → /llms.txt, rel="alternate" type="text/markdown" → the page twin.

How it stays current

No scheduled job, no committed generated files. scripts/gen_llms.py runs in the Docker builder stage on every build and regenerates everything from Hugo's own rendered data (an NDJSON dump emitted during hugo --gc, then deleted). Deploys already fire on every push to main, including scraper auto-commits, so new episodes land in the indexes automatically. Drift is structurally impossible.

Notes for review

  • Dockerfile gained a small llms stage (python:3.12-alpine) because the pinned hugo image's apk repos resolve to current Alpine and its python3 fails to load against the image's musl. Final stage now copies public/ from llms instead of builder.
  • nginx: stock nginx:alpine has no text/markdown mime mapping, so a location ~ \.md$ block sets the content type. The inserted blocks repeat root because the stock conf declares it inside location /.
  • Episode URLs: indexes use Hugo's real permalinks (/show/linux-unplugged/650/, numeric slug), not content filenames (0650.md).
  • config.toml: [outputs] home now explicitly lists ["html", "rss", "json", "llmsdump"] (previously Hugo defaults) — no behavior change for existing formats.

Verification (full docker build of this branch + served-container checks)

  • /llms.txt, /llms-full.txt, all 8 /show/<slug>/llms.txt: 200, spec-conformant structure (H1 → blockquote → H2 file lists)
  • /show/linux-unplugged/650/index.md, /membership/index.md, /live/index.md: 200 text/markdown
  • <link rel="describedby" href="/llms.txt"> + markdown alternate present on pages
  • Unit tests: test/test_llms_gen.py 4/4; e2e checks added in test/e2e/test_llms.py

Red5d-agent and others added 3 commits October 28, 2026 10:44
Implements the llmstxt.org v2 proposal:

- /llms.txt at the root: site map with per-show and core-page links
- /show/<slug>/llms.txt per show: full episode index (date, title,
  guests, description) newest-first, linking markdown twins
- /llms-full.txt: concatenated core (non-episode) pages
- markdown twins for every page/section at <url>index.md via Hugo
  output formats (episode template includes show notes, hosts,
  guests, duration, audio/video links)
- discovery link relations in <head>: rel=describedby -> /llms.txt,
  rel=alternate type=text/markdown -> page twin
- nginx serves .md with Content-Type text/markdown

Indexes are generated at container build time (scripts/gen_llms.py
consumes an NDJSON dump Hugo emits during the build, then removes it),
so they regenerate on every deploy, including scraper auto-commits:
no drift between content and llms files is possible.

Tests: unit tests for the generator (test/test_llms_gen.py) and e2e
checks of the served files (test/e2e/test_llms.py). Verified with a
full docker build of this branch: /llms.txt, /llms-full.txt, all 8
show indexes, and page twins serve correctly.
Per PR review: /show/<slug>/llms.txt listed every past episode (600+
entries, ~190 KB for LINUX Unplugged). The llms.txt spec treats these
files as curated maps, not full inventories.

Each show file now contains:
- show description + link to the show page markdown twin
- the 10 most recent episodes with dates, guests, and summaries
- the episode number range and URL pattern with oldest/newest examples
- a link to /show/<slug>/episodes.json: a generated JSON list of every
  episode (number, date, URL) for programmatic enumeration

episodes.json is emitted because Hugo 0.105 predates per-section
sitemaps (added in 0.108), so the spec's sitemap.xml pointer has no
per-section target on this site.

Also fixes markdown twins for taxonomy/term pages (e.g. /hosts/) by
adding taxonomy/term outputs, and updates unit + e2e tests.

Verified with full Docker build: all llms.txt markdown-twin links
return 200 text/markdown; per-show files 3.5-3.8 KB.
Add llms.txt support: auto-updating indexes + markdown page twins
@CGBassPlayer

Copy link
Copy Markdown
Collaborator

Thanks for the contribution! I am currently just getting back from being out of town. I will look this over as soon as I can

@CGBassPlayer

Copy link
Copy Markdown
Collaborator

So I was looking through and I think it would make more sense for this to be done with Hugo itself rather than a python script during the docker build. You should be able to use custom output formats and templates similar to what you are doing.

  • /llms.txt and /llms-full.txt: custom text/plain formats on home, with layouts index.llms.txt and index.llmsfull.txt.
  • /show/<slug>/llms.txt and episodes.json: formats on the show sections, with show/list.llms.txt. Enable them with outputs = [...] in each show's _index.md front matter, so they don't also render for /show/, /hosts/ and /guests/.
  • Episode lists: (site.GetPage "/show/<slug>").Pages.ByDate.Reverse, with first 10 for the recent list.
  • URLs: use .Permalink instead of the hardcoded SITE_BASE.

Going this route:

  • removes the need of the script and cleanup steps in the build
  • it allows the llm text files to work in our dev instance on render as well as locally
  • it makes it so all episodes work. Your current implementation will break on the launch's "T" episodes and office hours pocket episodes as they are prefixed with "T" and "p" respectively. It will also break if (and when) LUP hits episode 1000 since you prefix is only looking for a leading 0.

I do love the idea of making the site more LLM friendly. Let me know if you have any questions.

Red5d-agent and others added 2 commits October 30, 2026 08:08
…ript

Implements the maintainers' review feedback: everything is now Hugo-native.

- New output formats: llms/llmsfull (text/plain) on home, llms/episodes on
  show sections via 'outputs' front matter in each show's _index.md, so
  they don't render on /show/, /hosts/, /guests/.
- Templates: index.llms.txt, index.llmsfull.txt, show/list.llms.txt,
  show/list.episodes.json; partials/llms-body.html strips TOML front
  matter from markdown twins.
- URLs from .Permalink; canonical slugs from .Params.slug (0650.md -> 650).
- Episode ranges from .Params.episode type check (int/float64 = numbered,
  string = special), so the-launch 'T-' and office-hours 'p' episodes work
  and 4-digit episode numbers will too.
- Episode enumerations filter 'where .Pages "Type" "episode"' (section
  .Pages includes taxonomy/_index ghosts).
- head.html describedby now points at the show-scoped llms.txt for pages
  under a show, root llms.txt elsewhere.
- Dockerfile: python stage and llms cleanup steps removed (builder->nginx).
- e2e tests rewritten as pure-HTTP (requests), incl. T-/p episode and
  non-show 404 coverage; 8/8 pass against the built image.
Generate llms.txt with Hugo output formats (drop the Python build script)
@Red5d

Red5d commented Oct 30, 2026

Copy link
Copy Markdown
Author

Sure, makes sense. I've pushed some updates to the PR to implement it with just Hugo as you suggest, and handle the "T", "p", and 1000+ numbered episodes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants