Generate sitetree.json, llms.txt, and llms-full.txt at build time - #5311
Jeremy Rose (jeremyrose-viam) wants to merge 11 commits into
Conversation
…arkdown.html partial
… give llms-full.txt link its own heading
✅ Deploy Preview for viam-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…ndant in-code comments
|
Independent review of 0522c7a against base c4c03b3, both built with 1. Blocking:
|
| expression | output |
|---|---|
{{ .Title }}, {{ site.Data.x.v }}, {{ printf ... }}, {{ "it's & lit" }} bare |
raw, no escaping, in all four templates |
{{ partial "p.html" . }} where the partial prints {{ .Title }} |
Sec's & co |
{{ partial "p.txt" . }} (same body) |
Sec's & co |
{{ partial "p.md" . }} (same body) |
raw |
{{ partial "p.html" . }} where the partial ends in {{ return ... }} |
raw |
{{ partial "p.html" . | safeHTML }} |
still ' / & (escaping already happened inside the partial) |
So what escapes is a printing partial with an .html (or .txt) suffix, not top-level interpolation. That is exactly why 8d5f257's {{ return }} change fixed render-page-markdown.html. Consequences: the | safeHTML calls in list.llmstxt.txt and list.llmsfulltxt.md are no-ops (harmless, but the CLAUDE.md text will send the next person after the wrong thing), and the isHTML = false line on LLMSFULLTXT is doing nothing. Suggest rewriting the bullet to: "a partial whose file suffix is .html/.txt HTML-escapes anything it prints; return a string via {{ return }} (or use a .md partial) instead."
Smaller things
list.sitetree.json's header and the new CLAUDE.md section both citethemes/docsy/layouts/partials/sidebar-tree.html. Nothing exists there (docsy is a Hugo module viago.mod); the file in force is the repo's ownlayouts/partials/sidebar-tree.html.- "Mirrors the sidebar" is approximate: the sitetree drops
hide_children,sidebar_menu_truncate(default 50), thecanonicallink override, and the sidebar's show-toc_hide-when-on-active-path behavior. Four entries (/reference/sdks/{go,cpp,flutter,typescript}/) point at pages whose.mdmirror is a "This page redirects to ..." stub. data/llms_pages.yaml's header says it's read bylist.llmsfulltxt.txt; the file islist.llmsfulltxt.md.- The sitetree link in
llms.txtis injected by{{ if eq .title "Reference" }}; renaming that section in the YAML silently drops the link. Put it in its own unconditional block. - The new
<link rel="alternate" type="application/json" href=".../sitetree.json">asserts the sitetree is an alternate representation of the current page, sitting next to the genuinerel=alternate type=text/markdownmirror link.rel="index"or adescribedbysibling would be more honest. It also never renders on the home page (layout: "empty"useslayouts/docs/empty.html, which has its own<head>), and neither does the pre-existingrel="describedby"llms.txt link, so that's an inherited gap rather than a new regression, but home is the page an agent hits first.
Things I checked that hold up: vale 3.12.0 clean on CLAUDE.md and list.llmsfulltxt.md; markdownlint/prettier genuinely don't cover layouts/; make build-prod exits 0; the two pages inlined into llms-full.md are byte-identical to their own .md mirrors with no HTML/SVG/entity leakage; sitetree.json is valid JSON with no /tags/* leakage and a description on every node; the /llms-full.txt /llms-full.md 200 rule lands last in _redirects with no wildcard ahead of it; list.sitetree.json does not collide with list.typesense.json (typesense output is byte-identical before/after).
Generated by Claude Code
- Rename list.md to list.markdown.md: it shared the .md suffix with the new list.llmsfulltxt.md, and Hugo's list.<format> lookup picked the latter for every section/home Markdown mirror, silently replacing their real content with llms-full.txt's content sitewide. - Correct CLAUDE.md's escaping root-cause bullet: the risk is a printing partial with an .html/.txt suffix, not top-level list.<format> interpolation. Remove the now-proven-unnecessary safeHTML calls in list.llmstxt.txt and list.llmsfulltxt.md. - Fix sidebar-tree.html citation (the repo's own override, not the shadowed themes/docsy vendored copy) and soften the sitetree.json mirroring claim to reflect real, confirmed divergences. - Soften llms.txt's sitetree.json line: not literally every page (toc_hide pages, e.g. tutorials, are excluded). - Use rel="describedby" instead of rel="alternate" for the sitetree.json head link -- more accurate than claiming it's an alternate representation of the current page. - Note sitetree.json's path non-uniqueness for manualLink sections. - Fix stale list.llmsfulltxt.txt filename reference in data/llms_pages.yaml's header comment.
|
It looks like the following files may have been renamed. Please ensure you set all needed aliases: |
|
Thanks for this — the
Smaller things, all applied: fixed the Left alone deliberately: the four Appreciate the depth here — this caught something my own verification process structurally couldn't have (I was diffing the new files against themselves, never re-checked the pre-existing mirror output after the change). |
|
Re-reviewed 15b3fb8: rebuilt it and diffed against a fresh baseline build of Confirmed fixed
One thing I'd still want before merge
Every leaf page's Smaller
Generated by Claude Code |

