Skip to content

Technology Stack


Documentation Framework

The site is built on MkDocs with the Material theme, the mkdocs-static-i18n plugin for dual-language support, and the git-revision-date-localized plugin for showing last-modified timestamps.

Component Technology Version
Static site generator MkDocs 1.5+
Theme Material for MkDocs 9.5+
Internationalisation mkdocs-static-i18n 1.2+
Revision dates git-revision-date-localized 1.2+
Diagram rendering Mermaid via Material
Code font JetBrains Mono, vendored in docs/assets/fonts 2.304, self-hosted (SIL OFL 1.1)
Hosting Azure Static Web Apps โ€”
CI/CD GitHub Actions โ€”

Material's default code font, Roboto Mono, has no box-drawing characters, so diagrams drawn with them in a fenced block borrowed glyphs from a fallback font at another width and their borders drifted. JetBrains Mono draws them at column width, and it is vendored because Google Fonts' subsets omit that range. hooks/code_glyphs.py warns at build time when a box diagram uses a character the font cannot draw โ€” in practice, emoji.

graph LR
    subgraph "Documentation Framework"
        MKDOCS[MkDocs 1.5+]
        MATERIAL[Material Theme 9.5+]
        I18N[mkdocs-static-i18n]
        GIT_REV[git-revision-date-localized]
    end

    subgraph "Content"
        MD[Markdown Files]
        MERMAID[Mermaid Diagrams]
        CODE[Syntax Highlighting]
    end

    subgraph "Styling"
        CUSTOM[Custom CSS<br/>NL Design System]
    end

    subgraph "Deployment"
        GH_ACTIONS[GitHub Actions]
        AZURE_SWA[Azure Static Web Apps]
        CDN[Global CDN]
    end

    MKDOCS --> MATERIAL
    MATERIAL --> I18N
    MATERIAL --> GIT_REV

    MD --> MKDOCS
    MERMAID --> MKDOCS
    CODE --> MKDOCS

    MATERIAL --> CUSTOM

    MKDOCS --> GH_ACTIONS
    GH_ACTIONS --> AZURE_SWA
    AZURE_SWA --> CDN

    style MKDOCS fill:#4a90e2
    style MATERIAL fill:#e17000
    style I18N fill:#50c878
    style AZURE_SWA fill:#4a90e2

The page metadata header

Every page a sync has touched carries a one-line header directly under the breadcrumbs: its git created and updated dates, then one fact that says what the page describes. Which fact depends on the front-matter key the page declares.

Front matter Header reads Used on
component: Linked Data Explorer ๐Ÿ” Linked Data Explorer v2026.09.2 PROD ยท build 007b350 ยท #39 Component pages
scope: cross-cutting Applies to all components docs built 2026-09-09 contributing/**
scope: cross-cutting + verified: Applies to all components ยท verified 2026-09-09 against โœ๏ธ bbda389 ๐Ÿ” 007b350 โš™๏ธ 04e38c8 Cross-cutting pages whose claims have been re-checked

The version and build come from docs/repo-versions.json, loaded at build time by hooks/repo_versions.py; the stamp comes from the page's own front matter. The partial is overrides/partials/doc-meta.html. Pages under an archive/ path show the dates only, because an archived page describes an older release on purpose.

Two identities, because a release number is not enough

A component's version is written by hand at release time, so it names a release rather than a build of it. Acceptance and production can serve different builds of the same version, and a new build reaches an environment every time a deploy workflow runs โ€” with no release bump at all. The header therefore records the build beside the version: the frontend build that was serving the recorded environment when the docs were synced. A reader can compare it with the build line the running application prints in its changelog. See Build Provenance for what the two halves of a build id mean.

Why cross-cutting pages are stamped with commits, not builds

A contributing page makes claims about repository configuration โ€” workflows, SECURITY-PIPELINE.md, renovate.json, runner configs and their thresholds. Most changes to those produce no new build. Every frontend deploy workflow is path-filtered, as read on 27 September 2026:

Repository The frontend build fires on Does not fire on
Linked Data Explorer packages/frontend/**, its own workflow file, the root package.json / package-lock.json, and .nvmrc Other workflows, SECURITY-PIPELINE.md, renovate.json, backend runner config
RONL Business API packages/frontend/**, packages/shared/** and packages/pa-cockpit/**, its own workflow file, and .nvmrc The same
CPSV Editor Everything except docs/**, .claude/** and **/*.md SECURITY-PIPELINE.md and every other Markdown record

