How docs-spec-template produces both the ARC-compliant submission PDF and
the Antora static-site HTML from a single source tree, and the phased plan
to finish that migration.
- Chapter content lives once, in
modules/ROOT/pages/*.adoc, as standalone Antora pages (each starts with a level-0= Title). src/spec-sample.adocis a thin PDF assembler: it includes those pages withleveloffset=+1so their level-0 titles become chapters in the PDF, reproducing the historical section numbering.- Build the PDF:
make→build/<short>-v<ver>-<YYYYMMDD>.pdf. - Build the site locally:
antora antora-playbook.yml→build/site/.
The PDF wants one master document; Antora wants one file per navigable page.
leveloffset=+1 reconciles them:
modules/ROOT/pages/intro.adoc src/spec-sample.adoc (PDF assembler)
----------------------------- -----------------------------------
= Introduction (level 0) -> include::...intro.adoc[leveloffset=+1]
== Sub Section (level 1) => renders as "== Introduction" (Ch.1)
and "=== Sub Section" (1.1)
So the pages are the single content source; the assembler is PDF-only glue. PDF-only constructs stay in the assembler and never appear in a page:
include::../docs-resources/global-config.adoc[](cross-repo relative include — would break under Antora, so it lives only here)- the back-of-book
[index] == Indexmacro (Antora generates no index) :title-logo-image:,:pdf-theme:,:pdf-fontsdir:,:doctype: book, etc.
antora.yml # component descriptor (keep MINIMAL — see below)
antora-playbook.yml # LOCAL preview playbook (mirrors production UI)
modules/ROOT/
nav.adoc # site navigation
pages/ # single source of chapter content (Antora pages)
index.adoc # site landing page (Antora start_page; NOT in PDF)
intro.adoc chapter2.adoc contributors.adoc bibliography.adoc
examples/example.bib # bibliography database
src/spec-sample.adoc # PDF assembler (Makefile DOCS target)
Makefile # asciidoctor-pdf/html via Docker; ARC PDF naming
docs-resources/ # submodule: PDF fonts/themes/logo + global-config
The canonical site is built elsewhere, by the central playbook at
github.com/riscv-admin/antora.riscv.org (dev mirror:
github.com/riscv-admin/antora-dev.riscv.org). This repo is just a content
source consumed by that playbook.
The central playbook supplies, uniformly to every spec:
- Extensions:
asciidoctor-kroki(diagrams, incl. wavedrom),@djencks/asciidoctor-mathjax(math), an ASAM extension forcite:/bibliography::[], plus section/nav numbering extensions. - Shared AsciiDoc attributes:
doctype: book,icons: font,xrefstyle,source-highlighter: highlight.js, kroki config, math entities, etc. - UI:
github.com/riscv-admin/riscv-antora-only-uirelease bundle.
Consequence — keep antora.yml minimal (name/title/version/nav). Component
attributes override the playbook, so setting rendering attributes here (notably
sectnums, which the central section-numbering extension controls) desyncs this
spec from the rest of the library. Component identity/grouping keys are fine;
rendering attributes are not. Put preview-only rendering config in
antora-playbook.yml instead.
Add to content.sources: in the dev-site antora-playbook.yml (push the branch
first — Antora fetches from GitHub, not the worktree):
# docs-spec-template example spec (dev preview) — component: spec-sample
- url: https://github.com/riscv/docs-spec-template.git
branches: antora-setup
start_page: ROOT::index.adoc
start_path: /No nav: key needed (declared in antora.yml). Renders at /spec-sample/.
Separate from the central site, this repo also publishes a standalone copy of
the same content to its own GitHub Pages site, next to the release PDF. For a
repo seeded from this template that is not part of the RISC-V central library,
this is the primary HTML output; for specs that are consumed centrally,
docs.riscv.org stays canonical and this is a convenience.
- Workflow:
.github/workflows/publish-site.yml— runs onv*tag pushes and on manual dispatch, and deploys viaactions/deploy-pages. - Build:
scripts/build-pages-site.sh— the whole build, runnable locally. - Setup in a seeded repo: one manual step.
actions/configure-pagesis configured withenablement: trueand will turn Pages on by itself wherever it is allowed to — but the RISC-V organization restricts Pages site creation, so the workflow token is refused withResource not accessible by integrationand a repository admin must set Settings → Pages → Source to "GitHub Actions" once. Equivalent API call, with an admin token:gh api -X POST repos/<org>/<repo>/pages -f build_type=workflow. Once the site exists,enablement: trueis a no-op and releases publish unattended. The job skips itself on private repos, where Pages needs Team/Enterprise.
The Pages build does not get its own committed playbook. antora-playbook.yml
carries a hand-maintained mirror of the central playbook's rendering attributes,
and a second copy of that block would silently drift. Instead
scripts/gen-pages-playbook.js loads it and overrides four things into a
generated, gitignored antora-pages-playbook.yml:
| Key | Why |
|---|---|
site.url |
Project Pages sites live at https://<org>.github.io/<repo>/, not at a domain root; Antora needs the base path for the sitemap and canonical links. |
content.sources |
Which versions to publish (below). |
output.dir |
build/pages-site, so a publish never clobbers the build/site preview output. |
cover-logo |
Resolves the cover logo locally instead of from common::. |
index.adoc renders the logo through the cover-logo attribute rather than a
literal resource ID. It defaults in antora.yml to common::risc-v_logo.svg,
the shared asset the central build uses. A single-repo build has no common
component, so the Pages build stages the copy from the docs-resources submodule
into modules/ROOT/images/ (gitignored) and hard-sets the attribute to it. The
central build is unaffected, and the local-preview behaviour is unchanged — a
bare npm run preview still logs the one expected unresolved common:: image
that validate-content-source.yml exception-lists.
A build with no release tag to name it — the first manual publish in a freshly
seeded repo, or any workflow_dispatch before the first tag — gets
release-info.sh's dev version, vX.YY-<sha>-<date>. That string is published
under a short fixed label (dev, override with DEV_VERSION_LABEL) instead of
verbatim, for two reasons:
- It breaks the navigation. The RISC-V UI floats the version selector
beside the nav tree (
.version-box{float:right}plus.nav-version-group{overflow:hidden}, which establishes a block formatting context), so the nav only gets the width left over next to the selector, and the selector is as wide as its longest version string. At 22 characters the nav collapses to about three characters per line. Release tags are short (vX.Y) and render correctly, so this never affects a real release. - It churns URLs. Every dispatch would otherwise mint a
/spec-sample/<sha>/path that the next publish orphans, since each deploy replaces the whole site./spec-sample/dev/stays linkable.
The full dev version is not lost: it is still stamped into page-revnumber and
shown on the cover page, so a published dev site names the exact commit it came
from.
The version stamp is applied to the working tree at build time from
scripts/release-info.sh — the same source the PDF uses — so the published
version matches the PDF even though the tagged commit's committed antora.yml
has not been stamped yet. Nothing is committed back; getting the stamp onto
main stays build-pdf.yml's job, which opens a review PR for it.
The build also scans release tags and publishes any that carry a correctly
stamped descriptor, which is what would give the site a multi-version dropdown. A
tag qualifies only if it contains an antora.yml whose version: equals the tag
itself. That is deliberately strict, for two reasons:
- Antora aborts the entire build on any ref that has no
antora.yml, so tags predating the Antora layout must be filtered out. - A tag stamped with a different version would publish under a wrong or duplicate version path.
Today no tag qualifies, so the site publishes exactly one version. The reason
is ordering: a release tag is pushed first, and build-pdf.yml stamps
antora.yml on main afterwards — so a tag never contains its own version.
If the release flow is ever changed to stamp and commit before cutting the
tag, past releases begin appearing in the version dropdown automatically, with no
change to this script.
Chapter/section numbering is applied by the central playbook, not this repo.
The nav_numbering and section_numbering extensions read a per-component rule
from the playbook's numbering_rules anchor. That is why antora.yml sets no
sectnums (design rule under "Production model"): the central extension owns it,
and a local override would desync this spec. A generic template can't ship a rule
— chapter counts differ per spec — but the bundled example spec can.
The important subtlety: a rule's chapters: {start, end} are line numbers in
modules/ROOT/nav.adoc, not chapter numbers. The extension scans nav lines;
each line in [start, end] matching ^*+ xref:…[ becomes a chapter, numbered
sequentially from 1. For this example spec the nav xrefs currently sit at:
nav.adoc line |
page | numbering |
|---|---|---|
| 12 | index.adoc |
landing/cover — not a chapter |
| 13 | intro.adoc |
Chapter 1 |
| 14 | chapter2.adoc |
Chapter 2 |
| 15 | contributors.adoc |
unnumbered (front matter) |
| 16 | bibliography.adoc |
unnumbered ([bibliography]) |
so the entry to add to numbering_rules in the central playbook
(riscv-admin/antora-dev.riscv.org, antora/antora-playbook.yml) is:
- component: spec-sample
module: ROOT
branches: ['antora-setup'] # match the content-source branch/tag
chapters: {start: 13, end: 14} # nav lines 13–14 = intro, chapter2One entry covers both extensions (they share the &numbering_rules anchor).
section_numbering ignores appendices, so the bibliography stays unnumbered,
matching the PDF (where contributors is a [preface] and the bibliography is
unnumbered back matter).
⚠ This rule is line-coupled to
nav.adoc. Adding, removing, or reordering entries — or editing the nav header comments — shifts the line numbers, so the central rule must be updated in lockstep or site numbering silently drifts. (Adding the warning comment tonav.adocalready moved these from lines 7–8 to 13–14.) Also updatebranches:/tags:to match wherever the spec is consumed (dev =antora-setup; latermainor release tags).
- Phase 1 — Dual-source layout. Chapters →
modules/ROOT/pages/;src/spec-sample.adoc→ assembler withleveloffset=+1;antora.ymlminimal; nav/start_page repointed; UI bundle fixed to the RISC-V production bundle. Verified:make→ 14pp ARC PDF;antora antora-playbook.yml→ 5 pages + landing. (commit19211be) - Phase 2 — docs-resources cross-repo include. Verified the boundary
holds: the
include::../docs-resources/global-config.adoc[]appears only insrc/spec-sample.adoc; zerodocs-resourcesreferences undermodules/.global-config.adocdefines three attributes —company,url-riscv,doctype: book. Decision:company/url-riscvare unreferenced in content, so they do not need to reach the site;doctype: bookis already supplied uniformly by the central playbook, so it must not be duplicated inantora.yml(would override the playbook — see design rule above). Localantora antora-playbook.ymlbuild passes (exit 0, 5 pages) with no unresolved includes or cross-repo artifacts in the rendered HTML.
- Dev-site fixes (found on antora-dev.riscv.org after publishing): two
site-rendering bugs the local build could not surface (both need the central
extensions/assets):
- Bibliography rendered raw.
cite:/bibliography::[]came through as literal text. The central ASAM bibliography extension no-ops unless the component declaresasamBibliography: '<bib-resource-id>'inantora.yml. Fix: moved the bib to the canonicalmodules/ROOT/resources/riscv-spec.bib(wasmodules/ROOT/examples/example.bib;examples/is a reserved Antora family and no real spec puts a bib there), setasamBibliography: 'ROOT:resources/riscv-spec.bib'inantora.yml, and repointed the PDF assembler's:bibtex-file:to the new path.makePDF build re-verified green. - No cover page. The landing page was placeholder prose. Rebuilt
modules/ROOT/pages/index.adocon the ratified-spec cover pattern (image::common::risc-v_logo.svg+ title heading + version line + phase banner linking to riscv.org/spec-state). Thecommon::logo resolves on the site build; it warns in bare local preview (nocommoncomponent) until Phase 5.
- Bibliography rendered raw.
- Phase 3 — Attribute split. Added an
asciidoc.attributesblock to the LOCALantora-playbook.yml, mirroring the central playbook's shared rendering attributes (doctype, icons, xrefstyle, source-highlighter, toclevels, math/char entities, kroki config, image-path helpers) soantora antora-playbook.ymlpreview renders like production. Deliberately omitted: ISA-manual-specific content shortcuts (csrname,i/u/ra/reg_list), UI PDF-button plumbing (pdf_url,multiple_pdfs,pdf_list), and section-numbering keys (sectnumsetc. — commented out in central because the section-numbering extension controls them, per rule #2). PDF-only attributes stay in the assembler/Makefile;antora.ymlstays minimal (onlypage-group+ the spec-specificasamBibliography). Local build passes (exit 0, 5 pages). The extensions these attributes configure (kroki, mathjax, ASAM bib) land in Phase 5, so the attributes are inert in bare local preview until then. - Phase 4 — Version bridge. The ARC Author Guide requires the artifact's
identity to be the
vX.Yrelease tag; the HTML site version must match the PDF version exactly. Because Antora reads a STATICversion:from the committedantora.yml(the central playbook just fetches the branch — no build step runsrelease-info.shon the spec side), the bridge stamps that file, the Antora analogue of howscripts/update-spec-state.shstampsSPEC_STATE.md:scripts/stamp-antora-version.sh [version] [date]writesversion:and the cover attributes (page-revnumber,page-revdate,page-phase,page-phase-display,page-phase-notice) fromrelease-info.sh— the same source the PDF uses. Idempotent; preserves comments/nav.make stamp-antorawraps it with the Makefile'sVERSION/DATE, so site and PDF cannot diverge.antora.ymlversion:is now the exact tag (site path/spec-sample/<ver>/, a distinct citable folder per release — matching ARC's citability goal).index.adoccover renders the PDF title-page revision line verbatim:Version {page-revnumber}, {page-revdate}: {page-phase-display}, plus a phase banner mirroring the PDF "Document State" preface. No hardcoded version.- Verified: stamping
v0.8yields/spec-sample/v0.8/with cover "Version v0.8, 2026-06-12: Stabilized"; restamped to the repo's real state. - Release step: automated in
build-pdf.yml(see Phase 6) — the same run that builds the PDF stamps the matching version and opens a review PR againstmain; merging it is part of cutting a release. Manual fallback for local/offline releases:make stamp-antora VERSION=vX.Ythen commitantora.yml. - Hardening: the stamp script asserts that each key was substituted exactly
once and that the result still parses as YAML, then fails loudly. It
previously exited 0 having silently produced invalid YAML when a formatter
line-wrapped
page-phase-noticeinto a plain multi-line scalar (caught in review of #105); it now also drops such wrapped continuations.antora.ymlis excluded from yamlfmt (#106) so the wrap cannot return.
- Phase 5 — Extensions + preview. Wired the central playbook's asciidoc
extensions into the LOCAL preview so
antora antora-playbook.ymlrenders like production:package.jsonpins Antora +asciidoctor-kroki+@djencks/asciidoctor-mathjaxas devDependencies (reproducible preview, not a globalantora). Version note:asciidoctor-krokimust be0.18.1(what the central playbook uses);1.0.0needs a newer Asciidoctor.js than Antora 3.1.x bundles and dies withblock.$!= is not a function.antora-playbook.ymlgained anasciidoc.extensionslist (kroki + mathjax).docker-compose.ymlruns a local Kroki onlocalhost:9870(matching thekroki-server-urlattribute), so wavedrom renders without shipping source to a public service. Preview flow:npm install→docker compose up -d kroki→npm run preview.- Added a small
latexmathdemo tochapter2.adoc(the example spec now demonstrates math like it already did wavedrom/citations). - Verified: wavedrom → inline
<svg>(kroki),latexmath→ MathJax SVG (data-mml-nodemarkers), andmakePDF still builds clean with the new math. - Bibliography deliberately NOT mirrored locally. The central
cite:/bibliography::[]support is a custom, multi-file ASAM extension vendored in the central playbook (not on npm); replicating it here would be heavy and a divergence risk. Citations render on the central site (ASAM) and in themakePDF/HTML (asciidoctor-bibtex gem), so authors have two ways to check them without it living in this repo. Revisit only if local ASAM parity is needed.
- Phase 6 — CI (content-source validation). This repo is ONE content
source; the real site is built centrally from 20+ sources, so there is no site
to build here. Added
.github/workflows/validate-content-source.yml: on PR / push-to-main / dispatch it builds THIS component in isolation viaantora-playbook.yml(npm ci → Kroki service on :9870 →npx antora), as a gate so broken AsciiDoc / unresolved intra-component xrefs/includes / extension errors fail here instead of stalling the shared central build. It does NOT deploy. Cross-component refs (common::risc-v_logo.svg, xrefs into other specs) resolve only centrally, so they are exception-listed. Verified: clean tree passes; an injected broken xref fails the gate.- Version-stamp automation (Phase 4 lockstep, now wired).
build-pdf.ymlresolvesVERSION/DATEonce (shared by the PDF build and the stamp, so no date drift) and astamp-site-versionjob stamps the matchingantora.ymland opens a review PR againstmain. It runs only for real releases (skips PR previews and drafts) and is monotonic (never stampsmainbackwards when an older tag is rebuilt). Chosen overversion-bot.ymlbecausebuild-pdf.ymlis the run the author actually triggers to cut a release and the run that builds the PDF, so both come from the same source/version/run by construction. - Why a PR, not a direct push (changed in review of #105): the original job
ran
git push origin HEAD:main, assuming a seeded repo'smainis unprotected. That is a bad bet for a file copied into 20+ spec repos whose settings we do not control — and a rejected push fails after the Release is published, leaving a green release with a silently stale site. A PR works under any protection setting and matches the milestone-PR idiom already inversion-bot.yml. Trade-off: lockstep is no longer merge-free — an unmerged stamp PR means the site lags, so treat it as part of the release checklist.
- Version-stamp automation (Phase 4 lockstep, now wired).
make # ARC PDF (+ HTML) via Docker; VERSION/DATE overridable
make stamp-antora VERSION=vX.Y # stamp antora.yml to match the PDF release (commit the result)
# Local Antora preview (renders like production: diagrams + math):
npm install # one-time: Antora + kroki/mathjax extensions
docker compose up -d kroki # local Kroki server on :9870 (for wavedrom/diagrams)
npm run preview # antora --fetch antora-playbook.yml -> build/site/
docker compose down # stop Kroki when done
# GitHub Pages site, exactly as CI builds it (needs Kroki up, as above).
# NOTE: this stamps antora.yml in your worktree -- pass NO_STAMP=1 to leave it be.
NO_STAMP=1 ./scripts/build-pages-site.sh # -> build/pages-site/
./scripts/build-pages-site.sh https://org.github.io/repo # with the real base URL