⚠️ Design system CSS not found. Check the path in cms/docs.config.js → designSystemPath, then re-run npm run docgen.

Page Types

What a page is, and the chrome that follows from it

Docs / Page Types
Download .md file
Open .md in new tab
On this page
  • The six types
  • Overriding a preset
  • Where a page's source lives
  • The close contract
  • Per-tool CSS and JS stay global
  • Adding a type

Every page this site serves is one of six types. The type says what the page is; the chrome follows from that. Declare it with type: in frontmatter, or leave it out and get doc, which is what nearly every page is.

The types exist because the site had none. An audit in August 2026 found 125 served pages arriving in twelve different shapes, and the shape a page ended up with was an accident of which generator function emitted it and what its author remembered to copy. Two tool pages had quietly lost their footer and feedback block that way; none of the eight had a close.


The six types

type level header page bar TOC pager feedback + footer content frame
shelf 0 yes — — — yes grid
contents 1 yes full — — yes grid
doc 2 yes full yes yes yes grid
page 2 yes full — yes yes wide
tool 2 — — — — opt-in free
bare — — — — — — free, no sidebar

This table is generated from nothing — it is transcribed from PAGE_TYPES in
generate-docs.js, so check it
against that object when you change a preset. It has drifted once already:
contents was recorded as having no page bar for a day after section indexes
gained one.

Two things it decides that are easy to get wrong:

  • The contents rail and the reading measure are one decision, not two. A page
    keeps the 1080px measure because prose needs it, and the rail sits in the space
    that leaves. Releasing the measure removes the rail with it — which is the
    whole difference between doc and page.
  • tool supplies nothing it is not asked for. The framed tools opt into
    the template's header from their own frontmatter (header: "true"); the two
    full-bleed tools have none. Every tool carries exactly one bar — its own,
    in its body, ending in the close. What looks like shared chrome is a
    per-page opt-in, not the type.

doc is the default and the one you almost always want. A markdown body, the 1080px reading measure, an on-this-page rail, prev/next through the section, and a page bar with a breadcrumb and a close.

page is doc for a body that is not prose. Same chrome, but the body is raw HTML and the frame is wide instead of measured. Reach for it when the content is a comparison table, a gallery, or a diagram that the reading measure would crop for no benefit.

tool is the loosest type, deliberately. A tool can look like anything, so this type imposes almost nothing by default: the site shell, and a way out. Header and chrome are opt-ins (header: "true", chrome: true). The tool's bar is its own body's markup — one bar per page, ending in the close — and that close carries data-page-close, which the build enforces. bar: "true" exists for a tool that wants the generator's breadcrumb bar instead of its own; none does today.

bare sits outside the Barba container with no sidebar. support.html, access-denied.html and the auth pages.

shelf and contents are assigned by the generator for the pages it builds itself — the home page and the section indexes. They are in the table because the renderer keys on them, not because anyone authors them.

Levels

The level column is data-level on the Barba container, which is what motion resolves a transition from. L1 → L2 in the same section is an open, L2 → L1 is a close, L2 → L2 is a swap. A type with the wrong level animates wrongly, which is the main reason level belongs to the type rather than to the page.


Overriding a preset

A type is a default, not a cage. Five flags override it for one page:

flag effect
toc: false drop the on-this-page rail
bar: "true" / bar: "false" add or drop the generated page bar (breadcrumb + close). A tool that takes it also gets its close from it
header: "true" / header: "false" add or drop the page header — how a tool gets one without hand-drawing it
pagination: false drop prev/next
chrome: true / chrome: false force the feedback block and footer on or off

A tool page taking the generated bar may also set bar-width: "full" so it spans the viewport instead of aligning to the tool column. The flags are the escape hatch; if you find yourself setting the same combination on several pages, that is a new type asking to exist, not a flag to copy around.


Where a page's source lives

