Theme a documentation space

Embed
Preview of the card
<a href="https://forumulate.com/guides/docs-theme"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/86ff8656-03ba-4803-9353-50aa3b9d4fc8/social-card.svg?v=3e7ae33eec8b" alt="Theme a documentation space" width="600" height="315"></a>

theme.docs is the part of a space's theme that only a Documentation space reads: how its navigation, header, page, blocks, code, API reference and page foot look, beyond the keys every space shares. It lives inside the same theme you already set with PUT or PATCH /orgs/{orgId}/spaces/{spaceId}/theme.

  • Setting theme.docs on a space of any other type is refused with a 422. A site theme may carry it; it then reaches the site's documentation spaces.

  • What a reader renders is docsThemeOf(theme): the documentation defaults, under the site's theme, under the space's own. Every key you set wins.

  • Unset on a documentation space means the documentation default — the clean look — for every documentation space, including ones created before 2026-10-12 that never set these keys. look: "paper" brings back the previous look in one key.

  • GET …/theme always answers with what you stored, never the resolved theme, so you can send it back whole without freezing the defaults into it.

Four keys: sidebar, sections, mobileNav and homeAt. Every one is optional; leaving one unset renders the rail as it was before it existed. A colour is a hex or one of brand, ink, muted (which follow dark mode); …Dark keys are the dark-mode colour.

{
  "docs": {
    "sidebar": {
      "groups": "collapsible",
      "groupLabel": { "case": "none", "weight": 600, "size": 14, "color": "ink" },
      "chevron": "end",
      "active": { "style": "fill", "color": "brand", "background": "tint", "weight": 600 },
      "width": 272,
      "search": "auto",
      "methodBadges": true
    },
    "mobileNav": { "style": "drawer", "label": "section", "bar": true },
    "homeAt": "root"
  }
}

Key

What it does

groups

