Authoring reports
Reports live in apps/site/src/content/reports/<slug>/v<version>.mdx. Each file is one version of one report; the newest version is served at /reports/<slug>/ and every version stays at /reports/<slug>/v<version>/.
Frontmatter
Section titled “Frontmatter”title: "Atmospheric CO₂ at Mauna Loa"subtitle: "A reference analysis …"slug: atmospheric-co2-2026 # lower-case, hyphens; never changestype: analysis # report | analysis | brief (reports get a PDF cover page, a contents page and back matter on its own page)palette: goap # optional: default | landscape | goap — colour palette for this report (see /design/palette/)status: published # draft | review | published | superseded | withdrawn (drafts build only in dev)# withdrawn: { date: 2026-09-01, reason: "…" } # required with status: withdrawn — the tombstone says when and whyversion: "1.0" # semver-ish; bump for a new filedate: 2026-08-17updated: 2026-08-20 # optionalauthors: - name: Jane Doe orcid: 0000-0002-1825-0097 affiliation: Centre for Sustainable Development Reform role: Lead author - name: CSDR Platform Team type: organizationabstract: >- # used for <meta description>, JSON-LD and the reports list …keywords: [carbon dioxide, Mauna Loa]licence: CC BY 4.0doi: 10.5281/zenodo.0000000 # optional; when mintedfunder: … # optionalseries: Scaffold reference reports # optionaldata_bundles: [noaa-mlo-co2] # bundle ids used by the reportoutputs: { pdf: true, html: true, deck: true } # PDF, self-contained HTML and the presentation deck, written next to the page by pnpm builddeck: { mode: full } # full = the whole report as slides · headline = the talk version (no body prose)changelog: - { version: "1.0", date: 2026-08-17, note: First release }The schema is reportSchema in src/content.config.ts; the build fails on invalid frontmatter.
Public or internal is decided by the folder, not by frontmatter (D11): src/content/reports/<slug>/ is on the public site; src/content/internal/<slug>/ is only on the sign-in internal site (pnpm dev:internal to preview). Setting visibility: internal on a file under reports/ fails pnpm check — move the directory instead. See Deploy → Audiences.
The vocabulary
Section titled “The vocabulary”Plain Markdown does most of the work — Markdown tables are auto-wrapped in a scroll container,  becomes a captioned figure. Beyond that, import from @csdr/editorial:
import { Figure, Table, Callout, Ref, KeyMessages, StatRow, StatTile, DataDownload, Definition } from '@csdr/editorial';| Component | Use |
|---|---|
<KeyMessages> | Bulleted opening block; one number per bullet if you can |
<StatRow><StatTile …/></StatRow> | 2–4 headline numbers with unit, delta, source |
<Figure id caption source width webNote printNote> | Numbered figure; put a chart, image or map inside. webNote (“Hover for values…”) is dropped from the PDF/print; printNote (“An interactive version is online.”) appears only there |
<Table id caption units source printMode keep> | Numbered table; put a Markdown table inside (blank lines around it). In the PDF every table is measured and printed portrait (few columns), landscape (a genuine multi-column comparison, on its own landscape page with the title repeated on every page it occupies) or as records (paragraph-length rows become labelled records; the web keeps the table). printMode="portrait|landscape|records|appendix" overrides the measurement where the comparison needs it (appendix = compact reference material at 8.5 pt); keep stops a table splitting across pages. The build log and dist/.csdr-print-preflight.json say what each table got |
<Callout kind title> | note · key-finding · method · warning · caution |
<Ref to="fig-x" /> or {ref:fig-x} | “Figure N” / “Table N”, resolved at build |
[@key] | Citation from src/content/bib/references.bib; end the report with ## References and [^ref] |
[^1] | GFM footnote |
<DataDownload bundle="id" /> | Files, licence, provenance, citation of a bundle |
<Definition term="SEEA">…</Definition> | Inline definition |
See the live catalogue at /design/ — every component with a render and copyable code.
Numbers that come from data
Section titled “Numbers that come from data”Do not type numbers that the data can change. Put a small .astro “analysis cell” next to the report (see src/components/analyses/co2/) that loads the bundle with the data loaders, computes derived values once, and exposes charts and inline values (<Co2Value of="latestAnnual" />). The reference report is the worked example.
Versions
Section titled “Versions”To publish a revision, copy v1.0.mdx to v1.1.mdx, bump version, add a changelog entry, and set the old file’s status: superseded. Both build; the old one shows a banner and canonicalises to the new one.
To withdraw a report (an error, an embargo, a legal request): set status: withdrawn and withdrawn: { date, reason } on every version that must go, add a changelog entry, /publish. The page becomes a tombstone — title block, “withdrawn on … because …”, the citation record — with no body, no PDF/HTML/deck (existing download links stop working), noindex, a Withdrawn badge in the reports list; it stays at its address so citations resolve instead of 404-ing. Deleting the file instead leaves dead links everywhere the report was cited. Taking a whole site down is a different lever: Deploy → Taking a site down.
Checklist before /publish
Section titled “Checklist before /publish”pnpm checkandpnpm testpass- Every figure has
caption,sourceand (for charts) analtsentence - Key numbers come from an analysis cell, not the keyboard
- References render under
## References - Preview at
/reports/<slug>/and print preview look right
Print and PDF
Section titled “Print and PDF”Every build writes the PDF and the self-contained HTML export next to the page (see Deploy → Exports). What authors control:
- The PDF is a print-native document, not the web page on paper: a 10 pt body on a 154 mm measure (20 mm left / 36 mm right), numbered sections (
1,2.1— the screen stays unnumbered), figures at text width, no decorative rules. The same layer drives the HTML export, so the two files are one document in two containers. type: reportgets a title page (title block, authors, version/licence, the abstract and the key messages, which move up from the body for print), a contents page with page numbers (filled from a first render, then printed again) plus an imprint (recommended citation, versions, changelog, licence), and running headers from page 2;analysisandbriefflow from page 1 — title block, key messages, headline numbers, first section — with the same running header/footer.status: draft | review | supersededprints a watermark on every page and names the status in the running header. Drafts are not built in production at all;withdrawnreports have no exports.outputs: { pdf: false },{ html: false }or{ deck: false }switches an export off for that report.<Figure webNote="…" printNote="…">keeps screen-only sentences (“hover for values”) out of the PDF and adds a print-only one;<Table printMode keep>overrides how a wide or long table prints — by default the build measures it and chooses (see the table above). Links inside table cells stay clickable in the PDF but never spell out their URL; a long narrative table belongs in<Slide mode="skip">so the deck does not try to fit it on one slide.- Figures, callouts, key messages, stat rows and the data card never split across pages; tables may, with the header row repeated. Charts print at text width and at most 80 mm tall (90 mm for
width="wide") — the default chart shape (720 × 320) is chosen for that. Open a section with a sentence, not a figure: a heading is kept with whatever follows it, so a heading + full-width figure that does not fit moves to the next page together and leaves a gap. If something still paginates badly, the fix belongs inpackages/theme/src/print.css, not in the report.
Light and dark
Section titled “Light and dark”A report follows the reader’s theme, like the rest of the site — the control sits in the masthead (auto → light → dark). Charts are rendered twice at build time, once in the palette’s light set and once in its dark set, and the page shows whichever matches; nothing is authored twice. What a reader downloads is always light: the PDF, the HTML export and the presentation are the printed artefact, and printing a page from a dark browser prints the light one.
On a phone
Section titled “On a phone”A report page is one column below 62 rem: the contents becomes a collapsed Contents disclosure (tap to open — it is a plain <details>, so it works with JavaScript off, where it simply starts open), figures and tables scroll inside their own box rather than the page, and the deck’s document mode stacks the slides. Nothing about a report is authored twice for small screens; if something reads badly there, the fix belongs in packages/theme or apps/site/src/styles/report.css.
Presenting a report
Section titled “Presenting a report”Every build also writes <slug>-v<version>-slides.html — the Present button on the report page: the report as a full-screen deck that scales to any screen (projector, laptop, shared window; a phone in portrait shows the slides stacked), works from a download or a USB stick, prints one landscape page per slide, and keeps the slide number in the address so a link opens on the right slide. Keys: → ← ␣ · O overview · F fullscreen · ? help.
The deck is derived from the report by rules, so it can never disagree with it (same numbers, same charts, same palette): a title slide; the key messages (numbered, up to five a slide); the stat row as a numbers slide; a divider per ## section; a slide per ### heading with its prose (about 110 words a slide — longer runs continue on “continued” slides, never splitting a paragraph; short slides set larger); one slide per figure (the caption is the headline, the chart fills the slide, the source sits under it), per table (twelve rows, the rest noted) and per callout; the references; a closing slide with the citation, address and licence. Footnotes, the data section and web-only text stay out. Nothing is trusted to fit: the build opens the deck in Chromium and re-plans any slide that overflows; pnpm test fails if one still does.
Making it a talk, in the repo (never by editing the generated file):
deck: { mode: headline }— title, key messages, numbers, section dividers, figures, tables, callouts and closing only; no body prose. The one-line “make it succinct”.<Slide mode="skip">…</Slide>around anything that should not become a slide (a long methods paragraph, a table of appendices).<Slide mode="only">…</Slide>— content that exists only in the deck: three bullets that say what a section’s two paragraphs say, a takeaway line. It replaces that section’s prose on its slide and never shows on the page, in the PDF or in the HTML export. Numbers inside it still come from analysis cells.outputs: { deck: false }when a report should not be presented at all (a data note, a superseded version).
If a slide looks wrong for every report — spacing, type, colour — the fix belongs in packages/theme/src/deck.css (the design layer) or scripts/export-deck.mjs (the rules), not in a report. Decision: D14; plan: docs/plans/exports-presentation.md.