You are adding Write Type
A documentation page cms/<name>.md doc (omit type:)
A page with a hand-built body cms/<name>.md with type: "page" page
A tool cms/apps/<slug>.md + cms/apps/<slug>.html tool
A page outside the docs shell hand-authored from templates/page-template.html bare

tool and bare are rejected in a cms/*.md file, and the build says so. A tool has to be a source pair — its body must skip the markdown pipeline and it has a close to enforce — and a bare page has no sidebar and no Barba container, which the template cannot emit at all. Without the check both would render as a half-formed doc and no error.

Section indexes and the home page are generated. There is nothing to author.

Tools are a source pair

A tool is two files with the same basename:

  • cms/apps/<slug>.md — frontmatter only, no body.
  • cms/apps/<slug>.html — the body that goes between <main> and </main>. Nothing else: no <head>, no shell, no scripts, no footer.

Two files rather than one because a tool body must not go through the markdown pipeline. markdownToHtml rewrites markup unconditionally — <table> becomes .table-scroll > table.table, <pre><code> gains a copy button, inline <code> gets chipified. That is right for prose and wrong for an application. The CPM Calculator has a <table> that would be silently rewritten.

{{icon:name}} works in the body. Use it instead of pasting raw SVG.

Output goes to <section-folder>/<slug>.html — the tool pages keep the URLs they have always had.

The container gets data-tool="<slug>" automatically, so a tool's module has a stable root to scope its queries to:

var root = document.querySelector('[data-tool="email-signature"]');
if (!root) return;

The close contract

A tool page must give the reader a way out. The build fails without one. A tool the reader cannot leave is a dead end, and that is exactly the state all eight tools were in before this existed.

The close is the terminal cell of the tool's own bar — after the actions, after the overflow trigger:

<div class="bar-actions">
  <div class="dropdown bar-overflow" hidden>…</div>
  <a class="bar-close" data-page-close aria-label="Back to Tools">{{icon:close-large}}</a>
</div>

An <a> with no href gets the section index filled in, so the common case needs no path. Supply your own href when the tool should close somewhere else — it is left alone.

It has to be a link, not a button. Nothing reads data-page-close at runtime; it is a build-time marker, and the navigation comes from the href. A <button> carrying the attribute is inert, so the build rejects it. The same check catches a close that is commented out, or one whose only href is a data-href.

Name it for where it goes — aria-label="Back to Tools", not "Close SVG Cleaner". The × is the visual metaphor; the accessible name has to describe the navigation that actually happens (WCAG 2.4.4).

Doc pages' carries the same attribute, so "how does this page close" has one answer across every type rather than two mechanisms that drift.

No data-tooltip on the close: the tooltip bubble is unclamped and a control flush to the end of the bar pushes it outside the page. See bar. The aria-label is the accessible name and it is there. The same applies to the overflow trigger beside it — since both render as full-height cells at the trailing edge, both rely on aria-label alone.


Per-tool CSS and JS stay global

Every tool's stylesheet and module is loaded on every page, from extraStylesheets and extraScripts in cms/docs.config.js. That looks wasteful and is deliberate: a tool reached through a Barba transition never runs the incoming page's <head>, so its CSS and JS have to be present already. Moving them to per-page frontmatter would leave any tool arrived at by navigation unstyled and dead.


Adding a type

Don't, unless a shape genuinely recurs. Rule 7 of layer discipline (CLAUDE.md §17) applies here too — three real pages wanting the same combination before it becomes a type, and until then the override flags cover it.

If it earns its place: add a row to PAGE_TYPES in generate-docs.js, add it to the table above, and nothing else. Every emitter reads its chrome from that table, so there is no second place to update — which was the whole point of building it.

On this page
  • The six types
  • Overriding a preset
  • Where a page's source lives
  • The close contract
  • Per-tool CSS and JS stay global
  • Adding a type
Previous CSS
Next Page Layouts

Was this page helpful?

We use this feedback to improve our documentation.

Thanks for your feedback

Send feedback

© 2026 By Default