Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 150 additions & 7 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
name: Deploy to GitHub Pages
name: Validate and deploy

# Publishes a copy of the deck with speaker notes stripped (see
# tools/strip-notes.py), so the live site never exposes the S speaker view
# notes. The repo keeps the notes; only the deployed artifact is stripped.
# Validation runs on every pull request and push to main; deployment happens
# only from main, and only after validation succeeds.
#
# build publishes an ALLOWLISTED, notes-free copy of the site (see
# tools/strip-notes.py) plus generated extras: a slides.pdf, a social-card.png
# and an offline.zip per deck. PDFs/cards are cached and regenerated only when
# the deck or the shared engine changed (content-hash check inside
# tools/export-pdf.mjs).
#
# All actions used here are first-party (actions/*), referenced by major
# version tag. If supply-chain policy tightens, pin each to a full commit SHA.
#
# One-time setup: repo Settings → Pages → Build and deployment →
# Source = "GitHub Actions" (instead of "Deploy from a branch").

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
Expand All @@ -19,24 +28,158 @@ permissions:

# Allow one concurrent deployment; let an in-progress run finish.
concurrency:
group: pages
group: pages-${{ github.ref }}
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- name: Static checks (audit, strip-notes tests, landing-page sync)
run: |
python3 tools/test_strip_notes.py
python3 tools/build-index.py --check
python3 tools/audit.py --strict
python3 tools/strip-notes.py /tmp/site-check
python3 tools/audit.py --site /tmp/site-check --strict

- uses: actions/setup-node@v6
with:
node-version: 22

- name: Install Playwright
run: |
npm install --no-save playwright
npx playwright install --with-deps chromium

- name: Browser checks (1280×720, 844×390, 390×844)
run: node tools/browser-check.mjs

