Add the rail, pager, markdown pages and plates to the docs template

What does this MR do and why?

I want the docs template to ship an "On this page" rail, a pager, markdown pages and an SVG figure component, because sites built on it keep writing their own. It had no pager, no "On this page" list, no markup for the disclosures its chrome already styles, and no markdown pages: two exhibit sites added their own rail, and a guide that keeps its prose in another project renders it with its own script. The template now ships all of them, so a new site starts with them and an existing one can lift the files.

  • Every page gets the rail with an "On this page" group, the page-wide shell and a Pager in nav.ts order. The chrome styles the pager and the rail group with design/patterns/pagination.css (nav.page-pager) and design/chrome/docs/rail.css, by class only.
  • Markdown pages are an Astro content collection. gitlabRepository() reads another project at a tag or full commit SHA with the same patterns and entry ids as glob(), so nested patterns render at nested paths through guides/[...slug].astro.
  • Local markdown is trusted like the site's code. Markdown read from another project has its HTML passed through a DOMPurify allowlist that keeps what the chrome styles, and the build log names what it removed.
  • Plate inlines an SVG, local or fetched, so its token colors follow the theme. The build stops on a plate that carries script, a <style> element or a reference outside the file. Disclosure renders details.disclosure with a unique id, shown on a new Build failures page.
  • On a phone, Plate lets the frame scroll a wide figure instead of shrinking it: it gives the SVG its viewBox width as --diagram-width, which the docs chrome holds below 900px (up to 960px), and gives the figure tabindex="0" and the caption as its name so a keyboard reaches the scroll region. A viewBox with no positive width stops the build.
  • A relative .svg image alone in a markdown paragraph, ![caption](plate.svg), renders as a plate through the same plateFigure(), local or from another project at the same pin, so guide prose still shows the image on GitLab and follows the theme on the site.
  • anchors.js scrolls at once for a reader who prefers reduced motion, and a long code line or a fragment-opened disclosure stays inside the viewport.
  • A page can stay plain GitLab markdown: with no frontmatter title, its first level-one heading is the title and leaves the body, and a relative link to another page's .md file becomes that page's route under the site's base, fragment slugged. From another project, any other relative link opens the file on GitLab at the pinned commit, where GitLab resolves the same link; locally, a .md link outside the collection stops the build by name, as does a page with no title. Remote links are rewritten before the sanitizer runs, so it still decides what ships.
  • On a phone, where the rail stacks above the page, it folds into a closed Disclosure summarized "On this page", so the home page no longer lists every page before its content; wider, the rail is unchanged.

The content path

flowchart LR
  local["src/content/guides/**/*.md<br/>glob()"] --> coll["content.config.ts<br/>collection"]
  remote["group/project at a tag or full SHA<br/>gitlabRepository()"] --> clean["lib/sanitize.ts<br/>DOMPurify allowlist, removals logged"]
  clean --> coll
  coll --> route["pages/guides/[...slug].astro<br/>render(entry)"]
  svg["src/plates/*.svg or a fetched entry"] --> check{"lib/plates.ts<br/>anything to remove?"}
  check -- yes --> stop["build fails, naming it"]
  check -- no --> plate["Plate.astro<br/>figure.diagram-frame"]
  plate --> route
  route --> layout["Layout.astro<br/>rail and pager"]

The template still links the chrome at design/v2.4.1/, which has no pager or rail styles and the earlier anchors.js, until the release that carries this change repins it with just sync. The template's site gains dompurify and jsdom, and lists picomatch and github-slugger, which Astro already installs.

References

templates/docs/AGENTS.md, "Docs page authoring", is the authoring guide, and the template's Build failures page lists the messages the content path stops a build with.

Standards affected

None. No check.sh, tier or status changes.

Exemption implications

None. A consumer changes nothing until it lifts the new files, and the chrome additions apply only by class.

Test plan

  • just ship passes (21 standard(s) passed, 0 exempt, 0 failed), and a copy of the template with its placeholders replaced passes astro check and builds.
  • A plate fails the build, naming what it carries, for an event handler after a / or after a > inside a value, an iframe in a <foreignObject>, markup after the root </svg>, a <set> writing a javascript: link, entity-encoded and newline-split javascript: schemes, a <style> with @import, an external <image>, a <script>, a data: link, an outside url(), a CSS escape and a DOCTYPE internal subset. A plate with style="fill: var(--accent, #fc6d26)" and a root aria-labelledby builds, keeps the style and takes the Plate's label.
  • Through a local HTTPS server answering the loader's API calls, <img onerror>, a page-wide <style>, an iframe and a javascript: link stay out of the built page while a details.disclosure with its id and a callout stay in. Nested files, a frontmatter slug, a negated pattern, an upper-case full SHA and a retried 429 all build. A redirected token request, an http:// host, invalid UTF-8, two files with one id and a .. slug each stop with the documented message.
  • gitlabRepository() built 21 pages from standards/**/SKILL.md and inlined four SVGs from this project at 78645fa8bd3eb6d5ed8df0ebb41d2a2ea30985c0; 89 links in 18 of those pages that point outside the collection, such as ./templates/AGENTS.md, now open the file at that commit on gitlab.com.
  • Locally and through the HTTPS mock, under a /my-docs base: a page with no frontmatter takes its first heading as the title and drops it from the body, a frontmatter title keeps its heading, a schema-derived title wins, and a page with neither stops the build. .md links with and without fragments, through ../, and to a page with a frontmatter slug become routes; absolute, mailto: and fragment links stay as written; a remote javascript: link, plain or ending in .md, is still stripped; remotely, links outside the collection, relative, root-relative or to a directory, become the file's view at the pinned commit with the fragment as written, and one that climbs above the repository root stops the build; locally, a .md link outside the collection stops the build, naming the page and the link.
  • Rendered home and the markdown guide at 390 and 1280, light and dark: at 390 the rail is a closed "On this page" disclosure and the content starts 248px down; at 1280 the rail box matches the previous commit's on both pages.
  • Rendered at 1440 and 390 wide, light and dark, with the chrome served from this branch: the rail, the pager, a fragment link opening its row below the header (at once under reduced motion), and unique ids for repeated and non-ASCII headings and summaries, with the headings in the rail.

An AI tool wrote this change and ran the checks above.

Merge request reports

Loading
Loading