Findable by crawlers, readable by agents.
ApiGlow renders at runtime, in the browser, under hash routes — and that is exactly what the crawlers that matter cannot read. AI crawlers (GPTBot, ClaudeBot, PerplexityBot) fetch HTML but run no JavaScript; Google runs JavaScript but treats every #/… route as the same URL, so it can index the home view and never an operation page. The answer has two halves: what every install already does at runtime, and what the apiglow bake CLI writes to disk.
What every install already does
The app manages the document head on every route change, with zero configuration:
<title>— “{route} — {API title}”: the operation summary (orGET /path), the docs page title, the workflow name. Once booted, the app owns the title.- Meta description — created if the host page has none, updated in place otherwise: plain text derived from the route’s own content, capped around 160 characters.
- JSON-LD — one block per route:
APIReferencefor endpoints,TechArticlefor docs pages and scenarios,WebSiteon the home view. Views no type fits (the audit, the first-call page) emit none — a wrong type is worse than silence.
There is deliberately no runtime canonical: under hash routing every route shares one server URL, and fragments are stripped by crawlers — a per-route canonical would be meaningless. Canonicals live in the baked snapshots.
Staying out of the index
Some documentation must not be found. One key does it:
{ "seo": { "index": false } }The app injects <meta name="robots" content="noindex"> before first paint. The key is root-only in multi-spec — one page is served at one URL, and a crawler reads that HTML without ever choosing a spec. Two deployment notes from the config’s own comments: pair it with an X-Robots-Tag: noindex header to cover non-HTML files (the schema itself, baked .md mirrors), and do not also Disallow the page in robots.txt — a blocked URL is never fetched, so the noindex is never seen. And it is a request to well-behaved crawlers, not a protection: documentation that must not be read needs an auth wall, not a meta tag.
The bake: a static mirror from one command
apiglow bake is a companion CLI, run by the docs author — never by the reader. It ships inside the npm package ("bin": { "apiglow": "dist/bake.js" }, Node ≥ 24) and reads the same JSON config your host page inlines:
apiglow bake --config apidoc.config.json --site-url https://docs.example.com/ --out public/| Flag | Meaning |
|---|---|
--config | the JSON config the host page inlines in #api-doc-config |
--site-url | absolute URL of the deployed documentation page |
--out | directory the static tree is written to |
--language | catalog used for the snapshots’ chrome — en or fr, default en |
It writes one file tree, deployed next to the app page by whatever already deploys your site:
sitemap.xml
llms.txt
llms-full.txt
overview.html
op/{operationId}.html op/{operationId}.md
page/{slug}.html page/{slug}.md
scenario/{scenarioId}.html scenario/{scenarioId}.md
scenario/{scenarioId}.arazzo.jsonEvery route gets an HTML snapshot — canonical to itself, a <link rel="alternate" type="text/markdown"> to its sibling, JSON-LD, zero scripts, and one prominent link back into the interactive app (“Open in the interactive documentation”). No redirect, no hydration: the snapshot is honest static content, not a cloaking trampoline. Beside it sits a Markdown mirror — the same generator the in-app “Copy page” uses, {{var}} left literal. Each scenario additionally publishes its Arazzo recipe, the file a CI runner executes unchanged. overview.html is the crawler’s entry point: a link to every snapshot, so landing on the site never means a sitemap and nothing else. In multi-spec, everything nests under s/{specId}/… and the three root files cover all specs.
This is not a prerender and not static-site generation — no DOM runs in Node, and the reader’s install stays one script tag with no build step. It is the app’s export layer, written to disk: if a snapshot and the app ever disagree, the generator is what gets fixed.
What agents get out of it
Unbaked, llms.txt and llms-full.txt exist only as in-browser downloads whose links are hash routes — nothing an agent can fetch. Baked, both are served files: llms.txt links the .md mirrors per the llmstxt.org convention, and each llms-full.txt section carries a Source: line pointing at its served page. The agent surface guide covers what is inside the files themselves.
CI-friendly by design
The bake resolves its two kinds of addresses differently, on purpose: files the config names are read from disk relative to the config file (leading / included — only a URL with a scheme is fetched), while every URL written into a generated file resolves against --site-url. That is what lets the bake run in CI before anything is deployed.
What the bake refuses, and what it degrades
- A config saying
seo: { index: false }is a hard error — baking a noindex site is a contradiction. - A schema that will not load ends the run; everything else derives from it.
- A docs page carried by
contentIdlives in host-page HTML no Node process sees: it is named in the warnings and dropped from the map, the sitemap and the tree — an entry pointing at a file nobody wrote is worse than one entry fewer. - Raw HTML inside Markdown is escaped, not rendered in snapshots (no DOM sanitizer in Node): a baked page can render less richly than the app. Documented fallback, not a bug.
- A scenario whose schema is inlined (
openapi.spec) gets no Arazzo recipe — a generated recipe would name a source no runner can fetch. Warned, not silent. - The reader’s user overlay is never baked — what lives in a reader’s browser stays there.
- The sitemap carries no
<lastmod>(the generator has no clock, and a bake date would lie about when the documentation changed), and the bake emits norobots.txtand noindex.html— those stay the site’s own.
Re-run the bake whenever the schema, the config, the prose pages or the scenarios change — in practice: from the same CI job that deploys the site.