Summary
Adds three build-time-generated, agent-facing discoverability files to the Hugo site, building on the just-merged native Markdown mirror (#5307):
/sitetree.json— the full site IA as JSON, mirroring the core child-selection andmanualLinklogic of the real sidebar nav (layouts/partials/sidebar-tree.html, this repo's own override — not the vendoredthemes/docsy/copy). It's a deliberate simplification, not a byte-for-byte mirror: it hard-excludestoc_hidepages (e.g. individual tutorials) entirely, and doesn't replicate the sidebar's truncation/hide_children/active-path-dependent display logic, which don't have a meaningful analog in a static JSON tree./llms.txt— a curated, auto-generated entry point for agents, replacing the hand-maintainedstatic/llms.txt. Driven entirely bydata/llms_pages.yaml; titles/URLs/descriptions are always pulled live from the real pages viasite.GetPage, so they can't drift out of sync./llms-full.txt— full text of a small, curated set of "orientation" pages (currently 2), reusing the exact same Markdown-rendering partial (render-page-markdown.html) every page's own.mdmirror already uses.Also extracts a
render-page-markdown.htmlpartial out ofsingle.md/list.md. Both templates were created by #5307 and have been byte-identical since then; this just centralizes their shared body into one partial instead of two copies of the same logic. Sensible refactor, no change in function — verified with a byte-for-byte diff of two representative pages' generated output (onesingle.md-path leaf page, onelist.md-path section) before and after. It's what letsllms-full.txtreuse the exact same page-rendering instead of needing a third copy.data/llms_pages.yaml's header comment documents the curation strategy: orientation/workflow pages getfull: trueand are inlined intollms-full.txt; reference/lookup pages stay link-only inllms.txt, since an agent fetching one resource's own.mdmirror on demand beats a monolithic dump of every reference page.CLAUDE.mdhas a new "Agent discoverability files" section documenting this system, including several non-obvious Hugo behaviors this PR ran into:list.<format>page-template lookup keys off the output format'sNamefield, not itsbaseName—llms.txt/llms-full.txtinitially had a silent naming collision (both rendered identical content) until their template files were renamed to match their format names exactly.llms-full.txt's output format neededmediaType = "text/markdown"(nottext/plain) so shortcodes likealert/expandresolve to their existing clean.mdvariants instead of falling back to raw HTML. It publishes as/llms-full.mdinternally, with a Netlify rewrite (matching the existing section/home Markdown-mirror trick) exposing it at the conventional/llms-full.txtpath.llms-full.txt's output format the.mdsuffix (previous bullet) introduced a secondlist.*.mdtemplate alongside the pre-existinglist.md. Hugo'slist.<format>lookup turned out not to be a strict name-match — with two.md-suffixed candidates present, it silently preferred the new one for every section's and the home page's own real Markdown mirror output, replacing their content sitewide. Fixed by renaminglist.mdtolist.markdown.md, name-qualifying it the same way every other output format's template already was. This is the kind of regression that a scoped, per-file verification pass doesn't catch — worth a standing full-site check (e.g. grepping every generated mirror for content that shouldn't be there) rather than relying on spot-checks of the specific new files..html/.txtfile suffix, not in top-level page templates — bare interpolation directly inside alist.<format>/single.<format>template never escapes.render-page-markdown.htmlavoids it by composing a string and using{{ return }}instead of printing.Scope note: this covers items 1–2 of a larger agent-experience handoff. Accept-header content negotiation via a Netlify Edge Function (item 3) is intentionally out of scope here — it's differently-shaped work (new deploy infrastructure) that a separate branch is already touching.
Test plan
make build-prodcompletes with no errorsvale(pinned 3.12.0, matching CI) is clean on every.mdfile this PR touches or creates/sitetree.jsonis valid JSON;/hardware/-stylemanualLinkshims resolve to their real target; no/tags/*taxonomy leakage/llms.txtand/llms-full.txtare distinct, correctly generated, and contain no HTML-escaping artifacts/llms-full.txt's inlined pages render as clean Markdown (no raw HTML/SVG shortcode leakage), verified against their own.mdmirrorswarnfover ~200KB) and missing-page guardrail (warnfon an unresolvabledata/llms_pages.yamlentry) both tested in both directionshugo server: all three routes return 200 with correct content-types, and the new<link>tags render in a real page's<head>list.mdshadowing fix: every section's and home's.mdmirror across all 1677 pages verified to no longer containllms-full.txt's content🤖 Generated with Claude Code