label (a heading over each collection) or collapsible (each collection is a disclosure; a collection's nav.collapsed starts it closed).

accordion

true: opening a group closes the others (native, no script). Needs groups: "collapsible".

groupLabel

{size (10–24 px), weight (400–700), case: "upper"|"none", tracking (em), font: "body"|"heading"|"mono", color, colorDark}, or "hide" (kept for screen readers).

item

One row: {size, height, padding, gap, color, colorDark, radius, weight} (px), and lines — 1, 2 or wrap (unset): a longer label is clamped with an ellipsis and its full text is the row's tooltip. The defaults set 2.

active

The current page's row: style — fill, tint, bar, text, solid, outline — plus color, colorDark, background (a hex or tint), backgroundDark, weight, radius, bar (true or {width, color}). The row also carries aria-current="page". The defaults draw a 2px brand bar at the row's start over a soft tint; choosing your own style drops the default tint (except fill) and the bar (unless you set bar).

chevron

start (default), end or none; chevronColor. Leaf rows now line up with their branch siblings' labels. Unset chevronColor, a caret is drawn at 3:1 or better against the rail's background in both schemes.

guideLine

false removes the line beside nested pages.

groupRule, groupDivider, ruleColor

A rule down each collection's list; a hairline between collections. ruleColorDark (hex) is the rules' colour in dark mode; unset, ruleColor holds in both.

width

The rail's width, 200–360 px (default 240). Published as --docs-sidebar-width.

background, backgroundDark, border, borderColor

The rail as its own full-height column, from 768px up. borderColorDark (hex) is the end border in dark mode.

icons

{size, color, colorDark} for page and collection icons.

anchors

Up to 8 quick links ({label, href, icon}) under the search box.

footerLinks, status

Links, and a live status pill (the footer's status shape: {monitorSpaceId, style, color, case, font, dot}), at the rail's foot.

drilldown

Every collection is its own scope (see a collection's nav.navScope).

search

show, hide, or auto — hidden when the header has a search box (still shown inside the phone drawer).

searchHeight

The rail search box's height, 28–48 px (unset: today's 32px plain box; the defaults set 36). Setting it dresses the box like the header's: a magnifier, the chrome.searchStyle.kbd keycaps (which focus this box when no header search answers them) and, when theme.docs.ask is set and not placed in the sidebar, an Ask AI square beside it.

hideable

A button folds the rail away on a wide screen; remembered per browser.

methodBadges

HTTP method pills on API reference pages.

mono

callable: a title ending in () is set in the code face.

searchKbd

The rail search box's key hint: mod-k, slash or none. Unset follows chrome.searchStyle.kbd; chrome.searchStyle.kbdStyle: "plain" draws it as grey text instead of a keycap. { "docs": { "sidebar": { "searchHeight": 40, "searchKbd": "slash" } } }

groupsOpen

current: every collapsible group starts closed except the one holding the page you are on (pinned and unlabelled groups stay open); clicking another opens it. all opens them all. Unset: each collection's nav.collapsed. The defaults set current for a tree of more than five groups. { "docs": { "sidebar": { "groupsOpen": "current" } } }

footerPin

true: the rail's footerLinks sit at the bottom of the full-height rail under a full-width rule, even on a short page. Unset: right after the tree. { "docs": { "sidebar": { "footerLinks": [{ "label": "Status", "href": "/status" }], "footerPin": true } } }

groupLink

What a collection's heading in the rail does. Unset (link): the heading opens the collection's landing page (…/c/{slug}) and the chevron folds the group; the heading is marked current on its own page. toggle: the heading only folds the group, as before. A collection's nav.linkHub overrides this either way.

sections

Top-level sections that scope the rail, previous/next and search to a group of collections.

{
  "sections": {
    "placement": "sidebar",
    "style": "card",
    "items": [
      {
        "key": "guides",
        "label": "Guides",
        "icon": "book",
        "collections": ["getting-started", "guides"]
      },
      { "key": "api", "label": "API", "icon": "code", "collections": ["reference"] },
      { "key": "blog", "label": "Blog", "href": "/blog" }
    ]
  }
}
  • placement:

    • sidebar (default) — the switcher at the top of the rail.

    • header — the items as tabs in the header's sub-row.

    • headerLinks — no switcher of its own: your headerLinks (or chrome.subnav.links) are the switcher. Give each link a match — { "collection": [ids] } with the ids of the section's collections, or a pathPrefix — and it is marked current on that section's pages; the rail, previous/next and search scope exactly as with the other placements. Use this when the header already has one link per section, so the sections are not drawn twice.

    • none — the sections scope the rail, previous/next and search, and no switcher is drawn anywhere.

  • style: tabs, card (default in the rail), select or dropdown.

  • default: the item key a select or dropdown switcher names itself after on a page that is in no section (a composed front page). Unset, the first item — it never reads "Sections". Nothing is marked current either way. { "sections": { "style": "dropdown", "default": "embers", "items": [ … ] } }

  • An item names collections by slug. One with an href and no collections is a plain link — another space of the site, say — and is never "current".

  • key is lowercase letters, digits and dashes.

mobileNav

Key

What it does

style

inline (default: the rail is a disclosure above the page) or drawer (a sheet from the edge; the header's menu opens it too, with its links underneath). Without JavaScript the drawer falls back to the disclosure.

label

space (default), section (the current top-level group's name only — "Start with Basalt"), page, trail (the group › page trail, "Start with Basalt › Use Basalt with Next.js"), or your own words. With bar: true and a label other than section, the bar prints the trail as before. The defaults set section.

side

start or end.

bar

A sticky "≡ Section › Page" row under the header, flush against it.

sections

top lists the sections (when sections.placement is header or headerLinks) and your chrome.subnav links above the tree inside the drawer, current one marked, so a phone can leave the section it landed in. none (unset) does not. The defaults set top. Drawer only.

width

sheet (unset: up to 22rem) or full (the whole screen width). Drawer only.

head

true: a head row at the top of the sheet — your brand, the header's first button and a close button — then the search and the light/dark control. The sheet then covers the header while it is open. Drawer only.

The drawer opens scrolled to the page you are reading — scrollToActive: false opens it at the top instead. Its head row stays put while the tree scrolls, its search and light/dark control share one row, and the rail's sidebar.footerLinks are listed at its foot — with sidebar.footerPin: true held at the drawer's bottom edge under the rail's rule while the tree scrolls (the same key as the wide rail's pin; there is no separate phone key). With sections.placement: "headerLinks" and the same links in chrome.subnav, the drawer lists those links once (the current one marked) instead of a second "Sections" card; with sections.style: "select" the drawer shows the select.

Key

Values

Unset

mobileNav.scrollToActive

boolean

true

{ "docs": { "mobileNav": { "style": "drawer", "sections": "top", "width": "full", "head": true } } }

mobileNav.label: "trail" (scope: theme; no layer default — the defaults set section) brings back the full trail on a drawer opener without the sticky bar, and it works under every look:

{
  "docs": {
    "mobileNav": { "style": "drawer", "label": "trail" },
    "sidebar": {
      "footerLinks": [{ "label": "Contact support", "href": "/support" }],
      "footerPin": true
    }
  }
}

homeAt

root renders the space's home page at the space's own address instead of redirecting to …/{slug}. The page's slug address keeps working and names the root as canonical.

Switchers in the rail

The version, language and variant switchers are configured under Footer, versions and SEO; three keys there shape how they sit in the rail:

Key

Values

Unset

versionSwitcher.labelPlacement (also localeSwitcher)

inside (the select reads "Version: v4") or above (a small caption over the select)

inside

variantSwitcher.style

select, or card — a rounded card showing the current option's icon and name, with no caption

select

variantSwitcher.position

top (the top of the rail) or bottom (under the tree), for the sidebar placement

between the search and the tree

variantSwitcher.options[].icon

A glyph name, an image or an emoji, drawn 20px by the card style — on the card and in each menu row, at every width

none

variantSwitcher.labelPlacement

inside or above — the card style's label as a small caption over the card ("Select your SDK")

inside (no caption)

variantSwitcher.show

auto (only on a page with tabs or a code group carrying its syncKey) or always (every page of its scope)

auto

With placement: "rail" and position: "top" the switcher sits at the top of the page's right rail, above "On this page".

{
  "docs": {
    "variantSwitcher": {
      "syncKey": "fw",
      "style": "card",
      "position": "top",
      "options": [
        { "value": "next", "label": "Next.js", "icon": "code" },
        { "value": "remix", "label": "Remix", "icon": "book" }
      ]
    },
    "versionSwitcher": { "style": "select", "labelPlacement": "above" }
  }
}

Unless show is always, a variant select is drawn only on a page that has tabs (or a code group) with its syncKey — on any other page it would switch nothing. The switchers' select takes components.input.radius.

header.splitRule

With header.layout: "rail-split", splitRule: false drops the rule that closes off the brand cell (unset: drawn). { "docs": { "header": { "layout": "rail-split", "splitRule": false } } }

A collection's own rail settings (collapsed, icon, linkHub, hubCards, navScope, backStyle) live on the collection — see Authoring reference → Collections. backStyle shapes an isolated scope's head: link (unset) is "‹ All docs" then the collection's name; title is one centred "‹ Functions" row back to the scope above it (the section, when there is one) — PATCH /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId} with { "nav": { "navScope": "isolated", "backStyle": "title" } }. Page keys (icon, navTitle, badge, navKind, navFont) are page metadata.

Theme components can fill two rail outlets: docs-sidebar-start and docs-sidebar-end.

hub — a collection's landing page

theme.docs.hub shapes every collection's landing page (…/c/{slug}). Every key is optional; unset is the designed default.

Key

What it does

layout

cards (unset) or list. A collection's own nav.hubCards wins for that collection.

columns

1, 2 (unset) or 3 card columns at desktop widths.

descriptions

false hides each page's one-line summary (its subtitle, else its seoDescription).

updated

false hides "Updated …" on each entry and under the title.

readingTime

false hides "N min read" on each entry.

cover

false hides the collection's coverUrl banner (it is still the social image).

count

false hides "N pages" under the title.

breadcrumb

false hides the trail above the title (space › section › collection). The BreadcrumbList for crawlers stays.

pager

false hides previous (the collection before) / next (this collection's first page).

children

groups (unset): a page with pages under it becomes a titled sub-group. inline: it stays one card with "+N pages".

Mix freely: { "docs": { "hub": { "layout": "list", "updated": false }, "sidebar": { "groupLink": "toggle" } } } is a quiet list with fold-only headings; { "docs": { "hub": { "columns": 3, "descriptions": false } } } is a dense grid of titles. Every part carries a data-hub-* attribute (data-hub-entry, data-hub-entry-meta, data-hub-group, …) for custom CSS.

Every rail link (sidebar.anchors[], sidebar.footerLinks[]), header link, button and block takes audience (all / signedIn / signedOut), resolved before first paint — a "Sign up" button is for signed-out readers only by default. See "Who sees what" in the theming guide for the whole list, the account menu (chrome.accountMenu) and the reader's /me page.

Key

Values

ask.placement

floating (default — the corner button) | header (a pill in the header's controls) | sidebar (a row at the top of the sidebar) | search (no launcher; the first row of the search modal)

ask.label

≤ 40 chars — the launcher's words. Default "Ask AI", translated

ask.shortcut

boolean — ⌘I / Ctrl+I opens the panel, and the launcher shows the hint

ask.searchRow

boolean — an "Ask AI about …" row at the top of the search modal at any placement

docs.search.scope

space (default) | section — the search modal searches the current docs section first, with a chip to search everything

header.layout

full (default) | sidebar-first (the sidebar runs the full height; the header sits over the article and rail) | rail-split (a brand cell as wide as the sidebar, ruled off). Wide screens only

The launchers only show on a page where Ask AI is on, so a header shared with a blog keeps them off the blog. With rail-split, pair chrome.searchPlacement: "start" and chrome.search: "icon" so the search lands inside the brand cell, and leave layout.frame unset so the header row and the docs row line up.

The search modal itself is a chrome key: chrome.searchStyle.open: "modal" (see Header and footer).

The header's own chrome keys a documentation header most often wants (2026-10-12; the full table is in the theming reference):

Key

Values

chrome.brandText

auto (default) | show | hide — the brand's words beside the logo. auto: a wordmark (logoUrl at least 1.8:1 wide, or with a logoMark beside it) carries the name, so the words are for screen readers only; a square mark gets the words beside it

chrome.searchPlacement

center now centres the pill on the header (brand and links | search | controls); before-links puts the search at the far end with the links after it

chrome.header.linksAlign

after-search — the links straight after the search pill ("Search ⌘K · Ask AI")

chrome.searchStyle.width

the pill's width from 768px; it now gives way (down to 160px) before the buttons or "Log in" are crowded out

chrome.headerLinks[].phone, chrome.ctas[].phone, chrome.cta.phone

menu | bar — keep that link or button in the phone row beside search

chrome.headerLinks[].kbd

mod-k | slash | mod-i — keycaps after the link's words; the keys follow the link

chrome.headerLinks[].ink, inkDark, border, borderDark

hex — the link's own colour and outline (a grey pill instead of the brand); ctas[] take inkDark / borderDark too

chrome.signInStyle.order

before-ctas (default) | after-ctas

chrome.subnav.phone

scroll — the section row as one sideways-scrolling row on a phone. subnav no longer needs links, so this also styles the section tabs sections.placement: "header" puts there

chrome.sidebarToggle

true, or { "placement": "end" } — the drawer's button at the far end of the row

announcement.span

page (default) | content — a below-header strip as wide as the article and rail, its left edge on the article's; the sidebar stays flush under the header

announcement.phone

full (default) | lead — below 768px only the lead and the button, one line. Every banner runs edge to edge on a phone

chrome.header.paint

page (default) | brand — the bar painted your brand with a readable ink, under any look (the bold look is this key)

chrome.header.order

the end group's slots in order: search, links, actions, themeToggle, signIn, ctas, menu — e.g. ["themeToggle", "actions", "ctas"]

chrome.header.linksAlign

after-search holds under docs.header.layout: "sidebar-first" too: the links follow the pill in one group. end packs search, links and controls into one group at the end

chrome.searchStyle.label

"" — an icon-only pill ("🔍 ⌘K") that sizes to its content

chrome.searchStyle.kbdStyle

caps (default) | plain — the key hint as grey text

chrome.searchStyle.iconFrame

none (default) | circle | square — a 32px ring around the search: "icon" glyph

chrome.searchStyle.shrink

true (default) | false — keep the pill's width instead of narrowing it first

chrome.logoPhone

full (default) | mark — logoMark instead of the wordmark below 640px

chrome.brandText

also { "show": "auto", "source": "org", "phone": "hide" } — the org's name on a lone space; no words on a phone

chrome.subnav.endLinks

up to 4 items at the end of the section row, dropdowns allowed

chrome.header.menuButton.phone

auto (default) | always | never

chrome.header.menuButton.desktop

true — ☰ at the end from 1024px with your site and footer links

{
  "chrome": {
    "brandText": "auto",
    "search": "pill",
    "searchPlacement": "center",
    "searchStyle": { "width": 420, "kbd": "mod-k", "open": "modal" },
    "header": { "linksAlign": "after-search" },
    "headerLinks": [{ "label": "Ask AI", "href": "/ask", "kbd": "mod-i", "phone": "bar" }]
  },
  "announcement": { "text": "v5 is out", "placement": "below-header", "span": "content" }
}

Header fixes that need no key: below 420px at most one filled button stays in the phone row (the first one pinned there; the rest are in the menu); no header button or sign-in label wraps onto two lines; a centred search is a search glyph on one phone row (never a second row for the menu); the phone bar never shrinks the brand or a brand-placed version pill — the search folds to a glyph first, then buttons move into the menu; and under sidebar-first the rail's column reaches the bottom of the window on a short page.

{
  "chrome": {
    "search": "pill",
    "searchPlacement": "start",
    "searchStyle": { "label": "", "kbd": "mod-k", "kbdStyle": "plain" },
    "header": {
      "linksAlign": "after-search",
      "paint": "brand",
      "order": ["links", "search", "ctas"]
    },
    "logoPhone": "mark",
    "subnav": { "endLinks": [{ "label": "Help", "href": "/help" }] }
  },
  "announcement": {
    "text": "v5 is out",
    "lead": "New",
    "placement": "below-header",
    "span": "content",
    "phone": "lead"
  }
}

Search hits from the public search (GET /public/orgs/{orgId}/spaces/{spaceId}/search) carry collection ({id, name}, or null for a top-level page) and headings (up to three {text, anchor, level} that match — link to url#anchor). The search takes collections=<id>,<id> to keep hits inside those collections.

A page's metadata.searchExclude: true leaves it out of the reader search and the site search; it is still published, linked and in the sitemap (use noindex for search engines). Ask AI and the console search still find it.

Page

How the page head, the article column and the right-hand rail read. Every key is optional; unset draws the page as it was.

Key

Values

What it does

articleAlign

start | center

start lines the article and its text up with the title's left edge instead of centring it in the column. Unset = center. The text track follows layout.measure (unset 48rem)

articleWidth

narrow | default | wide | full, or a measure ("52rem", "70ch", "832px")

The space's default article width. A page's own metadata.layout wins; this beats contentWidth. Imported OpenAPI pages follow it

topRule

boolean | solid | split

The 2px rule under the header. true or solid: one solid line in your brand colour. split: the older two-part stripe (ink for 62% of the width, then the brand). false: none. Unset = shown solid (hidden when white-label, or when motion.ambient.scrollProgress is explicitly false); the documentation defaults (every look except paper) set false, so unset draws it only under look: "paper". Changed: unset and true used to draw the split stripe; see The top rule

pageHead.description

show | hide

The standfirst under the title — metadata.subtitle, else seoDescription. It carries data-fx-lede, so typography.levels.lede styles it

pageHead.breadcrumb

{ show?, separator?: "/" | "›" | ">" | "·" | "chevron", size?: 10–20, collection?: boolean, root?: { label, href? }, home?: boolean, placement?: "head" | "header" }

A trail above the title: the collection (unless collection: false or it is the default one), each parent page, then the page. root leads it. The structured data publishes the same trail. Never drawn on the space's home page (the page the space opens on, or a page hidden from the tree with pageChrome: false) unless home: true. placement: "header" moves it into the header bar beside the rail cell, from 768px, under header.layout: "rail-split" only. The page the space root opens on counts as the home whether or not a home page is named, and a step that repeats the next one ("Payments › Payments") is dropped. phone and style are in the round-4 table below

pageHead.kicker

none | collection | breadcrumb

A small line above the title: the collection's name, or the trail

pageHead.kickerStyle

{ font?: sans|mono|display, case?: none|upper, size?: 10–20, tracking?: -0.05–0.3, weight?: 300–800, color?, colorDark? }

How the kicker looks. Unset: 12px, semibold, uppercase, the eyebrow colour

pageHead.updated

top | footer | hide | actions

Where "Updated …" and the contributors sit. Unset = top. actions leads the actions row with a clock and "Last updated Mar 4, 2026", so the head has one meta row (the contributors stay in the byline)

pageHead.updatedFormat

date | relative | datetime

"Mar 4, 2026", "3 days ago", or the date with the time

pageHead.contributors

boolean

The contributors beside the date. Unset = shown

pageHead.rule

boolean | article | content | column

A hairline under the page head. true/article spans the article; content runs on to the rail's far edge (from 1280px, where the rail sits beside the article) and the rail starts below it, so the rule never crosses the contents list; column runs from the content column's edge to the rail's start edge and stops there

pageHead.titleGap

0–48

The gap between the title and the standfirst, in px. Unset = about 4px

pageHead.icon

boolean

false drops the page's icon before the title; the tree keeps it. Unset = drawn

pageActions.placement

below-title | title-end | rail | none | header

Where the actions bar goes. title-end puts it on the title's row — on a hideTitle page, at the article's top end. header: in the header bar beside the rail cell, from 768px, under header.layout: "rail-split" only (anywhere else it reads as below-title)

pageActions.label

text, ≤ 40

A small mono heading over the actions where they sit in the rail ("AI tools")

pageActions.style

pills | split | menu | text

split: the primary action plus a ▾ menu of the rest; menu: one "Page actions" button; text: inline links

pageActions.items

1–8 of copy, markdown, chatgpt, claude, perplexity, cursor, vscode, mcp

Which targets, in order. Unset = copy, markdown, ChatGPT, Claude, MCP. cursor/vscode install the space's MCP server in one click

pageActions.primary

one of the items

The one shown first (alone, in split)

pageActions.phone

disclosure | icon | bar | text | hide

Under 640px, icon shows glyphs only; text keeps every item as a labelled text link on one row ("Copy page | View as Markdown"), whatever style is; hide drops the bar there. bar: under 768px, one row under the header — the actions as one split control, the variantSwitcher select when the space has one, and a contents button — and the head's own actions and phone contents step aside

toc.levels

{ min: 2–4, max: 2–4 }

Which heading levels the on-this-page rail lists. Unset = h2–h3. A page's metadata.tocLevels wins

toc.steps

boolean

Also list stepper step titles

toc.label

text, or false

The rail's heading; false hides it (the list keeps its accessible name)

toc.labelCase / labelSize / labelFont / labelColor / labelColorDark / labelIcon

upper|none / 10–20 / sans|mono|display / #hex / #hex / boolean

The label's look

toc.itemSize

11–18

The entries' size in px

toc.activeColor / activeColorDark / activeStyle

#hex / #hex / bar | text | fill

The current entry: a bar (default), coloured text only, or a soft fill

toc.extras

up to 4 of backToTop, feedback, edit, actions

What stacks under the rail, in this order. The rule between them and the contents list is drawn only when the list is (a page with too few headings has none)

toc.extrasPlacement

before | after

before stacks the extras above the contents list. Unset = after

toc.phone

hide | disclosure | select

Under 1280px, where the rail is hidden: nothing (default), a collapsible list, or a "Jump to a section" select — after the whole page head (title, standfirst or lede, the meta row and byline), 16px below it. toc.phonePlacement moves it

panel

{ background?, backgroundDark?, border?, borderColor?, borderColorDark?, radius?: 0–32, inset?: 0–48 or { top?, right?, bottom?, left? } (each 0–48), padding?: 0–80 or {sm,md,lg}, shadow?: none|sm|md, includeRail?, phone? }

An inset surface around the article (and the rail, with includeRail), from 768px up — from 0px with phone: true, padded by padding.sm (unset 20px). A number inset is the gap at the right and bottom; the object sets each side (a side left out keeps that reading)

aside

{ width?: measure, top?: 0–200 }

The lifted aside column (a column with role: "aside"): its width (unset 24rem) and sticky offset in px

onThisPage: "always" now means it: one heading is enough for a rail, and a page with no heading at all drops the empty column.

Page metadata

Set with PATCH …/pages/{pageId}/metadata.

Key

Values

What it does

hideTitle

boolean

Hides the title on screen; it stays the page's <h1> for screen readers and search

subtitle

≤ 300 chars

The standfirst, when it should differ from seoDescription

hideUpdated

boolean

No "Updated …" on this page

hideCover

boolean

Keeps coverUrl for the share card, not on the page

layout

narrow | default | wide | full | center | custom

center: no tree, no rail, centred. custom: no tree, no rail, no width cap, title for screen readers only, no page chrome

template

doc | splash

splash: a hero instead of the title, no rail, full width, no page chrome

hero

{ title?, tagline?, image?, imageDark?, imageAlt?, actions?: up to 4 { text, href, variant?: primary|secondary|minimal, icon? }, layout?: stack|split, align?: start|center, titleSize?: length }

The splash hero. title defaults to the page title. titleSize (72, "4.5rem", "clamp(…)") sets the hero title's size, capped at 12vw on a phone; unset = 36px, 48px from 768px

tocLevels

{ min, max } or null

This page's rail depth

pageChrome

boolean

false drops the actions bar, "Updated", feedback and previous/next

titleAlign

start | center

Centres the page head

hero.alignPhone

start | center

The hero's alignment below 768px, where a split hero stacks. Unset = follows hero.align. Example: { "hero": { "align": "start", "alignPhone": "center" } }

themePatch

{ components?, docs?: { pageHead?, toc?, articleWidth?, pageActions?, frame?, panel?, feedback?, pager?, api?: { titleSize? }, blocks?: { prompt?: { icon? } } } } or null

This page's own slice of the theme — see Per-page theme below

Per-page theme (metadata.themePatch)

Scope: page. One page can wear a different block look or page head than the rest of the space — a landing whose cards are tiles while every guide keeps the outlined card, or one long reference page with a deeper contents rail. The patch takes any theme.components.* key and the docs page keys pageHead, toc, articleWidth, pageActions, frame, panel, feedback, pager, api.titleSize and blocks.prompt.icon, with exactly the shapes those keys have on the space theme. It is merged over the space theme for that page only (its CSS is scoped to [data-docs-page="<page id>"], so a neighbour never inherits it after a client-side navigation). Unknown keys are refused with a 422 naming them; the patch is replaced whole on each write and null clears it. Unset = the page follows the space.

PATCH /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/metadata
{ "themePatch": { "components": { "card": { "border": "tile" } },
                  "docs": { "toc": { "levels": { "min": 2, "max": 4 } }, "pageHead": { "rule": "column" } } } }

Where a value comes from, highest first: block attribute (a cards block's own border) → page themePatch → space theme → the look's patch → the built-in default.

{
  "docs": {
    "articleAlign": "start",
    "topRule": false,
    "pageActions": {
      "placement": "title-end",
      "style": "split",
      "primary": "copy",
      "phone": "icon"
    },
    "pageHead": { "updated": "footer", "breadcrumb": { "separator": "chevron" } },
    "toc": { "labelCase": "none", "labelSize": 13, "steps": true, "phone": "disclosure" }
  }
}

Stable hooks for customCss: [data-docs-crumbs], [data-docs-kicker], [data-docs-title-row], [data-docs-head-rule], [data-docs-top-rule], [data-docs-rail] (toc | aside | pane), [data-docs-rail-extras], [data-docs-aside], [data-docs-hero], [data-docs-panel], [data-docs-title-float] (a hidden title's actions), [data-docs-rail-actions-label], [data-docs-phone-bar], [data-docs-header-head] (the crumb and actions in a rail-split header), .page-actions-updated.

{
  "docs": {
    "pageHead": {
      "titleGap": 16,
      "icon": false,
      "rule": "content",
      "updated": "actions",
      "breadcrumb": { "show": true, "placement": "header" }
    },
    "pageActions": { "placement": "header", "style": "split", "phone": "bar", "label": "AI tools" },
    "toc": { "extras": ["feedback", "actions"], "extrasPlacement": "before" },
    "panel": { "phone": true, "radius": 12, "inset": { "top": 0, "right": 8, "bottom": 8 } }
  }
}

Round 4 page keys

Every key optional; unset = today's page. Scope theme means theme.docs.* (also accepted per page inside metadata.themePatch.docs); page means metadata.*.

Key

Scope

Shape

Default

Example

pageHead.breadcrumb.phone

theme / page

full | collapse | parent | hide

full (the whole trail, wrapping). The DOCS default layer sets collapse

{ "pageHead": { "breadcrumb": { "phone": "collapse" } } } — below 640px the first and the last two steps on one line, the middle behind a "…" that unfolds in place; parent = one "‹ Parent" link; hide = no trail on a phone

pageHead.breadcrumb.style

theme / page

trail | kicker

trail

{ "pageHead": { "breadcrumb": { "style": "kicker" } } } — no trail; the page's section (its nearest group) as a small upper-case eyebrow above the title, unlinked, in kickerStyle's look

pageHead.rule: "column"

theme / page

see pageHead.rule

—

{ "pageHead": { "rule": "column" }, "toc": { "border": true } }

pageActions.phone: "text" / "hide"

theme / page

see pageActions.phone

—

{ "pageActions": { "style": "split", "phone": "text" } }

pageActions.phoneToc

theme / page

text | icon

text

{ "pageActions": { "phone": "bar", "phoneToc": "icon" } } — the phone bar's contents button as the glyph alone (its label stays the accessible name)

toc.phonePlacement

theme / page

after-head | after-title | hide

after-head

{ "toc": { "phone": "select", "phonePlacement": "after-title" } } — after-title puts the phone contents straight under the title; hide removes it (and the phone bar's contents button) on phones

toc.border / borderColor / borderColorDark

theme / page

boolean / #hex / #hex

none / the hairline colour

{ "toc": { "border": true, "borderColor": "#e4e4e7" } } — a full-height rule down the rail's start edge, from 1280px

toc.extrasOnAside

theme / page

below | hide

below

{ "toc": { "extrasOnAside": "hide" } } — where the rail holds a lifted aside or the API pane, the extras go under it (always, even with extrasPlacement: "before"), or not at all

layout.guides (scope theme — the hairline column guides behind every reader page: columns, width, color, style, outer, gutter) gained two keys that matter on a docs page: layout.guides.gutter.inset (0–96 px, unset 0) is the space between each gutter band and the text — on a docs page the bands hug the article, so this is how far from the prose they sit; layout.guides.placement (content | page-edge, unset content) pins the END band to the page's far edge. Example: { "layout": { "guides": { "columns": 1, "gutter": { "pattern": "stripes", "inset": 48 }, "placement": "page-edge" } } }.

What every page now does, with no key (fixes, not options):

  • No breadcrumb on the home page. The trail there was the page's own name. pageHead.breadcrumb.home: true brings it back. The home is also the page the space root sends a reader to when nothing names one (the first page), and a step that repeats the next ("Payments › Payments") is dropped.

  • The phone contents list follows the whole page head (toc.phone) — the title, the standfirst or a large opening paragraph, the meta row — with a 16px gap, not the title alone (it read as the page's first paragraph). toc.phonePlacement is the way back to "under the title".

  • No background guide hairlines on a phone. Below 768px layout.guides draws neither the gutter bands nor the 1px rules.

  • With the phone bar drawn, a hidden title's floating actions row takes no height and the gap from the bar to the title is 16px.

  • A hideTitle page keeps its title-row actions, at the article's top end, and the contents list skips the heading that stands in for the title (a heading marked data-hero-heading, a heading inside [data-docs-hero], or the heading that leads the body of a hideTitle page).

  • Tables never crush. A cell in a table's scrolling frame keeps its words whole (columns at least 6ch, header cells on one line); a wide table scrolls inside its frame instead of breaking "Required" letter by letter.

  • Code is set in the system code face, not Courier: --font-mono defaults to ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace on the reader, and monoFontFamily still wins.

  • Four cards go four across (from a 720px track; two by two below it) instead of three and an orphan — when the cards block sets no columns and is not the row or horizontal look.

Round 5 page keys

Same rules as round 4: every key optional, unset = today's page; scope theme is theme.docs.*, also accepted per page in metadata.themePatch.docs. "Layer" says whether the DOCS default layer sets it.

Key

Scope

Shape

Default

Layer

Example

pageHead.rule: "bleed"

theme / page

a new value of pageHead.rule

—

no

{ "pageHead": { "rule": "bleed" } } — the hairline runs from the content column's outer edge (the sidebar's border) to the rail's border (with no rail beside it, the content area's edge), from 768px

pageHead.ruleColor / ruleColorDark

theme / page

#hex

the line colour

no

{ "pageHead": { "rule": "bleed", "ruleColor": "#e4e4e7", "ruleColorDark": "#3f3f46" } } — every rule spelling (article, content, column, bleed) takes it

pageHead.breadcrumb.kickerCase

theme / page

upper | none

upper

no

{ "pageHead": { "breadcrumb": { "style": "kicker", "kickerCase": "none" } } } — the section eyebrow as written ("Sending", not "SENDING")

pageHead.breadcrumb.kickerColor / kickerColorDark

theme / page

#hex

kickerStyle.color, else the eyebrow ink

no

{ "pageHead": { "breadcrumb": { "style": "kicker", "kickerColor": "#71717a" } } }

pageHead.breadcrumb.kickerSize

theme / page

px 11–16

kickerStyle.size, else 12px

no

{ "pageHead": { "breadcrumb": { "style": "kicker", "kickerSize": 13 } } }

pageActions.phoneIcon

theme / page

primary | code

primary

no

{ "pageActions": { "phone": "bar", "phoneIcon": "code" } } — the phone bar's split button wears the PRIMARY item's own glyph (the copy glyph for "Copy page"); code is the code glyph it used to show for every item

toc.onHero

theme / page

show | hide

show

hide

{ "toc": { "onHero": "hide" } } — a page that opens with a hero has no on-this-page rail and the article takes its width. A hero is: metadata.hero with a title or tagline; a first body block that is a section; a first block hooked or classed hero; or a hideTitle page's first heading. Decided on the server, so nothing shifts. A page's metadata.themePatch { "docs": { "toc": { "onHero": "show" } } } keeps its rail

What every page now does, with no key (round 5 fixes):

  • The phone text row never starts a line with a divider. With pageActions.phone: "text" below 640px, "Last updated" (pageHead.updated: "actions") takes its own line, and the actions are ONE nowrap row of the first two items; the rest open from the row's menu glyph. The count is made on the server, so the row never reflows.

Cards, tables, stepper and callouts on the page

These are theme.components.* keys, so they apply to every space type. All optional; unset draws today's block.

Key

Values

What it does

card.background / backgroundDark

#hex

Fills every card

card.iconAlign

start | center

start: the panel look's glyph in the top-left of its well, and the horizontal/row looks' icon at the top. center: the icon (and the text) centred over a stacked card, and centred on the row in the horizontal/row looks

card.ctaPlacement

below | end

horizontal cards: end puts the CTA at the row's end, vertically centred

card.border

card | tile | none

card is today's outline. none drops it; tile drops it and rules only the icon, as a 40px tile

table.more

{ style?: outline|solid, align?: start|center, fade?: boolean }

A collapsed table's "Show more": the outline pill (unset) or a solid brand button, at the start or centred, and fade fades the last visible rows into it

table.codeInk / codeInkDark

up to 6 #hex

Inline code colour per column, first column first — a class column in one ink, a value column in another

stepper.markerSize

16–48

The marker's diameter in px. Unset = 25.6

stepper.markerAlign

first-line | title

title centres the marker on the step title's line (the line is set to the larger of the marker and 1.4 × titleSize). Unset/first-line = top-aligned

hint.variants.<style>.titleGap

0–24

The gap under that callout style's title, in px (also published as --hint-title-gap on the callout). Unset ≈ 5.6px

{
  "components": {
    "card": {
      "background": "#f6f7f9",
      "backgroundDark": "#16181d",
      "iconAlign": "start",
      "border": "tile"
    },
    "table": {
      "more": { "style": "solid", "align": "center", "fade": true },
      "codeInk": ["#0284c7", "#7c3aed"]
    },
    "stepper": { "markerSize": 24, "markerAlign": "title", "titleSize": 24 },
    "hint": { "variants": { "info": { "titleGap": 4 }, "warning": { "titleGap": 4 } } }
  }
}

Blocks

Package D adds no theme.docs keys: the docs blocks' looks are theme.components.* (table, stepper, card, kbd, tabs, hint.variants, expandable.*Dark) and apply to every space type — see theme-components.md › "Docs blocks (wave D)".

One key sets a block's look for the whole space (round 3):

Key

Values

Default

What it does

blocks.prompt.variant

card, compact, pill

compact on a DOCS space (the DOCS layer); unset elsewhere, which is card

The look a prompt block takes when it names no variant of its own. A prompt that sets one keeps it.

blocks.prompt.layout

inline, stacked

inline

Round 4: stacked puts the actions under the text.

blocks.prompt.primary

copy, open

neither

Round 4: that action is drawn filled in the brand.

blocks.prompt.copyStyle

button, icon

button

Round 4: the copy control as the glyph alone.

blocks.prompt.phoneTargets

1–6

1 under compact

Round 4: the "Open in …" links a compact prompt keeps in its phone row; the rest go under More.

blocks.prompt.openInDesktop

0–6

all (today). The DOCS layer sets 2

Round 5: the "Open in …" links any prompt keeps in its row from 641px; the rest fold into a More menu drawn on the server (a native disclosure: Enter/Space open it, Escape closes it). Example: { "docs": { "blocks": { "prompt": { "openInDesktop": 2 } } } }

blocks.prompt.icon

a glyph name, or a short glyph

none

Round 5: drawn before the label of every prompt that names no icon of its own. Example: { "docs": { "blocks": { "prompt": { "icon": "sparkles" } } } }

blocks.prompt.labelCase

upper, none

upper

Round 5: none sets the label as written.

blocks.prompt.background / backgroundDark

#hex

today's surface

Round 5: the prompt's fill, light and dark. A prompt's own attrs win.

blocks.prompt.collapseStyle

footer, pill

footer

Round 5: a folded prompt (collapse) shows "Show more" as today's full-width bar, or as a floating "⌄ Show more" pill centred over the fade.

blocks.prompt.labelSize

10–20 (px)

11.2px (0.7rem); 14px on a compact row

Round 6: the label's size. Example: { "docs": { "blocks": { "prompt": { "labelSize": 14, "labelWeight": 500, "labelCase": "none" } } } }

blocks.prompt.labelWeight

100–900

600

Round 6: the label's weight.

blocks.prompt.labelColor / labelColorDark

#hex

the muted grey (ink on a compact row)

Round 6: the label's colour, light and dark.

blocks.prompt.divider

boolean

drawn

Round 6: false drops the rule under the head (and over a stacked actions row) — one undivided card.

blocks.prompt.actionsAlign

start, end

inline: Copy beside the label, "Open in …" at the end; stacked: from the start

Round 6: where Copy and "Open in …" sit in their row, as a group. Card rows only (a one-row compact and the pill keep their order); a prompt with its own align keeps it.

{ "docs": { "blocks": { "prompt": { "variant": "pill" } } } }

The blocks gained these attributes in round 3, each optional and each drawn only when set (the full table, with values, is in Authoring › Round 3 block attributes):

  • tabs: asCodeGroup (unset: a group of nothing but code blocks draws as a code group; false opts out) and variant: "card".

  • prompt: variant: "pill", align, textStyle: "prose", collapse, and {icon, label} logos in targets.

  • heading: icon, iconStyle (inline/tile), lineHeight (0.9–2). On a hideTitle page the first heading is marked data-hero-heading, and the rail skips it.

  • gridItem: textAlign, icon, iconStyle; cells stretch to their row.

  • section: pattern (dots/grid), patternColor, patternFade (radial/bottom).

Their hooks for elementStyles / custom CSS: .blk-icon[data-icon-style], .forumulate-prompt[data-variant="pill"], .prompt-logo, .prompt-more, .tabs[data-variant="card"], .doc-section[data-pattern], [data-hero-heading].

Code

Code has no theme.docs keys: everything about a code block lives in components.code / components.codeDark, which every space type reads — the copy button (copy), blocks and inline code painted separately (block, inline), the frame look, the language label, wrapping, terminal bars and the line colours. See "Code blocks and inline code separately" in the theming reference.

Key

Values

What it does

components.code.tabFont

ui | mono

The code group's tab labels (and its dropdown picker): ui sets them in the body face, mono in the code face. Unset = the code face. Published as --code-tab-font, which the API reference's sample tabs read too

{ "components": { "code": { "tabFont": "ui" } } }

Round 5 (R5-005, a fix): a code group's tabs already name each sample, so a showLanguage: true that comes from the THEME (the DOCS default layer sets it) no longer draws a language chip inside a code group — a standalone block keeps its chip. A group (or a block in it) that sets showLanguage: true itself still gets one, and the theme value "always" draws it everywhere, groups included: { "components": { "code": { "showLanguage": "always" } } }. The DOCS layer also sets components.code.tabFont: "ui", so a group's tab labels read in the interface face.

The language chip never covers code (a fix): a block whose first line runs under the chip reserves the chip's width on that line, and a terminal block carries the chip in its bar.

What a documentation space gets when those keys are unset is its DOCS default: an icon copy button, a pale bordered block (#fafafa / #e5e7eb, radius 10; #16181d / #2a2e35 in dark), a grey inline chip, the language label shown, and the github code theme when you named none. Every key you set wins.

API reference

Owner: package F. All under theme.docs.api; every key unset draws today's reference. (The DOCS defaults — split, landing, list — arrive with the defaults flip, not here.)

Key

Values

Unset

What it does

layout

stacked, split

stacked

split: on a page with exactly one top-level operation block, at ≥1280px the code samples, responses and "Try it" sit in a sticky pane beside the operation, in the width the on-this-page rail gave up (the rail is dropped on that page). Below 1280px it is one column: samples, then responses. A block's own layout attr wins.

params

table, list

table

list: parameters and fields as rows (name, type, required, description) with nested children behind "Show child parameters". A block's own params attr wins.

sdkPanel

every, landing, none

every

Where the SDK download panel mounts. landing: only on the reference's landing page (after its first paragraph), or on an unlabelled page that has an operation block.

examples.languages

up to 10 of curl typescript python go php ruby java csharp kotlin swift

the first seven

Which sample languages, in this order.

examples.default

one of the above

—

The tab shown first, until the reader picks one (their pick is remembered).

examples.picker

tabs, dropdown

tabs

A select instead of a tab strip.

examples.tabStyle

underline, pill

underline

The language strip's look. pill: small rounded pills, the active one tinted. Either way the strip is ONE row that scrolls sideways when the languages outgrow it — it never wraps.

examples.lineNumbers

boolean

false

A numbered gutter on every sample — the code block's own (numbers are never copied).

playground.display

interactive, simple, none

interactive

simple hides the header editor and the proxy toggle; none drops "Try it" and its payload.

playground.open

boolean

false

Open the console on load.

playground.prefill

boolean

false

Start parameters at the spec's examples.

playground.rememberAuth

page, session

page

session keeps credentials for every console of this space in this browser tab (sessionStorage, keyed by space and scheme, cleared when the tab closes — never localStorage), with a Forget button. Signed-in prefill from the reader's claims is not built.

specLink

boolean

false

An "OpenAPI spec" download link in each operation's head row (…/api-export/spec).

paramName.chip

boolean

true

false: parameter and field names as a bare bold mono name — no border, no fill. The DOCS default layer sets false. Example: { "api": { "paramName": { "chip": false } } }

textSize

12–18 (px)

today's sizes

The size of descriptions in parameters, fields, the body and responses. The DOCS default layer sets 14. Example: { "api": { "textSize": 14 } }

bodyHeading

disclosure, plain

disclosure

plain: the body's heading is a plain "Parameters" title (no ▾, no media type) — "Request body" when a path/query Parameters table is also there — and the Parameters section stops folding too. Example: { "api": { "bodyHeading": "plain" } }

url

full, path

full

path prints /emails rather than https://api.example.com/emails in the head (samples still call the full URL).

urlStyle

chip, pill

chip

pill: method and address as ONE bordered pill — "POST /emails", the method in its colour as text — that never wraps (it scrolls inside itself on a phone). Example: { "api": { "url": "path", "urlStyle": "pill" } }

phone.samples

after-params, first

after-params

Below 768px: first puts the code samples straight after the description.

phone.frame

boolean

true

false drops the operation's bordered card below 768px. Example: { "api": { "phone": { "samples": "first", "frame": false } } }

phone.url

full, path

full

Round 5: the address below 768px. path drops the server URL there ("POST /v11/projects"); desktop keeps url's choice. The DOCS layer sets { "frame": false, "url": "path" }. Example: { "api": { "phone": { "url": "path" } } }

urlStyle: "plain"

a new value of urlStyle

—

Round 5: the address as bare mono text — no chip, no border, no fill. With components.apiRef.methodStyle: "text" the head is two words of text. Example: { "components": { "apiRef": { "methodStyle": "text" } }, "docs": { "api": { "urlStyle": "plain" } } }

titleSize

px 20–48, or { "sm"?, "lg"? }

the theme's h1

Round 5: the page title's size on an OPERATION page (one carrying an API operation block) — guide pages are never touched. sm is below 768px, lg from it. Also accepted per page in metadata.themePatch.docs.api. Example: { "api": { "titleSize": { "sm": 24, "lg": 28 } } }

responses.style

list, panel

list

Round 5: panel attaches the example response under the code samples as a pale panel with a small "RESPONSE" strip; the status list folds into the strip's select (no JS: the first response shows).

responses.background / backgroundDark

#hex

a 4% ink wash

Round 5: the panel's surface, light and dark.

responses.ink / inkDark

#hex

the reader ink

Round 5: the response body's text on the panel. The code blocks' ink is often meant for a dark block, so the panel no longer borrows it.

responses.header

label, none

label

Round 5: none drops the strip's "Response" word and keeps the select. Example: { "api": { "responses": { "style": "panel", "background": "#f6f8fa", "backgroundDark": "#161b22" } } }

All of these are scope theme (theme.docs.api); a block's own layout and params attrs still win over the theme's.

The sample bar. Every operation's code samples sit under ONE header bar (~36px), like a code group: the language tabs at the code header's 13px (in components.code.tabFont's face, via --code-tab-font), and at the bar's end the copy control — plus an Ask-AI button when the space has an Ask-AI panel. The copy control follows components.code.copy, the same key the code blocks read: icon (and unset) is a glyph, text the words "Copy"/"Copied", cell a full-height cell divided from the tabs, none (or components.code.actions without "copy") no button. Example:

{ "docs": { "api": { "examples": { "tabStyle": "pill", "lineNumbers": true } } } }

Independently of these keys, every reference now: breaks the URL chip only at slashes, does not repeat the page title as a summary under the H1, renders descriptions as markdown, keeps the parameter table's Type/Required columns readable, and paints the SDK panel in the brand colour. Round 5 adds (fixes, no key): on a phone the operation head is ONE row — the method inline before the URL, the URL one line that scrolls in its own track with an end fade, never wrapped mid-path; in the split layout the parameter table fits its column (types wrap, "Required" never clips); and inside urlStyle: "pill" the path has no second inline-code fill — the pill is one flat surface. Round 4 adds: the samples are syntax-highlighted with the same highlighter and palette as a guide's code blocks (components.code.syntax / syntaxTheme, light and dark; curl is coloured as shell); the sample bar's language select, copy and ask controls take the code block's ink (components.code.block.ink), not the page's, so they read on a dark code bar over a light page; and inside a phone panel (panel.phone) the operation drops its own frame — no box in a box.

Mix and match: page and API reference keys are building blocks

Nothing on the page is tied to a look. A look (theme.docs.look) is only a PATCH of the individual keys documented above — it writes some of them for you, and every one of them works under every look, and under none. Each key has one scope: theme (theme.docs.*, theme.components.*, theme.layout.*), page (metadata.*, including metadata.themePatch), or block (a block's own attributes). The nearest scope wins: block → page → theme → look → default. Three combinations that are not any showcase replica, each a complete body:

A quiet reference with a loud landing. Stripe-style parameters and a path pill on the clean look; the space's home page alone gets tiles and no rail border. PATCH …/theme:

{
  "docs": {
    "look": "clean",
    "pageHead": { "rule": "column", "breadcrumb": { "show": true, "phone": "parent" } },
    "toc": { "border": true, "phone": "disclosure", "phonePlacement": "after-title" },
    "api": {
      "paramName": { "chip": false },
      "textSize": 14,
      "bodyHeading": "plain",
      "url": "path",
      "urlStyle": "pill",
      "phone": { "samples": "first", "frame": false }
    }
  }
}

then the home page's own metadata:

PATCH /orgs/{orgId}/spaces/{spaceId}/pages/{homeId}/metadata
{ "themePatch": { "components": { "card": { "border": "tile" } },
                  "docs": { "toc": { "border": false } } } }

A paper handbook with a kicker and a phone that stays out of the way. The default (paper) look, the section as an eyebrow instead of a trail, the page actions as text links on phones and the contents control gone there:

{
  "docs": {
    "pageHead": {
      "breadcrumb": { "show": true, "style": "kicker" },
      "kickerStyle": { "font": "mono", "size": 11 },
      "updated": "actions"
    },
    "pageActions": { "style": "split", "phone": "text" },
    "toc": { "phone": "select", "phonePlacement": "hide", "extrasOnAside": "hide" }
  }
}

A design-system reference with ruled guides and a branded API card. Guides 48px off the text with the end band at the page edge, a content-wide head rule (the rail starts under it), the phone bar with an icon-only contents button, and the API card from components.apiRef:

{
  "layout": {
    "guides": {
      "columns": 1,
      "gutter": { "pattern": "stripes", "inset": 48 },
      "placement": "page-edge"
    }
  },
  "components": { "apiRef": { "methodStyle": "soft", "card": true } },
  "docs": {
    "pageHead": { "rule": "content", "breadcrumb": { "show": true, "phone": "collapse" } },
    "pageActions": { "phone": "bar", "phoneToc": "icon" },
    "api": { "layout": "split", "examples": { "picker": "dropdown" } }
  }
}

A changelog-grade API with a quiet head and a panel. Round 5's keys on the muted look: the head rule bleeding to the sidebar in a faint ink, the section as a sentence-case grey kicker, rail-less landings, text-only method and URL with a 26px operation title, responses as a panel, and every prompt folded to two assistants with a pill fold:

{
  "components": { "apiRef": { "methodStyle": "text" } },
  "docs": {
    "look": "muted",
    "pageHead": {
      "rule": "bleed",
      "ruleColor": "#ececec",
      "ruleColorDark": "#26262b",
      "breadcrumb": {
        "show": true,
        "style": "kicker",
        "kickerCase": "none",
        "kickerSize": 13,
        "kickerColor": "#71717a"
      }
    },
    "toc": { "onHero": "hide" },
    "pageActions": { "phone": "bar", "phoneIcon": "primary" },
    "api": {
      "urlStyle": "plain",
      "titleSize": 26,
      "phone": { "url": "path" },
      "responses": { "style": "panel", "header": "none" }
    },
    "blocks": {
      "prompt": {
        "openInDesktop": 2,
        "collapseStyle": "pill",
        "labelCase": "none",
        "icon": "sparkles"
      }
    },
    "feedback": {
      "style": "labelled",
      "third": true,
      "labels": { "yes": "Yes, thanks", "no": "Not really" }
    }
  }
}

Every key is optional; unset renders what the reader rendered before the key existed, except where the documentation defaults say otherwise (see Look and defaults).

feedback

"Was this page helpful?"

{ "docs": { "feedback": { "style": "pill", "scale": 4, "align": "start", "placement": "rail" } } }

Key

Values

Unset

show

false removes the widget. Page views are still counted.

shown

placement

foot, rail (under the on-this-page list on wide screens; the foot keeps a copy for phones)

foot

style

buttons (Yes / No), pill (one rounded control), thumbs (icons only, after the label), labelled (round 5: worded buttons with no glyph)

buttons

third

true adds an "I have feedback" button that opens the comment form straight away (a comment sent from it is filed as a comment with no verdict — rating: "comment", outside the yes/no counts; Skip sends nothing). Round 5

none

labels

{ "yes"?, "no"?, "other"? }, each up to 40 characters — the buttons' words (the third button's too). Round 5

"Yes" / "No" / "I have feedback", translated

scale

2 (up / down) or 4 (four steps, sent as score)

2

align

start, center, end

start

label

Up to 80 characters

"Was this page helpful?", translated

comment

When the comment box opens: down (after a negative vote), always, none

down

metadata.hideFeedback: true removes it from one page. The comments readers write are at GET …/docs/feedback and under Analytics in the console.

{
  "docs": {
    "editLink": { "urlTemplate": "https://github.com/acme/docs/edit/main/docs/{path}" },
    "issueLink": {
      "label": "Report a problem",
      "urlTemplate": "https://github.com/acme/docs/issues/new?title={title}&body={url}"
    },
    "suggestEdits": { "show": true }
  }
}

urlTemplate is an absolute https URL. Its placeholders are filled and URL-encoded: {url} (the page's address), {title}, {slug} and {path} — the repository file a repository sync recorded in metadata.sourcePath, else <slug>.md (which is the file name our own export writes). For a GitHub sync the edit template is https://github.com/<repo>/edit/<branch>/<folder>/{path}. metadata.editUrl overrides the template for one page, and false hides the link there. label replaces "Edit this page" / "Report an issue".

suggestEdits.show offers "Suggest an edit" to signed-in members only; it opens the space's change requests in the console, where the usual eligibility check applies. Readers without an account cannot suggest edits.

pager

{ "style": "links" | "cards", "show": false } — previous / next at the page foot. cards draws each as a bordered box. Per page, metadata.hidePager and metadata.pager (prev / next as a page id or false, and label) override the tree's order.

versionSwitcher and localeSwitcher

{ "docs": { "versionSwitcher": { "placement": "header", "style": "select", "label": "Release" } } }

Key

Values

Unset

placement

sidebar, header (with the header's controls), brand (beside the logo)

sidebar

style

pills, select (a compact menu that works without JavaScript)

pills

label

Up to 40 characters, or false to hide it

"Version" / "Language", translated

A header placement falls back to the sidebar when the header has no slot for it. A version's badge and banner are set on the version itself (PATCH …/docs/versions/{key}, see the authoring reference). A version's address is /<key>/<page>, for example /v2/install. The older ?version=<key> form still works.

variantSwitcher

One control that switches every tabs block with the same syncKey — "show content for npm / pnpm / yarn" across the whole site:

{
  "docs": {
    "variantSwitcher": {
      "syncKey": "pm",
      "label": "Package manager",
      "options": [
        { "value": "npm", "label": "npm" },
        { "value": "pnpm", "label": "pnpm" }
      ],
      "default": "npm",
      "placement": "rail"
    }
  }
}

Pair it with tabs whose syncKey is pm and whose tab titles are the option labels (:::tabs{syncKey="pm"}). The choice is remembered across pages and written to ?tabs=pm:<label>. placement is sidebar (unset), rail or header. Showing whole blocks per variant is not a feature: use tabs.

localeFallback

"404" (unset) or "source". With source, a language address (/docs/es/<page>, or ?locale=es in query style) for a page that has no translation in that declared language serves the original under a "not translated yet" notice. Its canonical is the original. It is not noindex, because a canonical already says it is a copy. It is never in hreflang or the sitemap. See Language addresses.

Elsewhere

You want

Where it lives

Site-wide meta tags

customHead on …/analytics-tags; its <meta> and <link> tags render on every host.

Scripts on documentation pages

Theming, advanced → Which consent tier: scripts run only on a verified custom domain.

Title pattern, site name, hidden pages in search

seo.titleTemplate, seo.siteName, seo.indexHidden (theming-advanced, SEO keys).

Page keywords

metadata.keywords.

Look and defaults

What changed on 2026-10-12, and the way back

Every documentation space whose owner had not set these keys now reads with the clean look and the defaults below. Nothing was written to your space: the defaults are resolved when a reader loads the page, under every key you set, so anything you chose still wins.

To keep the reader exactly as it was:

{ "docs": { "look": "paper" } }

paper restores everything except the PDF tool: the space-wide PDF is now off by default on documentation spaces because it is a type declaration, not a theme key. Turn it back on with settings.readerTools.pdf: true.

The defaults

Area

Default (unset)

Key that changes it

Paper and ink

neutral white / near-black, dark #0b0b0f

palette, paletteDark

Type

UI sans, 16px body on 1.6, H1 36px 600 −0.02em, H2/H3 600

fonts, typography

Header

always on: logo, search pill centred on the header with a ⌘K chip, ONE theme button that cycles system → light → dark

chrome.search, chrome.searchPlacement, chrome.searchStyle, chrome.themeToggle.style (cycle)

Brand words

beside a square mark; screen-reader only beside a wordmark (no "Acme" image + "Acme" text)

chrome.brandText

Brand repeat

no brand at the top of the sidebar (a lone space; inside a site the site decides)

chrome.sidebarBrand

Article

start-aligned with the title

docs.articleAlign

Sidebar groups

collapsible, sentence case, 600, 14px, ink; chevron at the end

docs.sidebar.groups, .groupLabel, .chevron

Active row

brand text, 600, soft brand tint

docs.sidebar.active

Sidebar

272px; its search box hides when the header has one (36px tall when shown); API method badges; a long label clamps to two lines

docs.sidebar.width, .search, .searchHeight, .methodBadges, .item.lines

Phone

a drawer, labelled with the section, opening on the section list

docs.mobileNav (sections: "top")

Page actions

one split "Copy page ▾" at the title's end; an icon on a phone

docs.pageActions

Updated

in the page footer

docs.pageHead.updated

Breadcrumb

shown (never on the space's home page)

docs.pageHead.breadcrumb

Title → lede

16px

docs.pageHead.titleGap

Prompt block

the compact look

docs.blocks.prompt.variant

Stripe under the header

none

docs.topRule

On this page

sentence-case 13px label; stepper steps indexed; a disclosure on phones

theme.docs.toc

Code

copy icon, language chip, neutral block and inline surfaces, the github code theme — only where you chose no code colour

components.code, codeDark, syntaxTheme

Links

underlined when your brand is under 3:1 against the ink (so a link is findable without colour)

components.link.underline

Tables

hairline rows, compact, no zebra

components.table

Lists

a muted dash

components.list.marker

Tabs, steps, keys

scrolling tab rows, outlined step markers on a line, keycaps

components.tabs, components.stepper, components.kbd

Pager

cards

docs.pager.style

Feedback

a pill

docs.feedback.style

API reference

two-pane at ≥1280px, the SDK panel on the landing page only, parameters as a list

theme.docs.api

Ask AI

a floating launcher, plus an "Ask AI" row in search

theme.docs.ask

A lone logo in dark mode

drawn inverted — a lone logoMark too (not when you pinned dark or uploaded a dark logo)

logoDarkMode

PDF of the space

off

settings.readerTools.pdf

Some of these keys belong to features shipping in the same wave; a default for a key your reader does not draw yet does nothing until it does.

look

{ "docs": { "look": "muted" } }

look

What it is

clean

Neutral white paper, a UI sans, hairline tables, a quiet sidebar, the documentation defaults below

muted

clean with the canvas on the soft grey surface tone and a tinted sidebar

bold

clean with a solid active sidebar row and the header painted in your brand (its text picked to read on it)

panel

clean with the article in an inset panel on the panel tone (the page's panel key)

dark

clean, pinned dark — your own colorScheme still wins

paper

The documentation defaults OFF: the warm paper and every reader default as they were before 2026-10

Every look other than clean is a small patch over clean, and every key you set yourself wins over any look, field by field.

The clean look's tokens

Emitted at zero specificity, so any palette slot you set always wins:

Token

Light

Dark

--bg

#ffffff

#0b0b0f

--surface

#fafafa

#111114

--panel

#f4f4f5

#08080b

--ink

#0f1115

#ededef

--muted

#5d636b

#a1a1aa

--ink-3

#676c75

#8b8b94

--line

rgba(15,17,21,.12)

rgba(237,237,239,.12)

--line-strong

rgba(15,17,21,.3)

rgba(237,237,239,.3)

--line-control

rgba(15,17,21,.5)

rgba(237,237,239,.42)

--grid

rgba(15,17,21,.04)

rgba(237,237,239,.04)

The tokens follow colorScheme (and colorSchemeStrict) exactly as the reader does, and a section painted light/dark takes that scheme's set.

Type in the clean look: the face is the platform UI sans, published as --docs-ui-font; running text at 16px on a 1.6 leading; H2 and H3 at 600; the page title 36px (30px on a phone), weight 600, tracking −0.02em. Your fonts, typography and mobile.density keys still win.

Inline code sits on a 6% ink wash with the hairline border rather than the brand's, reading --reader-code-inline-bg / -border first, so components.code still wins.

CSS variables for documentation

Variable

Written by

Default

--docs-sidebar-width

docs.sidebar.width

15rem; 272px under clean

--docs-content-width

the article's max width

the page width

--docs-nav-height

the header's rendered height

—

--docs-ui-font

the clean look

the platform UI sans

See also CSS variables worth targeting.

Icons

Anywhere an icon takes a NAME — a card, a button, a callout, a nav entry — it accepts our own glyphs (book, code, message, calendar, chart, puzzle, lock, shield, star, bell, search, sparkle, link, cog, clock, heart, image, trophy, webhook, check-circle, warning, eye, bookmark, hash, upload, pencil, quote, user, …) and the extended set below, drawn from Lucide (ISC). Our glyph wins where a name is in both. Anything else that looks like a word is refused (422), so a misspelt name never prints as text; an emoji or a short symbol is still drawn as text.

Extended names: activity, alarm-clock, alert-circle, alert-triangle, anchor, archive, arrow-down, arrow-left, arrow-right, arrow-right-left, arrow-up, arrow-up-right, at-sign, atom, award, badge-check, banknote, bar-chart, battery, binary, blocks, bluetooth, book-marked, book-open, bot, box, braces, brain, bug, calendar-check, calendar-days, camera, check, chevron-down, chevron-left, chevron-right, chevron-up, circle, circle-check, circle-dot, circle-x, clipboard-list, cloud, cloud-download, cloud-upload, code-2, coffee, compass, component, container, copy, corner-down-right, cpu, credit-card, database, dollar-sign, download, edit, external-link, eye-off, file, file-code, file-json, file-plus, file-text, files, filter, fingerprint, flag, flask-conical, folder, folder-open, function-square, gauge, gift, git-branch, git-commit, git-fork, git-merge, git-pull-request, github, gitlab, globe, graduation-cap, grid, grip-vertical, hard-drive, heading, headphones, help-circle, history, home, hourglass, inbox, info, key, key-round, languages, layers, layout, layout-grid, library, lightbulb, line-chart, link-2, list, list-checks, list-ordered, lock-keyhole, lock-open, log-in, log-out, mail, map, map-pin, maximize-2, megaphone, menu, message-circle, message-square, mic, milestone, minimize-2, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation, network, newspaper, notebook-pen, octagon-alert, package, palette, panel-left, paperclip, pause, pie-chart-2, play, plug, plus, power, printer, quote-2, receipt, redo-2, refresh-cw, regex, repeat, rocket, rotate-ccw, scan-face, scan-search, scissors, search-code, send, server, settings-2, share-2, shield-alert, shield-check, shopping-cart, sliders, smartphone, sparkles, square, square-terminal, sun, table, tablet, tag, target, terminal, test-tube, text-cursor, thumbs-down, thumbs-up, ticket-check, timer, trash, trending-up, type, undo-2, user-check, user-plus, users, variable, video, wallet, wand, wifi, workflow, wrench, x, zap, zoom-in, zoom-out.

The top rule (changed)

The 2px rule under the header used to be a two-part stripe: ink for the first 62% of the width, then your brand. Under a header painted near the ink colour the ink part vanished, and what was left looked like a progress bar stuck at 40%. So the rule is now one solid line in your brand colour wherever it is drawn: with topRule unset under look: "paper", and with topRule: true.

You want

Set

The solid brand line

{ "docs": { "topRule": true } } (or "solid")

The old split stripe

{ "docs": { "topRule": "split" } }

No rule

{ "docs": { "topRule": false } } — what every look except paper already does

The rule now also follows you onto the space root, /c/ hubs and tag pages (see One frame on every docs surface), so it no longer comes and goes as a reader moves from a page to its collection.

Looks are patches: what each one sets

A look writes a handful of ordinary keys under your own, and nothing else. Every key it writes is documented above and works under any look. clean is the documentation defaults (see The defaults); each other look is clean plus:

look

Keys it adds over clean

muted

palette.paper: "soft", docs.sidebar.background: "#f4f4f5", docs.sidebar.backgroundDark: "#111114"

bold

chrome.header.paint: "brand", docs.sidebar.active: { "style": "solid", "bar": false }

panel

palette.paper: "grey", docs.panel: { "radius": 16 }

dark

colorScheme: "dark"

paper

nothing: the documentation defaults are off, and every unset key renders as it did before 2026-10

Layering is theme → space → page: a site theme's docs, then the space's own theme, then a page's metadata.themePatch (or its per-page theme.docs, below). Within each, a key you set beats the look. Three combinations that are not any template:

The muted canvas with the bold header, keeping the clean sidebar row:

{ "docs": { "look": "muted" }, "chrome": { "header": { "paint": "brand" } } }

The panel look's inset article on a white page, the rail inside the panel, and the split stripe back:

{
  "docs": {
    "look": "clean",
    "panel": { "radius": 16, "includeRail": true, "shadow": "sm" },
    "topRule": "split"
  },
  "palette": { "paper": "white" }
}

dark with the bold sidebar row only, and the front door framed with the tree:

{
  "docs": {
    "look": "dark",
    "sidebar": { "active": { "style": "solid", "bar": false } },
    "frame": { "front": "tree" }
  }
}

One frame on every docs surface

The space root, every /c/<slug> collection hub and every /tags/<tag> page now wear the same frame as an article: the article alignment, the inset panel (and panel.phone), the top rule, frame.tree: "hide", and the panel look. Before, they were drawn flat, so a "Panel" look gave inset articles beside a flat hub. The search box, Ask AI, the version, language and variant switchers (variantSwitcher.show: "always") and the rail outlets are there too.

Key

Values

Unset

What it does

frame.tree

show, hide

show on a documentation space

The page tree beside the content, on articles, hubs, tag pages and a framed front door

frame.rail

show, hide, auto

the page as it was

The on-this-page rail's default; auto draws it only for a page with four or more H2s. onThisPage (space or page) still wins

frame.front

tree, bare

what the stored front page says: framed when its sidebar is the docs tree (sidebar.source: "docs-nav"), else bare

The space's front door (its landing or composed front page). tree puts it inside the docs frame with the page tree beside it, whatever the front page says; bare is full width with no tree

frame.front exists because a space on the generated landing (never opened in the front-page builder) had no way to show its tree on its own home. Unset keeps every existing front door exactly as it is, so set { "docs": { "frame": { "front": "tree" } } } to frame yours.

Reader addresses a documentation space has

Address

What it is

/<mount>

The front door: the home page (homeAt: "root"), a landing or a composed front page, framed per frame.front

/<mount>/<page>

A page. /<mount>/<version>/<page> is the same page in another version

/<mount>/c/<slug>

A collection's hub — theme.docs.hub. In the sitemap per language

/<mount>/tags/<tag>

Every page carrying metadata.tags <tag>, as hub cards with a count, in the docs frame. It reads theme.docs.hub too. A tag no page carries is a real 404 with no canonical

/<mount>/<page>.md

The page as Markdown, in the address's language

/<mount>/print

The whole space as one printable document (settings.readerTools.pdf), with its own title, noindex, in the address's language

/<mount>/updates.xml

RSS of recently changed pages, linked by slug. /<mount>/<lang>/updates.xml is the same feed in that language

/<mount>/<lang>/…

Every address above in a published language. The root and hubs name themselves canonical per language, with hreflang alternates

Public JSON for the same: GET …/tags/{tag}, GET …/pages/{pageId}.md and GET …/updates.xml.

Newer keys

Every key below is optional and unset renders what the reader drew before it existed. All are theme.docs.* unless the scope says otherwise.

Header, search and Ask AI

Key

Values

Unset

What it does

ask.suggestions

up to 6 questions, 120 characters each

none

One-click starter questions in the empty Ask AI panel (and in the Ask AI launcher widget)

ask.handoff

{ href, label?, when?: "unhelpful" | "always" }

none

"Ask a person instead" under an answer, opening href (a path on your site or an https link) with {question} filled in. unhelpful: after a "No"; always: every answer

theme.docs.search.placeholder

≤ 80 characters

chrome.searchStyle.label, else "Search" translated

The docs search's words, in the rail and in the header on this space

theme.docs.search.facets

boolean

off

A row of section chips with counts in the search dialog that narrows the hits

theme.docs.search.algolia

{ appId, searchKey, indexName }

our own search

Search with your Algolia DocSearch index instead (see What you need to set up)

header.hideOnScroll

boolean

the header stays

The header slides away while the reader scrolls down and returns on the first scroll up

header.github

{ repo: "owner/name", stars?, label?, placement?: "sidebar" | "header" }

none

A repository button; stars adds the live star count, fetched by our server and cached for an hour (a reader's browser never calls GitHub). sidebar (default) or header

offline.enabled

boolean

off

Readers keep the pages they opened for offline reading. Never a signed-in, shared-link or network-limited page; turning it off retires installed copies

Page head, actions and navigation

Key

Values

Unset

What it does

pageHead.readingTime

byline, actions, hide

none

"6 min read", counted from the page body

pageHead.owner

byline, hide

none

"Maintained by …" from the page's metadata.owner

pageHead.updatedFormat

(existing)

—

Now honoured in the actions row too (updated: "actions")

pageActions.custom

up to 4 { label, urlTemplate, icon? }

none

Your own actions after the built-in ones. {url}, {path}, {title} and {markdownUrl} are URL-encoded into an absolute http(s) template

availability

{ show?: "both" | "nav" | "title" | "none", notice?, labels? }

both, notice on

How a page's metadata.availability stage reads: a pill in the tree and beside the title, your own word per stage, and a notice above a deprecated page's body

sidebar.collapseDepth

0–6

the branch on the current path opens

Tree levels at or above this start open (collections are level 1); deeper ones start collapsed. groupsOpen still decides the groups

related

{ show, source?: "auto" | "manual", count?: 1–12, placement?: "foot" | "rail", style?: "list" | "cards", label? }

not shown

"Related pages". auto ranks pages sharing this page's tags, then its siblings; manual lists only metadata.related. Default 4, at the foot, as a list

versionSwitcher.banner

{ show?, wording?, style?: "note" | "strip" }

only a version's own banner (unreleased, unmaintained)

show: true adds "You are reading the docs for {version}. The latest is {latest}." with a link, on every older version; false shows none

versionSwitcher.badge

boolean

none

A chip with the version's name beside the title

versionSwitcher.remember

boolean

no memory

A version the reader picked is where the root and version-less addresses open next time

editLink.urlTemplate {locale}

a new placeholder

—

The reader's language (empty on the original), for repositories that keep one file per language

blocks.code.wrapToggle

boolean

none

A "wrap long lines" button on every code block

API reference

Key

Values

Unset

What it does

api.examples.params

all, required

all

required keeps only required fields and query parameters in the generated samples, for a call a reader can paste and run

api.examples.auth

{ placeholder, personalize? }

no credential

What the samples show for the key or token (Bearer YOUR_API_KEY); personalize swaps in the token the reader saved in "Try it" (playground.rememberAuth: "session"), in their browser only

api.collections

{ kinds?: ["postman", "insomnia", "bruno"], runInPostman? }

none

Download links for the space's own collections beside each operation, and a "Run in Postman" button for your published collection link (a postman.com address)

api.asyncapi

{ verbs?: "send-receive" | "publish-subscribe", messageExample?: "show" | "hide", protocol? }

send-receive

How AsyncAPI channel pages name their operations, whether the example message shows, and a protocol chip (WS, MQTT, AMQP…)

GraphQL schemas import into the same reference: upload a .graphql SDL (or an introspection result), optionally with the endpoint readers' "Try it" calls, and each operation becomes a page with GraphQL, cURL, TypeScript and Python samples. Two blocks go with the reference: Rate limit (rateLimit) and Error codes (errorCodes, each row anchored at #error-<code>); see the authoring reference.

Page metadata

Set with PATCH …/pages/{pageId}/metadata; null clears.

Key

Values

What it does

availability

alpha, beta, preview, ga, deprecated, experimental, new

The release stage of what the page documents, drawn per theme.docs.availability

owner

a member's id

Who keeps the page true: named in the byline (pageHead.owner), listed on the stale-page report, and reminded to review it (see settings.docsReview below)

related

up to 12 slugs or ids

This page's own related pages; they lead the related list

navClass

one class name

A CSS class on the page's tree row, for customCss

navData

up to 8 { key: value }

Written as data-nav-<key> on the tree row

nofollow

boolean

robots: nofollow on this page, independently of noindex

Labels in every language (translations)

Every label you type into theme.docs (the contents heading, the feedback question and buttons, Ask AI's name and starter questions, switcher captions, the search placeholder, the version banner's wording…) used to have one value for every language. theme.docs.translations gives each language its own:

{
  "docs": {
    "toc": { "label": "In this guide" },
    "ask": { "label": "Ask the docs", "suggestions": ["How do I install it?"] },
    "translations": {
      "es": {
        "toc": { "label": "En esta guía" },
        "ask": { "label": "Pregunta a la documentación", "suggestions": ["¿Cómo lo instalo?"] },
        "labels": { "Getting started": "Primeros pasos" }
      }
    }
  }
}

Fixed labels are named by their key path. List items (section tabs, rail anchors and footer links, variant options, custom page actions) are matched by their original words in labels, so reordering a list never scrambles the translation. A missing language or key reads in the original, and a label you turned off (false) stays off. The .md twin, print view and updates.xml use the same words for the address's language. See Translated labels.

Per-item overrides

A single documentation page can carry its own theme in two ways:

  • metadata.themePatch — the page keys listed under Per-page theme, scoped to that page.

  • the page's own customization (PATCH …/pages/{pageId}/metadata with theme, alongside customCss, shareImageUrl, jsonLd…). On a documentation space it accepts the token keys every piece takes (colours, fonts, components, typography, layout, contentWidth, motion) and the docs namespace. Any other type's namespace (post, forum, …) is refused with a 422 naming it, because nothing on a docs page would draw it.

The page's contentWidth and layout.widths.text reach the article column. See Theme your site → Per-page customization.

Outlets

Theme components can fill these slots on a documentation space (Theme components):

Outlet

Where

Context

docs-page-header, docs-page-footer

Above and after the article

page.title

docs-sidebar-start, docs-sidebar-end

Top of the left rail under the brand; foot of the rail

—

docs-rail, docs-rail-end

Top of the right rail (above "On this page"); foot of the right rail

page.title

docs-hub-above-list, docs-hub-below-list

A /c/<slug> hub or a tag page, above and below its list

collection.title, collection.slug

The page and sidebar outlets now also render on the space root, hubs and tag pages, so a sponsor wall in docs-sidebar-end is on every docs surface.

In the studio

  • Look → Docs reader: every setting holds every theme.docs key, split into folders: Navigation; Header, search and Ask AI; Page head; Contents rail; Page foot; API reference; Blocks; Collection pages; Versions and languages; Frame and panel (the look, frame, panel, widths and topRule); Labels per language; and More. Double-clicking a part of the page opens its keys.

  • Look → Page tools switches settings.readerTools (below), and beside it Ask page owners to review after (days) sets settings.docsReview.

  • A collection's settings hold its rail controls: starts collapsed, its own scope and the back link's style (nav.collapsed, navScope, backStyle).

  • A page's settings card holds Page owner.

  • Settings holds the SEO panel (with Generated share card and Extra meta tags), Network access, and Embed, with a choice of widget (search box, Ask AI box, or the Ask AI corner button).

  • Housekeeping → Change requests: each request has a Preview link, a stable address of the space with that request's edits that works until it is merged or closed (POST …/preview/change-requests/{crId}, Preview links).

Behaviour settings

Setting

Default on a documentation space

What it does

settings.readerTools.search

on

The search box in the rail

settings.readerTools.pdf

off

The PDF / print link and /print

settings.readerTools.copyMarkdown

off

A separate "Copy page" tool (the actions bar already copies)

settings.readerTools.aiActions

on

"Open in ChatGPT / Claude"

settings.readerTools.mcp

on

"Connect with MCP"

settings.docsReview.afterDays

off

7–730. A page with an owner that goes unedited this long sends its owner one review reminder per interval (checked daily)

settings.networkAccess

everyone

Limit the space to some networks — see Limit a space to some networks

seo.metatags

none

Up to 20 { name, content } <meta> tags on every page (PUT …/seo). Names the reader writes itself (description, robots, og:*, twitter:*) are refused

The settings.* keys are written with PATCH /orgs/{orgId}/spaces/{spaceId} and { "settings": { … } }, validated strictly:

curl -X PATCH "https://forumulate.com/api/orgs/$ORG/spaces/$SPACE" \
  -H "Authorization: Bearer $FORUMULATE_TOKEN" -H "Content-Type: application/json" \
  -d '{ "settings": { "readerTools": { "pdf": true }, "docsReview": { "afterDays": 90 } } }'

Embeds, previews and analytics

  • Ask AI launcher. /api/embed/ask.js puts a corner Ask AI button in your own product, with your ask.suggestions, answers with their sources, and your ask.handoff. It takes data-org, data-space, data-position (bottom-right or bottom-left), data-label, data-accent, data-theme and data-locale; window.Forumulate.ask has open(), close() and ask(question). See Embeds.

  • Local preview. forumulate dev <folder> --space <spaceId> serves a folder of Markdown on your machine, reloading as you save, rendered by our own renderer (POST …/docs/render, which saves nothing). --site <your docs address> borrows your published look.

  • Analytics and chat vendors. Besides the existing ones, the analytics settings take Mixpanel, Amplitude, Heap, Hotjar, Microsoft Clarity, LogRocket, Koala, Pirsch, FullStory, Clearbit and Front Chat, each behind the consent banner like the rest. See Analytics.

  • Importers. Mintlify, ReadMe and Docusaurus projects import with their navigation (alongside GitBook, MkDocs, Notion, Confluence and the help-desk importers); see Importers.

  • Live code. Runnable HTML/CSS/JS and React demos are their own guide: Live code playgrounds.

What you need to set up

These features stay off until you give them your own account's details. Until then readers see exactly what they saw before.

Feature

What you need

Where you enter it

Until then

Algolia DocSearch

Your Algolia Application ID, the search-only API key (never an admin key), and the index name

theme.docs.search.algolia, in Look → Docs reader: every setting → Header, search and Ask AI

Our own search

Run in Postman

A published Postman collection link from your Postman workspace

theme.docs.api.collections.runInPostman

No button (the Postman/Insomnia/Bruno downloads need nothing)

Analytics and chat vendors

Each vendor's public site or project ID

Your space's or organization's analytics settings

No script is loaded

Live code playgrounds

Nothing from you: a sandbox domain on Forumulate's side

—

The block's fallback picture

The Algolia key is public by design (Algolia issues it for browsers), and its host is added to that space's allowed connections only.

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Operation

Request

What it does

Shortcuts

Public_search

GET /public/orgs/{orgId}/spaces/{spaceId}/search

Get the search in a space (public reader)

client.public.search()<br>MCP search_docs

The Forumulate teamUpdated