Skip to content

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>/.

title: "Atmospheric CO₂ at Mauna Loa"
subtitle: "A reference analysis …"
slug: atmospheric-co2-2026 # lower-case, hyphens; never changes
type: 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 why
version: "1.0" # semver-ish; bump for a new file
date: 2026-08-17
updated: 2026-08-20 # optional
authors:
- name: Jane Doe
orcid: 0000-0002-1825-0097
affiliation: Centre for Sustainable Development Reform
role: Lead author
- name: CSDR Platform Team
type: organization
abstract: >- # used for <meta description>, JSON-LD and the reports list
…
keywords: [carbon dioxide, Mauna Loa]
licence: CC BY 4.0
doi: 10.5281/zenodo.0000000 # optional; when minted
funder: … # optional
series: Scaffold reference reports # optional
data_bundles: [noaa-mlo-co2] # bundle ids used by the report
outputs: { pdf: true, html: true, deck: true } # PDF, self-contained HTML and the presentation deck, written next to the page by pnpm build
deck: { 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.

Plain Markdown does most of the work — Markdown tables are auto-wrapped in a scroll container, ![alt](src "caption") becomes a captioned figure. Beyond that, import from @csdr/editorial:

import { Figure, Table, Callout, Ref, KeyMessages, StatRow, StatTile, DataDownload, Definition } from '@csdr/editorial';
ComponentUse
<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.

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.

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.

  • pnpm check and pnpm test pass
  • Every figure has caption, source and (for charts) an alt sentence
  • Key numbers come from an analysis cell, not the keyboard
  • References render under ## References
  • Preview at /reports/<slug>/ and print preview look right

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: report gets 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; analysis and brief flow from page 1 — title block, key messages, headline numbers, first section — with the same running header/footer.
  • status: draft | review | superseded prints a watermark on every page and names the status in the running header. Drafts are not built in production at all; withdrawn reports 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 in packages/theme/src/print.css, not in the report.

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.

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.

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.