Prose pages, declared in the config.
docsPages weaves guides, tutorials and changelogs into the navigation, above the API reference — rendered in the browser like everything else. The feature page makes the case; this guide is the full syntax.
Three kinds of entry
docsPages is an ordered array, and array order is nav order — there is no position field. Entries are discriminated by their keys:
{
"docsPages": [
{ "slug": "getting-started", "title": "Getting started",
"url": "/docs-pages/getting-started.md", "home": true },
{ "group": "Guides", "collapsed": false, "pages": [
{ "slug": "pagination", "title": "Pagination", "url": "/docs-pages/pagination.md" },
{ "title": "Status page", "href": "https://status.example.com" }
]},
{ "title": "GitHub", "href": "https://github.com/acme/api" }
]
}- A page —
slugplus exactly one body source (next section). Optionallykind: "changelog", which opts it into the timeline treatment. - A group — one level, collapsible,
collapsed: trueto start closed. Groups hold pages and external links, never other groups.idis optional; it only serves multi-spec merge identity and defaults to the slugified title. - An external link —
hrefinstead of a body, rendered with an “external” icon, opened in a new tab.
An invalid entry — a page without slug or body, a group without pages, a link without href — is dropped with a console warning, never silently rendered wrong.
Two nav zones: docs above, appendix below
By default every entry sits above the API reference, under the “Documentation” heading. A top-level entry of any kind — page, group or external link — can instead declare nav: "bottom" and move to a trailing zone at the foot of the sidebar, below the whole reference and the webhooks: the support-and-legal half of the docs stops pushing the endpoints down.
{
"docsPages": [
{ "slug": "getting-started", "title": "Getting started", "url": "/docs/start.md" },
{ "slug": "support", "title": "Support", "url": "/docs/support.md", "nav": "bottom" },
{ "group": "Legal", "nav": "bottom", "pages": [ … ] },
{ "title": "Status", "href": "https://status.example.com", "nav": "bottom" }
]
}The trailing zone deliberately carries no heading of its own — only a separator: it reads as an appendix, not as a second docs section. nav is a top-level choice; inside a group it is ignored with a console warning, because a group travels whole. And the zone is an order, not just a place: the prev/next pager, the search listing and the exports all walk the top zone first, then the reference, then the bottom zone — a page declared first but sent to the bottom is the last one the pager reaches.
Where a page’s body comes from
One of three keys — declare exactly one; should several slip in, what the page carries beats what it would have to fetch:
| Key | Body | Typical use |
|---|---|---|
url | A file to fetch — .md by default, .html and .txt recognized by extension | The default: files next to index.html |
content | The text itself, in the config | A backend that generates the config generates the prose with it |
contentId | The id of an element of the host page holding the text | Hand-authored prose behind a login, or a host that serves exactly one route |
The contentId vessel is a script element with a non-executable type — a bare <script> would run its content as JavaScript before the app ever reads it:
<script type="text/markdown" id="doc-pagination">
# Pagination
Collection endpoints return one page at a time. Start with
[create a pet](apidoc:createPet).
</script>Two mechanics worth knowing: a literal </script> inside a code sample closes the element early (that page belongs in a file or in content), and the indentation shared by every line is removed, so the block can sit indented in your markup without markdown reading four spaces as a code block. A contentId pointing at nothing fails like an unreachable file: a visible error naming the id, never a blank page.
Formats. A carried body has no extension, so format ("markdown", "html" or "text") says it when needed; it wins over the extension and over the element’s type. HTML pages pass through the same sanitizer as everything else and skip the markdown-only features; .txt renders as preformatted text, without a ToC but with its pager. A leading YAML frontmatter block in a Markdown file is stripped and ignored — a README authored for another tool renders cleanly.
Or point at a manifest
docsPages may be a string URL instead of an array:
{ "docsPages": "/docs-pages/manifest.json" }The manifest is { "pages": [ … ] } with the same entries, and relative URLs inside it resolve against the manifest itself — the docs folder stays self-contained, versioned and moved independently of the host page. A manifest that fails to load or parse surfaces a visible nav-level error and leaves the reference nav intact.
Languages and the home takeover
title (every kind) and every body field of a page accept a per-language map — { "en": "/guide.en.md", "fr": "/guide.fr.md" } — resolved as current UI language, then en, then the first declared key. Switching the language re-renders the open page and the nav labels. A plain string means “same in every language”.
home: true on at most one page makes it the landing view: #/ renders that page, and the technical welcome view (exports, pinned scenarios, stats) moves to #/overview with its own auto-generated nav entry. Declared twice, the first wins with a console warning.
Writing pages that talk back
Markdown pages get three enrichments on top of GFM, each degrading gracefully in any other renderer (a fourth, {{var}} interpolation, has its own section):
Callouts —
> [!NOTE],[!TIP],[!IMPORTANT],[!WARNING],[!CAUTION]on the first line of a blockquote render as styled alerts, exactly the syntax GitHub renders.Code tabs — adjacent fenced blocks with no blank line between them become one tabbed block; the tab label is the fence’s meta string (
```js Node.js) or the language name. The reader’s language pick is remembered and applied to every tab group on every page. Blocks separated by a blank line stay independent — the feature is opt-in by adjacency.Operation references — a link with the
apidoc:scheme renders inline with the operation’s method badge, addressed byoperationIdor by"METHOD /path":See [create a pet](apidoc:createPet) or [list pets](apidoc:GET /pets).Operation cards come from a fenced block whose info string is
apidoc:operation, one reference per line — each renders method, path, summary and a deprecation badge when applicable, the whole card linking to the operation:```apidoc:operation GET /pets/{petId} createPet ```A reference that resolves to nothing renders visibly broken — struck through, with a tooltip — never as a dead link. Cards are links into the reference, not embedded consoles: running requests stays in the try-it panel, by design. References address the active spec only; there are no cross-spec references.
Pages that read with the reader’s own values
A guide that says curl {{baseUrl}}/pets is a guide the reader has to translate before using. Markdown pages resolve {{var}} from the same composition the try-it reads — the selected environment, with any host-supplied credentials underneath it — so the page renders with their base URL, their tenant id, and the fence’s copy button copies the interpolated text. That is the whole feature: what you paste has to run.
Three outcomes, and the page is explicit about each:
- A resolved, non-sensitive value renders as plain text. No chip, no highlight, no decoration — a snippet has to read as a snippet.
- A sensitive value never renders. You get a masked chip (the name and
••••) and the value enters neither the DOM, nor the clipboard, nor the search index, nor any export. Not redacted afterwards: never emitted. Every host-supplied credential is sensitive by definition, so those are covered too. - A missing value renders a warning chip carrying the name — the same principle as the try-it’s send-blocking, which has no send to block here. The chip is a button, and it opens the environment manager, because defining the variable is the only sensible next step. Under
environmentsLockedthe chip stays and the offer goes.
Copying a chip yields {{name}}, the template it stands for — pasting a bare variable name as if it had resolved would be the one genuinely dangerous outcome. An empty value counts as missing, exactly as in the try-it.
To write about a variable rather than resolve one, escape it with a backslash. In a fence or a code span, backslashes are literal, so type \{{name}}; in prose, markdown eats one, so type \\{{name}}. This page’s own examples are the use case.
Three surfaces stay uninterpolated, each for its own reason: .html and .txt pages (interpolation is a Markdown enrichment like callouts and code tabs, and an HTML author owns their markup), the search index (built once from the source — indexing values would make results depend on the selected environment, and would index secrets), and llms.txt / llms-full.txt (machine exports of the published documentation, not of one reader’s session).
Two details for authors: headings interpolate too, but their anchors are computed from the source before the values land, so a deep link never depends on who is reading; and values are inserted as text, never as markup, so a base URL full of underscores cannot turn half your paragraph into emphasis.
Changelog pages
A page declaring kind: "changelog" renders as a release timeline — a dot in the left gutter per h2, one continuous line joining them:
{ "slug": "changelog", "title": "Changelog",
"url": "/docs-pages/changelog.md", "kind": "changelog" }The convention is one h2 per release with the date in the heading text — ## 1.2.0 — 2026-05-01. A convention and some CSS, deliberately: nothing parses that date, so nothing has to agree on a locale, and the same file renders as an ordinary markdown changelog in GitHub or any other viewer. The treatment rides the rendered content, so it works on .md and .html alike; a .txt page has no h2 to decorate and gets nothing. An unknown kind is dropped with a console warning, like an unknown format.
“Was this page helpful?”
Set feedback.url and every docs page grows a verdict row between the content and the pager:
{ "feedback": { "url": "https://example.com/docs-feedback" } }A click POSTs this, as JSON, to your endpoint — there is no other recipient, and no endpoint of ours anywhere in the product:
{ "page": "pagination", "verdict": "up" }verdict is "up" or "down"; page is the entry’s slug, not its URL — the route is a page’s identity, while files and carried bodies move underneath it. Declare no feedback.url and there is no row at all: the default is null, and the app never sends anything anywhere on its own. This is the one place in ApiGlow where a reader’s action reaches a server, and it exists only because you asked for it, pointing where you said.
The row answers back. On success the buttons give way to a thank-you — one verdict per rendering, no vote counter, nothing stored. On failure it says so and stays clickable, so a flaky network costs the reader nothing. Both outcomes are announced in the live region, so a screen-reader user hears the result rather than guessing.
Two things to plan for. The POST carries a content-type: application/json header, which makes it a preflighted cross-origin request unless the endpoint is same-origin: your server has to answer the OPTIONS as well, the same negotiation the try-it console needs. And in multi-spec feedback is overridable per spec — one API can gather verdicts while its neighbour gathers none, or each can post to its own collector.
Chrome, search, exports
Every page gets a table of contents from its h2/h3 headings — the section being read is highlighted as you scroll — prev/next paging in flattened, zone-ordered nav order, and its section in the Cmd+K palette: page content is indexed, lazily, on the first palette open — a result deep-links to the matching heading. The same prose flows into llms.txt (top-zone pages under their group titles as ## Guides, bottom-zone ones as ## Resources) and llms-full.txt (bodies inlined) — see llms.txt & MCP.
In multi-spec, docs pages merge by identity — slug for pages, id for groups, href for links: a spec’s entry replaces the matching root entry in place, unmatched entries append. Root-level pages shared by every spec are a feature here, unlike scenarios; home resolves per spec, so one API can take over its landing view while its neighbor keeps the classic welcome.
The one thing the reader’s browser stores for all of this is the selected code-tab language. Nothing else persists.