# Lightweight visual regression: only when the shared engine changed on a
# PR — capture the component catalogue from the merge-base and from the
# head, compare with a loose tolerance, and upload diff images as artifacts
# on failure. Skipped when the catalogue deck itself changed in the same
# PR: the baseline then shows different *content*, so a pixel comparison
# can only measure the intended edit, not engine drift.
visual:
runs-on: ubuntu-latest
timeout-minutes: 25
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Check whether the shared engine changed (and the catalogue did not)
id: engine
run: |
BASE=$(git merge-base origin/${{ github.base_ref }} HEAD)
CHANGED=$(git diff --name-only "$BASE" HEAD)
engine=false; catalogue=false
echo "$CHANGED" | grep -qE '^shared/(theme\.css|deck\.js|reveal/)' && engine=true
echo "$CHANGED" | grep -qE '^talks/(_showcase|_template)/' && catalogue=true
if [ "$engine" = true ] && [ "$catalogue" = false ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
[ "$engine" = true ] && echo "::notice::visual regression skipped — the catalogue deck changed in this PR, so the merge-base screenshots show different content"
fi
echo "base=$BASE" >> "$GITHUB_OUTPUT"

- uses: actions/setup-node@v6
if: steps.engine.outputs.changed == 'true'
with:
node-version: 22

- name: Install Playwright
if: steps.engine.outputs.changed == 'true'
run: |
npm install --no-save playwright pixelmatch pngjs
npx playwright install --with-deps chromium

- name: Capture merge-base screenshots
if: steps.engine.outputs.changed == 'true'
run: |
git worktree add /tmp/base-tree ${{ steps.engine.outputs.base }}
node tools/browser-check.mjs --root /tmp/base-tree --decks _template --screenshots /tmp/shots-base || true

- name: Capture head screenshots
if: steps.engine.outputs.changed == 'true'
run: node tools/browser-check.mjs --decks _template --screenshots /tmp/shots-head

- name: Compare
if: steps.engine.outputs.changed == 'true'
run: node tools/visual-diff.mjs --before /tmp/shots-base --after /tmp/shots-head --out /tmp/visual-diff

- name: Upload diff images
if: failure()
uses: actions/upload-artifact@v6
with:
name: visual-diff
path: |
/tmp/visual-diff
/tmp/shots-base
/tmp/shots-head
if-no-files-found: ignore

build:
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/configure-pages@v6
- name: Strip speaker notes into ./_site
run: python3 tools/strip-notes.py _site

- name: Build the allowlisted, notes-free site
run: |
python3 tools/strip-notes.py _site
python3 tools/audit.py --site _site

- uses: actions/setup-node@v6
with:
node-version: 22

- name: Install Playwright
run: |
npm install --no-save playwright
npx playwright install --with-deps chromium

# PDFs + social cards are expensive: restore the last run's outputs and
# let export-pdf's content-hash check skip every unchanged deck.
- name: Restore PDF/social-card cache
uses: actions/cache@v5
with:
path: .extras-cache
key: deck-extras-${{ hashFiles('talks/**', 'shared/**') }}
restore-keys: deck-extras-

- name: Export PDFs and social cards
run: |
if [ -d .extras-cache ]; then cp -r .extras-cache/talks/. _site/talks/ 2>/dev/null || true; fi
node tools/export-pdf.mjs --root _site
mkdir -p .extras-cache
rm -rf .extras-cache/talks
(cd _site && find talks -maxdepth 2 \
\( -name slides.pdf -o -name social-card.png -o -name .extras-hash \) \
-exec install -D {} ../.extras-cache/{} \;)

- name: Build offline bundles
run: |
python3 tools/build-offline.py --root _site
find _site -name .extras-hash -delete

- uses: actions/upload-pages-artifact@v5
with:
path: _site

deploy:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ __pycache__/

# Claude Code (local preview dev-server config, not shared)
.claude/launch.json

# Local build previews + CI-installed browser-check dependencies
_site/
node_modules
package.json
package-lock.json

# CI cache for generated PDFs/social cards
.extras-cache/
57 changes: 57 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page not found — Slides · Frédérick Madore</title>
<meta name="robots" content="noindex">
<link rel="icon" href="/shared/logo-africamultiple.png">
<link rel="stylesheet" href="/shared/fonts/fonts.css">
<style>
:root {
--green: #009260; --green-deep: color-mix(in oklch, #009260, #06140d 26%);
--paper: oklch(0.991 0.001 250); --rule: oklch(0.165 0.014 258);
--ink: oklch(0.205 0.016 258); --ink-bold: oklch(0.150 0.016 262);
--ink-soft: oklch(0.405 0.018 258);
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; display: grid; place-items: center;
background: var(--paper); color: var(--ink);
font-family: "EB Garamond", Georgia, serif; font-size: 1.2rem; line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
main { max-width: 34rem; padding: 2rem; }
.eyebrow {
display: inline-flex; align-items: center; gap: 0.6em; margin: 0 0 1.2rem;
font-family: "Libre Franklin", system-ui, sans-serif; font-weight: 700;
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.18em; color: var(--ink-soft);
}
.eyebrow::before { content: ""; width: 1.6em; height: 3px; background: var(--green); border-radius: 1px; }
h1 {
font-family: "Libre Franklin", system-ui, sans-serif; font-weight: 800;
font-size: clamp(3rem, 10vw, 5rem); letter-spacing: -0.03em; line-height: 0.95;
color: var(--ink-bold); margin: 0 0 1rem;
}
p { font-style: italic; color: var(--ink-soft); margin: 0 0 1.6rem; }
a {
font-family: "Libre Franklin", system-ui, sans-serif; font-weight: 700;
font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--green-deep); text-decoration: none;
border-bottom: 3px solid var(--green);
padding-bottom: 0.15em;
}
a:hover { color: var(--green); }
hr { border: 0; height: 4px; background: var(--rule); margin: 0 0 1.6rem; }
</style>
</head>
<body>
<main>
<p class="eyebrow">Slides · Frédérick Madore</p>
<h1>Not found</h1>
<hr>
<p>This page doesn't exist — the talk may have moved or the address has a typo.</p>
<a href="/">Back to all talks</a>
</main>
</body>
</html>
52 changes: 52 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Licence

This repository mixes code, scholarly content and third-party material, each
with its own status.

## Code — MIT

The original code in this repository — the shared engine (`shared/deck.js`,
`shared/theme.css`), the landing page, the build and validation tooling under
`tools/`, and `serve-deck.py` — is released under the MIT License:

> Copyright (c) 2026 Frédérick Madore
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.

## Slide text and original figures — CC BY 4.0

The scholarly content of the presentations under `talks/` — slide text,
speaker notes and figures authored by the presenters — is licensed under
[Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/).
Please attribute **Frédérick Madore** (and the named co-presenter of the talk,
where applicable) with a link to <https://slides.frederickmadore.com/>.

## What these licences do NOT cover

- **Vendored software** (reveal.js, highlight.js) and the **fonts**
(EB Garamond, Libre Franklin) keep their own licences — see
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
- **Institutional logos** (Africa Multiple Cluster of Excellence, University
of Bayreuth, King's College London, ORCID, GitHub, LinkedIn, MCP) remain the
marks of their owners and are used for identification only.
- **Externally sourced images** in `talks/*/assets/` and `shared/assets/`
(digitised newspaper pages, archival photographs, screenshots of external
websites and reports) remain under their original rights; their sources are
credited on the slides where they appear. They are reproduced here for
scholarly presentation and are not covered by the CC BY grant above.
Loading
Loading