So a build id is silent about exactly the changes that falsify these pages. The commit on the branch of record is not: it moves with every change.

Two refinements, both from 19 September 2026. .nvmrc joined every filter, because a Node bump that is not in the list changes the version and deploys nothing. And the filters above are now the push filters: on the acc workflows whose checks became required, the pull_request trigger carries no path filter at all, the same pattern having moved into a changes job that skips the build rather than never starting it. A pull request therefore starts these workflows whatever it touches, while still building โ€” and so producing a new build id โ€” only when it touches something relevant. The premise of this section is unchanged: see how a path-filtered workflow became requireable.

What a stamp promises

---
scope: cross-cutting
verified:
  date: 2026-09-09
  against:
    CPSV Editor: "bbda389"
    Linked Data Explorer: "007b350"
    RONL Business API: "04e38c8"
---

A component appears in a stamp only if its claims on that page were re-checked against that commit, on that date. Nothing else earns an entry โ€” not editing the page, not a sync of that component that left the page alone. That is why the stamp is per page rather than one site-wide list of current builds: a sync re-verifies only the pages it touches, and a global list would assert verification that never happened.

Two consequences are deliberate:

  • An old stamp is information. It tells the reader how long a page has gone without being checked, which is more useful than a fresh date that means nothing.
  • A partial stamp is honest. build-provenance.md named two components, not three, from 9 September until the 20 September pass, because on 9 September only the CPSV Editor's and the Linked Data Explorer's claims on it were re-checked.

On a stamped page the stamp replaces docs built. The two dates answer different questions and will usually drift apart โ€” docs built moves with every sync of any component, the stamp only when this page is re-checked โ€” but side by side the site-wide date is the weaker one: it says a sync happened somewhere, not that this page was part of it. Unstamped pages keep docs built, because there it is the only date that applies.

SHAs are quoted because YAML reads an unquoted all-digit SHA as a number, and a leading zero as octal โ€” 0123456 becomes 42798 before the template sees it.

Staleness is checked by the sync, not the build

The site build is offline and checks nothing about a stamp. The /iou-document-patch skill does, with .claude/skills/iou-document-patch/scripts/stamp-staleness.py. For each stamped page and component it lists the commits on the branch of record that touched CI-relevant paths since the stamp:

docs/en/contributing/supply-chain.md  โ€” verified 2026-09-09
  โš  CPSV Editor            bbda389  โ†’  origin/acc 701d966  โ€”  3 CI-relevant commits since
        deployed by: Deploy PROD (white-sky) #88
        1b2457e ci: refresh the transitive dependency tree, which nothing here ever did
        b1af50b docs: record that scan gates acc, in the three places that claimed otherwise
        2e4c1ac feat(ci): scan dependencies with Semgrep, on a ref rather than a laptop

That is its first run, two days after the stamps were written, and the second subject line shows the point: a component repository recording that "three places claimed otherwise" is a contributing page going stale in the repository's own words.

The script also fails on a malformed stamp โ€” an unquoted SHA, an unknown component, a commit that does not resolve โ€” and on a recorded build that contradicts the Actions run it names.

Since 24 September it also counts a Promote to Production run as a deploy. In the Linked Data Explorer and the RONL Business API the production deploys run as jobs inside that run, so a check on workflow names alone would report a promoted commit as having deployed nothing.

Every SHA leads to the run that deployed it

A stamped SHA and a recorded build both link to GitHub, where the three applications' deploy workflows run โ€” not to the GitLab mirror repo_url points at. The mirror carries the same commits under the same SHAs, but it lags behind and shows none of the runs, so following a SHA there answers neither question a reader has: was this deployed, and as which build?

