diff --git a/.github/workflows/cleanup-stale-branches.yml b/.github/workflows/cleanup-stale-branches.yml
new file mode 100644
index 00000000..449e9e13
--- /dev/null
+++ b/.github/workflows/cleanup-stale-branches.yml
@@ -0,0 +1,58 @@
+name: Clean up stale branches
+
+on:
+ push:
+ branches: [master]
+ paths:
+ - .github/workflows/cleanup-stale-branches.yml
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ cleanup:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - name: Delete obsolete branches
+ env:
+ GH_TOKEN: ${{ github.token }}
+ REPOSITORY: ${{ github.repository }}
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ branches=(
+ "agent/final-repository-cleanup"
+ "agent/fix-vertical-stack-backgrounds"
+ "agent/pure-deterministic-install"
+ "agent/stage1-two-repo-runtime"
+ "agent/stage2-legacy-compatibility"
+ "agent/stage3-migrate-all-presentations"
+ "agent/stage4-pure-exporter"
+ "agent/stage5-pages-integration"
+ "agent/stage6-remove-reveal-fork"
+ "agent/stage-7-canonical-sources"
+ "agent/stage-7-canonical-sources-final"
+ "chore/vercel-preview-deployments"
+ "experiment/reveal-6-migration"
+ "feat/presentation-index"
+ "fix/reveal-upstream-bug-backports"
+ "fix/vercel-preview-export-recursion"
+ "security/reveal-xss-backports"
+ "docs/production-build-readme"
+ )
+
+ for branch in "${branches[@]}"; do
+ if gh api "repos/${REPOSITORY}/git/ref/heads/${branch}" >/dev/null 2>&1; then
+ echo "Deleting ${branch}"
+ gh api --method DELETE "repos/${REPOSITORY}/git/refs/heads/${branch}"
+ else
+ echo "Already absent: ${branch}"
+ fi
+ done
+
+ echo "Remaining branches:"
+ gh api --paginate "repos/${REPOSITORY}/branches?per_page=100" --jq '.[].name'
diff --git a/README.md b/README.md
index 3abcd11c..27acfb1c 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,30 @@
# Digiguru presentations
-This repository contains Digiguru's presentation content and the **Pure** Reveal.js runtime. Pure is the only runtime/build path.
+Source, runtime and build tooling for the Digiguru presentation archive.
-Stage 6 removed the inherited framework/build stack. Stage 7 migrated the then-current 25 presentation sources to the canonical `pure-v1` content format and removed the historical HTML compatibility parser and frozen metadata registry. The rescued Christmas LEGO Movie Sets quiz brings the current canonical corpus to 26 decks.
+**Production:** https://art.digiguru.co.uk/presentation/
-## Architecture
+This repository owns the presentation content and the shared **Pure** Reveal.js runtime. It does **not** directly host the production `/presentation/` site. Production is assembled and deployed by [`digiguru/digiguru.github.io`](https://github.com/digiguru/digiguru.github.io), which checks out an exact commit from this repository and exports the presentation build into its GitHub Pages artifact.
-- Root `*.html` files are canonical `pure-v1` presentation sources.
-- `pure/src/` contains the shared runtime and UI.
-- `pure/src/presentation-runtime/stack-backgrounds.js` preserves shared backgrounds declared on vertical stacks while respecting explicit child backgrounds.
-- `pure/build/deck-source.mjs` reads canonical content/configuration.
-- `pure/build/audit-sources.mjs` rejects historical runtime wiring and checks the corpus capability/theme footprint.
-- `pure/deck.html` is the shared HTML shell for every built deck.
-- `scripts/presentations.mjs` discovers sources, validates metadata and exports the built artifact.
-- `scripts/presentation-accessibility.mjs` validates accessibility.
+## What lives here
+
+- Root `*.html` files are the canonical `pure-v1` presentation sources.
+- `pure/src/` contains the shared Reveal.js runtime, UI and presentation behaviour.
+- `pure/build/deck-source.mjs` reads and validates canonical presentation source.
+- `pure/deck.html` is the shared HTML shell used by every built deck.
+- `scripts/presentations.mjs` discovers presentations, validates metadata and exports the built site.
+- `scripts/presentation-accessibility.mjs` validates presentation accessibility.
- `scripts/smoke-presentations.mjs` browser-tests the exported website artifact.
-- `pure/package.json` and `pure/package-lock.json` define the npm dependency graph.
-- `pure/dist/` is generated output; never edit it manually.
+- `pure/package.json` and `pure/package-lock.json` define the runtime/build dependency graph.
+- `pure/dist/` is generated output. Never edit it manually.
-Presentation sources contain content and declarative configuration, not their own runtime.
+Presentation source files contain content and declarative configuration; they do not own their own Reveal runtime.
-## Setup and validation
+## Local development
-Use the Node version pinned by `.node-version`.
+Use the Node version pinned by `.node-version` (the repository currently requires Node `>=24.11.0 <25`).
-Start the development server with:
+Start the development server:
```bash
npm start
@@ -32,14 +32,20 @@ npm start
`npm start` performs a deterministic `npm ci --prefix pure` from the committed lockfile before starting Vite.
-For a fresh production build:
+For a clean production build:
```bash
npm run pure:install
npm run build
```
-Useful checks:
+The build output is written to:
+
+```text
+pure/dist/
+```
+
+Useful validation commands:
```bash
npm test
@@ -51,11 +57,160 @@ npm run pure:smoke
npm run presentations:smoke
```
-`npm run build` produces the static product in `pure/dist/` and assumes the locked Pure dependencies are already installed. `npm test`, `npm start` and `npm run pure:check` are self-contained and install them when needed.
+`npm run build` assumes the locked Pure dependencies are already installed. `npm test`, `npm start` and `npm run pure:check` install them when needed.
+
+## How production deployment works
+
+Production is deliberately a **two-repository build**.
+
+```text
+presentation/master
+ │
+ │ push
+ ▼
+GitHub Actions: digiguru/presentation/.github/workflows/js.yml
+ │
+ ├─ Pure validation / audit / browser smoke
+ ├─ tooling + metadata + accessibility + lint
+ ├─ npm run build
+ └─ dispatch-website
+ │
+ │ workflow_dispatch with exact presentation SHA
+ ▼
+digiguru/digiguru.github.io/.github/workflows/jekyll.yml
+ │
+ ├─ checkout digiguru.github.io
+ ├─ checkout digiguru/presentation at that exact SHA
+ ├─ generate presentation metadata
+ ├─ build Jekyll site
+ ├─ export Pure into _site/presentation
+ ├─ stamp release metadata
+ ├─ validate generated site/assets
+ ├─ deploy GitHub Pages
+ └─ smoke-test deployed site
+ │
+ ▼
+https://art.digiguru.co.uk/presentation/
+```
+
+### 1. Presentation CI
+
+On pull requests and pushes to `master`, `.github/workflows/js.yml` runs the presentation checks and production build.
+
+The build job installs the locked Pure dependency graph once, then validates workflow syntax, custom tooling, presentation metadata, accessibility and lint before running `npm run build` and the exported-site Chrome smoke tests.
+
+A separate `pure` job runs the Pure checks, dependency audit and direct Pure Chrome smoke tests.
+
+### 2. Cross-repository dispatch
+
+After a push to `master`, `dispatch-website` calls the `digiguru/digiguru.github.io` `jekyll.yml` workflow using the `WEBSITE_DISPATCH_TOKEN` secret.
+
+The dispatch passes the **full 40-character presentation commit SHA** that triggered the build. This is important: the website does not need to guess which presentation revision should be deployed.
+
+### 3. Website build
+
+The website workflow checks out this repository at the supplied SHA under `presentation-source/`, generates the presentation manifest, builds the main Jekyll site, then runs:
+
+```bash
+node presentation-source/scripts/presentations.mjs --export _site/presentation
+```
+
+That places the Pure output under the website's `/presentation/` path. Release metadata records both the website SHA and the presentation SHA so a deployed build can be traced back to both repositories.
+
+### 4. GitHub Pages deploy
+
+The completed website artifact is deployed by `actions/deploy-pages`. The website repo then runs a deployed-site smoke test.
+
+The canonical public URL is:
+
+**https://art.digiguru.co.uk/presentation/**
+
+## Vercel previews
+
+Vercel is useful for previews, but it is **not the production hosting path** for `art.digiguru.co.uk/presentation/`.
+
+`vercel.json` uses the same deterministic Pure build:
+
+```text
+install: npm run pure:install
+build: npm run build
+output: pure/dist
+```
+
+Vercel therefore previews this repository directly, while production embeds the exported build inside the `digiguru.github.io` Pages artifact.
+
+## Key watch-outs
+
+### Do not enable GitHub Pages on this repository
+
+`digiguru/presentation` must not independently claim the production presentation path. Production Pages belongs to `digiguru/digiguru.github.io`.
+
+This previously caused routing conflicts where the old presentation Pages site intercepted `art.digiguru.co.uk/presentation/` instead of the website's exported Pure artifact.
+
+### The website dispatch secret is production infrastructure
+
+`WEBSITE_DISPATCH_TOKEN` must remain configured in this repository and must be allowed to dispatch the website's `jekyll.yml` workflow. If that secret is missing, expired or loses permission, presentation CI can build successfully but the production website will not be asked to rebuild.
+
+### The dispatch currently waits for `build`, not the separate `pure` job
+
+In `.github/workflows/js.yml`, `dispatch-website` currently has:
+
+```yaml
+needs: build
+```
+
+It does **not** currently depend on the separate `pure` job. A failing `pure` job therefore does not, by itself, prevent the website dispatch if `build` succeeds. If the intention is "every CI job must be green before production dispatch", change the dependency to include both jobs rather than assuming that is already enforced.
+
+### Website pushes can also consume `presentation/master`
+
+The website's `jekyll.yml` also runs for pushes to `digiguru.github.io/main`. When no `presentation_sha` input is supplied, it deliberately checks out `presentation/master`. This is useful for normal website releases, but it means the website repository is also capable of deploying the current presentation `master` independently of this repository's dispatch.
+
+### Keep source and runtime responsibilities separate
+
+Root presentation files are canonical `pure-v1` content. Shared behaviour belongs in `pure/`.
+
+Do not reintroduce:
+
+- deck-owned Reveal runtime bundles
+- local Reveal/plugin runtime links
+- executable deck-owned Reveal initialization
+- a second presentation registry
+- hand-edited files in `pure/dist/`
+
+`npm --prefix pure run audit:sources` guards against the historical runtime patterns that the Pure migration removed.
+
+### Preserve presentation metadata
+
+Every presentation must declare at least:
+
+```html
+
+
+
+
+```
+
+`presentation-attendance` is optional. Multiple `presentation-theme` tags are allowed.
+
+### Keep accessibility and background behaviour covered
+
+Every image requires an `alt` attribute.
+
+Backgrounds should remain declarative:
+
+```html
+
+```
+
+For vertical stacks, an outer-section background is inherited by children that do not explicitly declare their own background; explicit child backgrounds win. Unit and browser regression tests cover this behaviour.
+
+### Dependency changes must update the Pure lockfile
+
+Runtime/build npm dependencies live in `pure/package.json`. Any dependency change must update `pure/package-lock.json`, and CI/Vercel install with `npm ci --prefix pure`.
## Canonical presentation source format
-Every presentation declares metadata and content explicitly:
+A minimal source looks like this:
```html
@@ -79,52 +234,28 @@ Every presentation declares metadata and content explicitly:
```
-`presentation-name`, `presentation-version` and `presentation-date` are required. `presentation-attendance` is optional. Multiple `presentation-theme` tags are allowed.
-
Sources may contain inline `