Integrate 4-tier ontology manual into the Astro site
Context and Problem Statement
The OPDA ontology model is now documented as a 4-tier presentation in docs/manual/ (228 markdown files + 239 PNG diagrams, all generated from the 24 emitted TTLs per the IA blueprint at docs/information-architecture/). The content is currently consumable only via the local HTML export at docs/manual/_export/ — it is not published to the Astro site at openpropdata.org.uk and not reachable from the production navigation.
The Astro site (per ADR-0003) is the public delivery channel for OPDA content. It carries:
- 8 top sections per
src/lib/site.ts(strategy / governance / engagement / modelling / schema / implementation / adoption / library), each with sidebar groups + items declared in TypeScript - A single
Layout.astro+ shared named components (Diagram,Sidebar,SidebarItem,Header,Breadcrumbs,PageFooter,AuthButton,Comments) - A
data-theme="dark"attribute-driven token system (NOTprefers-color-scheme) — light/dark switched via a top-bar toggle; CSS custom-variant rewiresdarkto[data-theme="dark"] - Mermaid loaded client-side by
public/ui/client.js; its current theme values derive fromsrc/lib/diagram-palette.tsandpublic/ui/design-tokens.css. The formerdesign/mermaid-theme.jsis retained only as historical evidence. Diagram.astrowrapper component withsize+captionprops; mermaid source goes in the slot- All pages are individual
.astrofiles; no Astro content collections in use yet — every page is hand-written
The manual material does not fit one-page-per-file at the Astro layer:
- ~41 entities × 3 tiers (Concept + Logical + Physical-Ontology) = ~123 entity files
- 23 SKOS schemes × 2 tiers (Logical enumerations + Physical-Ontology vocabularies) = 46 scheme files
- 15 exemplars × 1 tier (Physical-Ontology) = 15 exemplar files
- 7 modules × 4 tiers = 28 module README pages
- 4 tier READMEs + cross-cutting topic pages (three-graph-separation, severity-tiers, shacl-af-rules, etc.) — ~15 pages
- Total ~227 documentation pages to integrate
Hand-authoring 227 Astro pages would defeat the IA’s generation discipline (the manual is meant to be regenerated from the TTLs when the ontology changes — per docs/information-architecture/README.md §“Source of truth”). Static .astro files per entity break that.
This ADR decides:
- Navigation scheme for surfacing the manual in the public site
- Which pages are JSON-driven content collections vs which remain static
.astroauthored pages - How the existing design system + Mermaid + dark/light handling extends to the new section without duplication
Decision Drivers
- Regenerability. The manual is generated from the TTLs; integrating it shouldn’t lock the content into hand-authored
.astrofiles that drift from the source. - Audience routing. The umbrella README’s tier-table maps audience → tier. The site navigation should mirror that mapping so a visiting surveyor / data engineer / SPARQL consumer lands on the right tier directly.
- Design coherence. The site provides a shared header, breadcrumbs, sidebar, footer, tokens and dark/light behavior. Manual pages reuse that shared shell; ADR-0073 may evolve its visual language without creating a parallel theme.
- Mermaid discipline. Diagrams already render client-side via
public/ui/client.js+Diagram.astro. Manual pages must use the same loader and the samethemeVarsfor dark/light coherence — no per-tier mermaid setup, no inline<script>mermaid initialisation. src/lib/site.tsas single navigation source. Per ADR-0003, navigation lives in typed TS, not in markdown frontmatter or filesystem walks. The manual integration must extendsite.tsconsistently.- No
.astrofiles per entity. Authoring (or regenerating) 41 × 3 = 123 entity.astrofiles is bad ergonomics + bad re-gen story. - Content lives in
docs/, route lives insrc/pages/. Per the project structure (docs/manual/is the canonical content surface;src/pages/is the routing surface). Integration must respect that boundary — Astro reads fromdocs/manual/at build time; nothing indocs/manual/becomes the Astro route by accident.
Considered Options
- Option A — Hand-author 227
.astrofiles mirroring the manual structure (one.astroper.md). Maximum control; zero regenerability; defeats the manual’s generation discipline. - Option B — Astro content collections sourced from
docs/manual/using Zod-validated frontmatter + dynamic-route[...slug].astrotemplates per tier. Per-entity / per-scheme / per-exemplar pages render from markdown + frontmatter. Tier READMEs + IA spec pages + cross-cutting topics are also content-collection entries (single mechanism). Reusable per-tier components (EntityCard,AttributeTable,TurtleBlock,SchemeMembershipTable,ShapeBlock,ExemplarReport) consume the collection data. Single navigation declared insrc/lib/site.tsextended with amanualsection. - Option C — External docs site (Docusaurus / mkdocs) at a sub-domain (manual.openpropdata.org.uk). Off-loads the integration entirely. Loses design coherence; duplicates deployment; breaks single-site UX.
- Option D — Render the manual at build time into the existing
_build/then iframe-embed insrc/pages/manual/. Worst of both — opaque to Astro; no SEO; broken dark-mode coordination.
Decision Outcome
Chosen option: B — Astro content collections sourced from docs/manual/, with reusable Zod-typed templates per tier + extended site.ts navigation + existing components for layout / theme / Mermaid, because it preserves the regenerability discipline (manual MD is canonical; Astro renders), keeps the design system + dark/light + Mermaid loaders unified, and bounds new authoring to ~12 reusable components + 4 dynamic-route templates instead of 227 hand-authored pages.
Navigation scheme
src/lib/site.ts gains a new manual section in HEADER_ORDER between modelling and schema:
// HEADER_ORDER updated
export const HEADER_ORDER = [
'strategy', 'governance', 'engagement',
'modelling', 'manual', 'schema', // 'manual' added between modelling + schema
'implementation', 'adoption', 'library',
] as const;
// SECTIONS.manual added
manual: {
key: 'manual',
title: 'Ontology manual',
summary: 'Four-tier presentation of the OPDA ontology model — concept narrative for SMEs, logical entity-relationship view for engineers, physical deployment topology for triplestore operators, and physical-ontology Turtle for ontology engineers.',
groups: [
{
heading: 'Overview',
items: [
{ url: '/manual', title: 'Section overview' },
{ url: '/manual/information-architecture', title: 'Information architecture' },
{ url: '/manual/validation-report', title: 'Validation report' },
],
},
{
heading: 'Concept tier — for SMEs',
items: [
{ url: '/manual/concept', title: 'Tier overview' },
{ url: '/manual/concept/foundation', title: 'Foundation' },
{ url: '/manual/concept/property', title: 'Property' },
{ url: '/manual/concept/agent', title: 'Agent' },
{ url: '/manual/concept/transaction', title: 'Transaction' },
{ url: '/manual/concept/claim', title: 'Claim' },
{ url: '/manual/concept/governance', title: 'Governance' },
{ url: '/manual/concept/descriptive', title: 'Descriptive' },
],
},
{
heading: 'Logical tier — for engineers',
items: [
{ url: '/manual/logical', title: 'Tier overview' },
// (7 module entries, same shape as Concept)
],
},
{
heading: 'Physical — deployment',
items: [
{ url: '/manual/physical-database', title: 'Tier overview' },
{ url: '/manual/physical-database/named-graphs', title: 'Named graphs' },
{ url: '/manual/physical-database/derived-profiles', title: 'Derived profiles' },
{ url: '/manual/physical-database/content-negotiation', title: 'Content negotiation' },
{ url: '/manual/physical-database/overlay-deployment/baspi5', title: 'BASPI5 deployment' },
{ url: '/manual/physical-database/operations', title: 'CI gates' },
{ url: '/manual/physical-database/modules', title: 'Per-module deployment views' },
],
},
{
heading: 'Physical — ontology',
items: [
{ url: '/manual/physical-ontology', title: 'Tier overview' },
{ url: '/manual/physical-ontology/three-graph-separation', title: 'Three-graph separation' },
{ url: '/manual/physical-ontology/severity-tiers', title: 'Severity tiers' },
{ url: '/manual/physical-ontology/shacl-af-rules', title: 'SHACL-AF rules' },
{ url: '/manual/physical-ontology/vocabularies', title: 'SKOS schemes' },
{ url: '/manual/physical-ontology/profiles/baspi5', title: 'BASPI5 profile' },
{ url: '/manual/physical-ontology/exemplars', title: 'Diagnostic exemplars' },
// (7 module entries below)
],
},
],
},
The sidebar’s heading-group structure surfaces audience routing inline: a reader landing on /manual sees the four tiers grouped under audience labels, picks theirs, and drops into the matching tier. Per-module entries within each tier match the docs/manual/<tier>/<module>/ directory structure.
Header order placement (between modelling and schema) reflects the manual’s purpose: it bridges the modelling-process content (governance, ODR corpus discussion) and the schema-detail content (PDTF JSON schemas, overlay forms).
JSON-driven content collections (the regenerable bulk)
src/content/manual/ is a new content collection rooted at the existing docs/manual/ markdown tree. The collection config (src/content.config.ts) declares Zod schemas for the entry types:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const baseFields = {
tier: z.enum(['concept', 'logical', 'physical-database', 'physical-ontology']),
module: z.enum(['foundation', 'property', 'agent', 'transaction', 'claim', 'governance', 'descriptive']).optional(),
title: z.string(),
entityUri: z.string().url().optional(), // opda:<EntityName> URI when applicable
sourceTtl: z.string().optional(), // 'source/03-standards/ontology/opda-property.ttl'
sourceOdr: z.string().optional(), // dct:source link target
};
const manualEntries = defineCollection({
loader: glob({ pattern: '**/*.md', base: '../../docs/manual' }),
schema: z.object({
...baseFields,
kind: z.enum([
'tier-readme',
'module-readme',
'entity',
'scheme',
'exemplar',
'cross-cutting', // three-graph-separation.md, severity-tiers.md, shacl-af-rules.md, etc.
'per-module-deployment', // physical-database/modules/<module>.md
'derived-profile',
'overlay-deployment',
'operations',
]),
}),
});
export const collections = { manual: manualEntries };
Per-tier dynamic-route templates render the entries:
src/pages/manual/
├── index.astro # Static — section landing
├── information-architecture.astro # Static — links to IA spec PDFs/HTML
├── validation-report.astro # Static — embeds VALIDATION-REPORT.md
├── concept/
│ └── [...slug].astro # Dynamic — getStaticPaths from concept entries
├── logical/
│ └── [...slug].astro # Dynamic — getStaticPaths from logical entries
├── physical-database/
│ └── [...slug].astro # Dynamic
└── physical-ontology/
└── [...slug].astro # Dynamic
Each [...slug].astro switches on entry.data.kind to pick the right tier-component:
---
import Layout from '@/layouts/Layout.astro';
import { getCollection, render } from 'astro:content';
import EntityPage from '@/components/manual/EntityPage.astro';
import SchemePage from '@/components/manual/SchemePage.astro';
import ExemplarPage from '@/components/manual/ExemplarPage.astro';
import ModuleReadmePage from '@/components/manual/ModuleReadmePage.astro';
import TierReadmePage from '@/components/manual/TierReadmePage.astro';
import CrossCuttingPage from '@/components/manual/CrossCuttingPage.astro';
export async function getStaticPaths() {
const entries = await getCollection('manual', e => e.id.startsWith('concept/'));
return entries.map(e => ({ params: { slug: e.id.replace(/^concept\//, '').replace(/\.md$/, '') }, props: { entry: e } }));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const tierComponents = {
entity: EntityPage, scheme: SchemePage, exemplar: ExemplarPage,
'tier-readme': TierReadmePage, 'module-readme': ModuleReadmePage,
'cross-cutting': CrossCuttingPage,
};
const TierComponent = tierComponents[entry.data.kind];
---
<Layout title={entry.data.title}>
<TierComponent entry={entry}>
<Content />
</TierComponent>
</Layout>
This is the JSON-driven mechanism: 227 entries, ~12 reusable templates, 4 dynamic routes. When the ontology regenerates and docs/manual/ is re-emitted, Astro picks up the new content automatically at the next build.
Static authoring (the non-regenerable shell)
Five files stay as hand-authored Astro pages:
| Page | Why static |
|---|---|
src/pages/manual/index.astro | Section landing — audience-routing copy + tier-cards. Not derived from the manual content; hand-authored editorial. |
src/pages/manual/information-architecture.astro | Surfaces the 4 IA-spec docs from docs/information-architecture/ with explanatory framing. The IA specs themselves are markdown but their landing page is curated. |
src/pages/manual/validation-report.astro | Embeds docs/manual/VALIDATION-REPORT.md with surrounding context. |
src/components/manual/*.astro | The 12 reusable per-kind templates (EntityPage, SchemePage, etc.) — hand-authored UI. |
src/lib/site.ts (the manual section block) | Navigation source of truth. |
Reusable components
12 new components under src/components/manual/. Each composes existing primitives such as Diagram and Breadcrumbs — no design tokens or themes invented. The retired PageMeta primitive was removed on 2 September 2026.
| Component | Role |
|---|---|
TierLanding.astro | Hero + tier-summary + audience callout (Concept “for SMEs”; Logical “for engineers”; etc.) |
ModuleLanding.astro | Module overview header (used by module-readme kind) |
EntityPage.astro | Concept / Logical / Physical-Ontology entity wrapper — pulls in the <Content /> slot then renders cross-tier links + source-URI footer |
SchemePage.astro | SKOS scheme wrapper — <Content /> + members table component |
ExemplarPage.astro | Exemplar + paired expected-report wrapper |
CrossCuttingPage.astro | Three-graph-separation / severity-tiers / shacl-af-rules pages — content + linked-tier callouts |
EntityHeader.astro | UFO meta-category badge + module breadcrumb + cross-tier link strip |
AttributeTable.astro | Logical-tier typed-attribute table (consumes table data extracted at build time from frontmatter) |
TurtleBlock.astro | Physical-Ontology Turtle code-block with copy-to-clipboard + namespace-resolver tooltip |
SchemeMembersTable.astro | SKOS scheme members table with sortable notation column |
ShapeBlock.astro | SHACL shape block with severity-tier badge + targeted-class link |
CrossTierLinks.astro | Footer strip linking Concept ↔ Logical ↔ Physical-Ontology versions of the same entity |
Design-system reuse
- No new tokens. All colours / spacing / typography use the existing CSS custom properties from
src/styles/global.css(which already powers[data-theme="dark"]switching). - No new layout. All manual pages render via
Layout.astro— same header / sidebar / footer / breadcrumbs as the rest of the site. - Manual-specific accents (UFO meta-category badge colours, severity-tier badge colours) defined as CSS custom properties under
:rootwith dark-mode variants under[data-theme="dark"]— extending the existing token system, not bypassing it.
Mermaid integration
The 239 PNGs at docs/manual/<tier>/diagrams/<doc>/<name>.png are local-export artefacts produced for offline browsing of the markdown. They are NOT shipped to the Astro site. Instead, the manual’s Mermaid source renders client-side at view time through the shared GraphDiagram island, identical to every other site page.
Concrete consequences:
- Source markdown’s
<details><summary>Mermaid Source</summary>\n\“mermaid\n…\n```\nblocks → at Astro build time, the markdown processor unwraps thesource and emits…blocks (orper the existing site convention). The PNGreferences in the .md are stripped at build (or wrapped in a src/layouts/Layout.astroadopts bare.mermaidblocks into the shared island;src/scripts/graph-diagram-mermaid.tsloads Mermaid and ELK.- Dark/light:
src/lib/diagram-palette.tsprojects the OPDA semantic and categorical palette for each theme; a theme change re-renders the island. No additional dark-mode wiring is needed for the manual. - The
Diagram.astrocomponent’ssize+captionprops remain the canonical authoring path for new diagrams elsewhere on the site; the manual content uses bare<div class="mermaid">because its source is generator-emitted (not hand-authored).
ELK layout flows through naturally — any mermaid block in the manual content whose YAML frontmatter has config: layout: elk gets the ELK plugin (already loaded by client.js:233).
Build-time markdown processor
Astro’s built-in markdown processor (remark/rehype) handles the manual .md content. Two custom remark/rehype plugins:
remarkUnwrapMermaidDetails— finds<details><summary>Mermaid Source</summary>\n\“mermaid\n…\n```\nblocks, replaces the entirewith…. The` reference immediately above is stripped (PNG is offline-only).
rehypeFrontmatterUriExtraction— forentity/scheme/exemplarkinds, extracts the OPDA URI from the markdown body (e.g. firstopda:<EntityName>H3 in Physical-Ontology classes.md sections) and adds it to the entry’s frontmatter asentityUri. Enables cross-tier link generation without re-parsing markdown at render time.
Both plugins live at src/lib/remark/ and are wired in astro.config.mjs.
URL convention
Per ADR-0002: bare slugs, no .html, no trailing slash. Manual URLs:
| Source markdown | Astro URL |
|---|---|
docs/manual/README.md | /manual |
docs/manual/concept/README.md | /manual/concept |
docs/manual/concept/property/property.md | /manual/concept/property/property |
docs/manual/logical/agent/enumerations/role-scheme.md | /manual/logical/agent/enumerations/role-scheme |
docs/manual/physical-ontology/vocabularies/built-form-scheme.md | /manual/physical-ontology/vocabularies/built-form-scheme |
docs/manual/physical-database/modules/property.md | /manual/physical-database/modules/property |
The [...slug].astro template per tier consumes the path-after-tier as slug. Astro emits the static HTML at build time.
Consequences
- Good, because regenerability is preserved — when the ontology TTLs change and the manual regenerates, Astro picks up the new markdown automatically at next build; zero
.astrofile edits. - Good, because design coherence is preserved — manual pages render through
Layout.astrowith the same header / sidebar / breadcrumbs / footer; samedata-theme="dark"token system; same Mermaid loader. - Good, because reusable components bound the new authoring effort — 12 Astro components serve 227 content entries; per-tier visual variants live in templates, not duplicated in content.
- Good, because the navigation extends the existing single-source
src/lib/site.tsdiscipline; no parallel navigation system; no filesystem-walked sidebar. - Good, because Mermaid + ELK + dark-mode coordination comes for free — the manual reuses
public/ui/client.js; no per-section mermaid setup. - Good, because the 239 generated PNGs stay offline-only — they’re for the local export at
docs/manual/_export/, never shipped to the site. The Astro build emits live-rendered Mermaid instead, which respects dark-mode toggling and ELK layout dynamically. - Bad, because Astro content collections + dynamic routes are new to this project — no existing template to copy; first content-collection usage in the repo. Mitigation: collection definition is ~30 lines; per-tier
[...slug].astro~50 lines; the bulk of complexity is in the 12 reusable components, which are conventional Astro. - Bad, because the two custom remark/rehype plugins (
remarkUnwrapMermaidDetails,rehypeFrontmatterUriExtraction) add build-time complexity. Mitigation: both plugins are <50 lines; tested via standard remark-test pattern; failure modes are visible at build time (broken markdown → build fails). - Bad, because
docs/manual/content needs frontmatter (kind:+ optionaltier:/module:/entityUri:/sourceTtl:) for Zod validation. The generator must emit frontmatter alongside content. Mitigation: the 4 worker scripts already emit frontmatter; small extension to add the new fields, then regenerate. - Neutral, because shipped HTML is build-time-static — no SSR, no API calls. SEO + caching behave identically to the rest of the Astro site.
Confirmation
The ADR is honoured when ALL of these hold:
src/lib/site.tsextended —manualsection appears inHEADER_ORDERbetweenmodellingandschema;SECTIONS.manualdeclares the per-tier groups.- Content collection wired —
src/content.config.tsdefinesmanualEntrieswith the Zod schema;getCollection('manual')returns ≥220 entries. - Four dynamic routes emit —
src/pages/manual/{concept,logical,physical-database,physical-ontology}/[...slug].astroeachgetStaticPathsfrom their tier’s entries;astro buildproduces a static.htmlper entry. - 12 reusable components live under
src/components/manual/and consume entry data + slot the<Content />from markdown. - No parallel Mermaid setup — the shared GraphDiagram adoption path handles every manual page’s diagrams.
- Dark/light toggle works on a manual page — manually verify by opening
/manual/concept/property/property, toggling the theme, confirming mermaid diagrams re-render with the dark theme variables. - ELK-laid-out diagrams render correctly — any
config: layout: elkblock in the manual’s<div class="mermaid">output renders via the ELK plugin client-side. - No
docs/manual/_export/files referenced fromsrc/— the local PNG export is offline-only; the Astro site renders Mermaid live. - No new design tokens — accent CSS custom properties for UFO meta-category / severity-tier badges all defined under
:root+[data-theme="dark"]per the existing convention. - Build succeeds —
npm run buildemits ~220 manual pages alongside the existing 158 site pages with zero errors.
Manual confirmation test: npm run build && npx serve dist && open http://localhost:3000/manual/concept/property/property — verifies the page renders, the sidebar shows the manual section, the breadcrumb path is correct, the master-entity diagram renders (mermaid + ELK), the dark-mode toggle works.
Implementation programme
This ADR is the architectural anchor; the engineering work to realise its ### Confirmation 10-item gate is sequenced as a 5-ADR programme. The implementing session reads docs/plan/manual-astro-integration.md first — that plan is the handover doc for cold-starting the implementation.
| Phase | ADR | Realises ADR-0015 §Confirmation | Dependency |
|---|---|---|---|
| 1 — Bootstrap | ADR-0016 — Manual content-collection wiring + site nav | #1 site.ts; #2 collection; #3 routes; #10 build | ADR-0015 |
| 2 — Components | ADR-0017 — Manual component library (12 components + accent tokens) | #4 components; #6 dark/light; #9 token discipline | ADR-0016 |
| 3 — Plumbing | ADR-0018 — Manual remark + rehype plugins | #5 Diagram.astro unchanged; #7 ELK renders; #8 no _export/ refs | ADR-0016 |
| 4 — Handshake | ADR-0019 — Modelling-section / manual handshake | (orthogonal — navigation coherence) | ADR-0016, ADR-0017 |
| 5 — Regeneration | ADR-0020 — Generator emission of manual frontmatter | implicit (frontmatter ownership) | ADR-0017, ADR-0018 |
Phases 2 + 3 run in parallel after Phase 1; Phase 4 + 5 gate on the parallel pair. Validation discipline (per programme plan §8) mirrors the ontology programme’s §9: independent validator per ADR; PASS gate on soundness + completeness + cross-ADR consistency + report committed.
More Information
- Implementation programme plan (handover doc):
docs/plan/manual-astro-integration.md. Read this first when picking up the work in a new session — it sequences the 5 sub-ADRs, defines validation discipline, names inherited open items, and gives the implementing session its reading order. - Sister implementation programme (precedent):
docs/plan/ontology-implementation.md— the 7-ADR ontology programme this plan’s swarm + validation pattern is modelled on. - Predecessor ADRs: ADR-0003 — Idiomatic Astro refactor (site architecture); ADR-0002 — Folder hierarchy + slug taxonomy (URL convention); ADR-0007 — Ontology generator specification (the regenerator); ADR-0011 — Module TBox emission (source of entity content).
- IA blueprint:
docs/information-architecture/— defines the 4 tier structures the integration mirrors. - Content source:
docs/manual/README.md— the canonical 228-markdown content surface this ADR integrates. - Mermaid + ELK loader:
public/ui/client.js:228-269— existing initialisation reused as-is. - Theme tokens:
src/styles/global.css+public/ui/design-system.css—data-theme="dark"attribute-driven; extended for manual-specific accent tokens, never bypassed. - Astro content collections docs: https://docs.astro.build/en/guides/content-collections/ (Zod schema definition + dynamic route patterns the implementation follows).
- Out of scope:
- Authoring the 227 manual content entries (already done — they live in
docs/manual/). - PDF export from the site (the local
_export/workflow handles offline PDF; the Astro site is HTML-only). - Per-overlay route templates for TA6 / NTS / LPE1 etc. (defer until those overlay profiles emit; ADR-0013 Phase-2/3 work).
- JSON-LD HTTP content negotiation for
https://opda.org.uk/pdtf/<EntityLocalName>URI dereference (separate deployment-layer work; the manual page is the HTML landing target for those redirects but the redirect setup itself is in ADR-0006). - Migrating the existing
src/pages/modelling/content into content collections — owned by ADR-0019 per-page decision.
- Authoring the 227 manual content entries (already done — they live in
Amendment — shared Mermaid runtime (1 September 2026)
The single-loader decision remains in force, but the implementation owner moved from
public/ui/client.js to src/scripts/graph-diagram-mermaid.ts. Layout.astro adopts
bare .mermaid output from Diagram.astro, Markdown and generated pages into the
same GraphDiagram shell. Components and content remain source authors; none imports,
initialises or renders Mermaid independently.
Comments
Loading comments…
Sign in to post a comment