Each component's ci_repo in repo-versions.json names the repository where its deploys run. For the Norm Editor and CPRMV that is GitLab, because their pipelines are GitLab CI; for the other three it is GitHub. A build additionally records run_url, the exact Actions run that produced it, and the header links the build id there.

The staleness script asks GitHub for each stamped SHA's deploy runs, and prints them. On 11 September all stamps matched:

Stamped SHA Deploy runs it triggered
CPSV Editor bbda389 Deploy PROD (white-sky) #88
Linked Data Explorer 007b350 Deploy Frontend to Production #39, Deploy Backend to Production #15, Deploy Ropa site to Productuion #4 (sic โ€” the workflow's own spelling)
RONL Business API 04e38c8 Deploy Frontend to Azure ACC #270, Deploy Public Site to Azure ACC #71, Deploy PA Demo to Azure ACC #102

A commit that changes only CI configuration deploys nothing

Because deploy workflows are path-filtered, a commit touching only renovate.json, SECURITY-PIPELINE.md or a non-deploy workflow triggers the supply-chain audit and no deploy at all โ€” the Linked Data Explorer's bd52eac is one. A page verified against such a commit has no build a reader can match its stamp to. The script warns rather than fails: the stamp is still true about the repository, it just cannot be followed to a running application. When the choice is free, stamp the most recent commit that did deploy, provided no CI-relevant commit sits between it and what you read.


Build & Deploy Pipeline

This repository has two deployment pipelines, on two different hosts, and they are easy to confuse:

Pipeline Host Trigger Deploys
.github/workflows/azure-static-web-apps-*.yml GitHub Actions Push to main, and pull requests against it The production site; PRs build a preview
pipeline/azure_ado_pipeline.yml Azure DevOps Manual only, since 21 August 2026 The acceptance site

Both build with MkDocs and publish to Azure Static Web Apps.

The Azure DevOps pipeline used to fire on every push to acc and on pull requests against it, which is what made acc the acceptance branch in practice. It now carries trigger: none and pr: none, so it runs only when someone starts it by hand. Pushing to acc therefore deploys nothing on its own today โ€” but the pipeline is still there, and re-enabling it is a two-line change.

The GitHub remote is not the only one: this repository is also pushed to Azure DevOps (flevoland) and to the open-regels GitLab instance. Only the first two run anything.

Both builds run mkdocs build --verbose, not --strict, so a broken internal link or a page missing from the nav produces a warning rather than failing the deploy. Run mkdocs build --strict locally before pushing if you want that caught.

graph TB
    DEV[Developer] -->|Edit Markdown| LOCAL[Local Repository]
    LOCAL -->|git push| GITHUB[GitHub Repository]

    GITHUB -->|Trigger| CI[GitHub Actions]

    CI -->|1. Install| DEPS[Python Dependencies<br/>mkdocs, mkdocs-material<br/>mkdocs-static-i18n]
    DEPS -->|2. Build| BUILD{mkdocs build}

    BUILD -->|3. Generate| EN_SITE[site/<br/>English Site at root]
    BUILD -->|3. Generate| NL_SITE[site/nl/<br/>Dutch Site]

    EN_SITE -->|4. Deploy| AZURE[Azure Static Web Apps]
    NL_SITE -->|4. Deploy| AZURE

    AZURE -->|5. Serve| PROD_EN[https://iou-architectuur.open-regels.nl/]
    AZURE -->|5. Serve| PROD_NL[https://iou-architectuur.open-regels.nl/nl/]

    USER[End Users] -->|Visit| PROD_EN
    USER -->|Visit| PROD_NL

    PROD_EN <-.Language Switch.-> PROD_NL

    style DEV fill:#4a90e2
    style GITHUB fill:#e17000
    style AZURE fill:#50c878
    style PROD_EN fill:#4a90e2
    style PROD_NL fill:#e17000
    style BUILD fill:#ffd700