ontoref-outreach/scripts/decks/gen-deck-pages.nu

126 lines
5.6 KiB
Text
Raw Normal View History

decks: the slides stop being pixels — Slidev's own render, captured and re-homed The published deck was a hand-written carousel of exported PNGs: the words of every slide were an IMAGE of words. Nothing indexed them, no glossary tooltip could reach them, no screen reader read them, and the page drifted from the source the moment a slide changed. The deck is NOT re-parsed. Slidev's markdown is not standard markdown, and its meaning does not live in the markdown anyway — it lives in a compiled bundle of UnoCSS utilities, theme rules and <style scoped> blocks. Re-implementing that is re-implementing a renderer, in any language. So Slidev renders (its own /print route, the one `slidev export` drives) and we capture what it produced: the DOM, and the CSS the browser actually applied. Measured on the served page: 0 → 2152 indexable words, 16/16 slides reachable, 0 third-party origins added, 0 broken references. Twelve traps, none of which raised an error: · The CSS must be asked of the BROWSER (17 stylesheets; index.html links 2). · Collapsing :root/html/body imports the SPA's viewport cage — it clipped the deck at 900px and swallowed 14 of 16 slides, silently. · UnoCSS's preflight, scoped, kills the glossary underline it was supposed to enable. · :not() inherits its argument's specificity — the boundary guard grew with every exemption until it reverted the slide scaling. :not(:where(…)) contributes zero. · scale(calc(100cqw / 980)) is invalid CSS and is dropped in silence; on a phone the box was 328px while the content stayed 980px and two thirds of every slide was cut. · The site's markdown renderer destroys an inline <script>: curly quotes, injected <p>, &amp;&amp;. CSS and JS go to external files — correctness, not optimisation. · The 980x552 frame CUT 12 of 16 slides (Slidev cuts them too; nobody had seen it, because the slides were PNGs of themselves). · transform: translateY(0) — an IDENTITY transform — makes <main> the containing block for position:fixed, and the zoom modal landed at top = -221px. Two views over one DOM (slider default, stacked reads with Ctrl+F), an index derived from each slide's own H1, a sticky rail, zoom, and Slidev's own copy buttons revived — they shipped in the captured DOM at 0x0, unclickable by any human. Twelve gates (A–L), and IN THE CHAIN: `just decks` in sync-full, `decks-check` in `check` (on disk), `decks-served` in `check-live` (the page). A generator outside the chain is a generator that does not run.
2026-07-14 19:41:51 +01:00
#!/usr/bin/env nu
# Project the Slidev decks onto the site as real HTML — text, not pixels.
#
# The site's deck pages used to be a hand-written carousel of exported PNGs: the words of
# every slide were an IMAGE of words. Nothing indexed them, no glossary tooltip could
# reach them, no screen reader read them, nobody could select or translate them, and the
# page drifted from the source the moment a slide changed.
#
# The deck is NOT re-parsed. Slidev's markdown is not standard markdown, and its meaning
# does not live in the markdown anyway — it lives in a compiled bundle of UnoCSS
# utilities, theme rules and <style scoped> blocks. Re-implementing that is re-implementing
# a renderer. So Slidev renders, and we capture what it produced (its /print route, the
# same one `slidev export` drives) and re-home it in the site.
#
# Usage:
# nu scripts/decks/gen-deck-pages.nu # build + project + gate
# nu scripts/decks/gen-deck-pages.nu --dry-run # project nothing, report
# nu scripts/decks/gen-deck-pages.nu check --url <page> # gate a served page
def outreach [] { $env.FILE_PWD | path dirname | path dirname }
def presentation [] { [(outreach) "presentation"] | path join }
2026-07-17 01:00:32 +01:00
def manifest [] {
let f = [(outreach) "scripts" "decks" "decks.ncl"] | path join
^nickel export --format json $f | from json
}
2026-07-17 01:00:53 +01:00
# The deck→artifact keys, from lib/variants.mjs — the one authority on the rule. This file used to
# mirror it by hand ("the variant key mirrors lib/project.mjs"), and a mirror is a copy that drifts:
# a THIRD copy in site/justfile never learned that a bilingual deck is one source per language, and
# gated an asset dir that does not exist. Ask, do not re-derive.
def variants [] {
let script = [(outreach) "scripts" "decks" "lib" "variants.mjs"] | path join
^node $script | from json
}
2026-07-17 01:00:32 +01:00
# ONE dist PER DECK. `slidev build` builds a single entry, so a shared dist/ means the second
# deck silently captures the FIRST one's slides — the projection would be wrong and nothing
# would say so. The deck's own build dir is what makes this a pipeline instead of a one-off.
#
# --force rebuilds even if the dist exists: the whole point is that the page follows the source,
# and a stale dist is a page that quietly stopped following it.
def ensure-build [id: string, source: string, --force] {
let dest = [(presentation) "builds" $id] | path join
let built = [$dest "index.html"] | path join
let src = [(presentation) $source] | path join
# Rebuild when the SOURCE is newer than the build. Without this, editing a slide and running
# `just decks` silently captured the stale build — the whole point of the projection is that the
# page follows the source, so a build older than its source is a page that stopped following it.
# --force rebuilds regardless (e.g. after a Slidev/theme upgrade the source mtime did not move).
let stale = if ($built | path exists) {
(($src | path exists) and (($src | path expand | path type) == "file")
and (((ls $src).0.modified) > ((ls $built).0.modified)))
} else { true }
if not $force and not $stale { return }
print $"(ansi cyan)slidev build ($source) → builds/($id)(ansi reset)"
cd (presentation)
# `slidev build` IGNORES --output (it is an `export` flag; on `build` it is accepted and does
# nothing). Measured: it wrote to dist/ in both flag positions. So build where it insists, then
# move. Fighting the CLI here would be fragile; moving a directory is not.
rm -rf dist
^./node_modules/.bin/slidev build $source
if not ("dist/index.html" | path exists) {
error make { msg: $"slidev build no produjo dist/ para ($source)" }
decks: the slides stop being pixels — Slidev's own render, captured and re-homed The published deck was a hand-written carousel of exported PNGs: the words of every slide were an IMAGE of words. Nothing indexed them, no glossary tooltip could reach them, no screen reader read them, and the page drifted from the source the moment a slide changed. The deck is NOT re-parsed. Slidev's markdown is not standard markdown, and its meaning does not live in the markdown anyway — it lives in a compiled bundle of UnoCSS utilities, theme rules and <style scoped> blocks. Re-implementing that is re-implementing a renderer, in any language. So Slidev renders (its own /print route, the one `slidev export` drives) and we capture what it produced: the DOM, and the CSS the browser actually applied. Measured on the served page: 0 → 2152 indexable words, 16/16 slides reachable, 0 third-party origins added, 0 broken references. Twelve traps, none of which raised an error: · The CSS must be asked of the BROWSER (17 stylesheets; index.html links 2). · Collapsing :root/html/body imports the SPA's viewport cage — it clipped the deck at 900px and swallowed 14 of 16 slides, silently. · UnoCSS's preflight, scoped, kills the glossary underline it was supposed to enable. · :not() inherits its argument's specificity — the boundary guard grew with every exemption until it reverted the slide scaling. :not(:where(…)) contributes zero. · scale(calc(100cqw / 980)) is invalid CSS and is dropped in silence; on a phone the box was 328px while the content stayed 980px and two thirds of every slide was cut. · The site's markdown renderer destroys an inline <script>: curly quotes, injected <p>, &amp;&amp;. CSS and JS go to external files — correctness, not optimisation. · The 980x552 frame CUT 12 of 16 slides (Slidev cuts them too; nobody had seen it, because the slides were PNGs of themselves). · transform: translateY(0) — an IDENTITY transform — makes <main> the containing block for position:fixed, and the zoom modal landed at top = -221px. Two views over one DOM (slider default, stacked reads with Ctrl+F), an index derived from each slide's own H1, a sticky rail, zoom, and Slidev's own copy buttons revived — they shipped in the captured DOM at 0x0, unclickable by any human. Twelve gates (A–L), and IN THE CHAIN: `just decks` in sync-full, `decks-check` in `check` (on disk), `decks-served` in `check-live` (the page). A generator outside the chain is a generator that does not run.
2026-07-14 19:41:51 +01:00
}
2026-07-17 01:00:32 +01:00
mkdir builds
rm -rf $dest
mv dist $dest
decks: the slides stop being pixels — Slidev's own render, captured and re-homed The published deck was a hand-written carousel of exported PNGs: the words of every slide were an IMAGE of words. Nothing indexed them, no glossary tooltip could reach them, no screen reader read them, and the page drifted from the source the moment a slide changed. The deck is NOT re-parsed. Slidev's markdown is not standard markdown, and its meaning does not live in the markdown anyway — it lives in a compiled bundle of UnoCSS utilities, theme rules and <style scoped> blocks. Re-implementing that is re-implementing a renderer, in any language. So Slidev renders (its own /print route, the one `slidev export` drives) and we capture what it produced: the DOM, and the CSS the browser actually applied. Measured on the served page: 0 → 2152 indexable words, 16/16 slides reachable, 0 third-party origins added, 0 broken references. Twelve traps, none of which raised an error: · The CSS must be asked of the BROWSER (17 stylesheets; index.html links 2). · Collapsing :root/html/body imports the SPA's viewport cage — it clipped the deck at 900px and swallowed 14 of 16 slides, silently. · UnoCSS's preflight, scoped, kills the glossary underline it was supposed to enable. · :not() inherits its argument's specificity — the boundary guard grew with every exemption until it reverted the slide scaling. :not(:where(…)) contributes zero. · scale(calc(100cqw / 980)) is invalid CSS and is dropped in silence; on a phone the box was 328px while the content stayed 980px and two thirds of every slide was cut. · The site's markdown renderer destroys an inline <script>: curly quotes, injected <p>, &amp;&amp;. CSS and JS go to external files — correctness, not optimisation. · The 980x552 frame CUT 12 of 16 slides (Slidev cuts them too; nobody had seen it, because the slides were PNGs of themselves). · transform: translateY(0) — an IDENTITY transform — makes <main> the containing block for position:fixed, and the zoom modal landed at top = -221px. Two views over one DOM (slider default, stacked reads with Ctrl+F), an index derived from each slide's own H1, a sticky rail, zoom, and Slidev's own copy buttons revived — they shipped in the captured DOM at 0x0, unclickable by any human. Twelve gates (A–L), and IN THE CHAIN: `just decks` in sync-full, `decks-check` in `check` (on disk), `decks-served` in `check-live` (the page). A generator outside the chain is a generator that does not run.
2026-07-14 19:41:51 +01:00
}
2026-07-17 01:00:32 +01:00
def main [--dry-run, --force] {
# One build per SOURCE, not per deck: a bilingual deck is two sources, and sharing a build is
2026-07-17 01:00:53 +01:00
# how an English URL ends up serving Spanish slides.
for v in (variants) {
ensure-build $v.key $v.source --force=$force
2026-07-17 01:00:32 +01:00
}
decks: the slides stop being pixels — Slidev's own render, captured and re-homed The published deck was a hand-written carousel of exported PNGs: the words of every slide were an IMAGE of words. Nothing indexed them, no glossary tooltip could reach them, no screen reader read them, and the page drifted from the source the moment a slide changed. The deck is NOT re-parsed. Slidev's markdown is not standard markdown, and its meaning does not live in the markdown anyway — it lives in a compiled bundle of UnoCSS utilities, theme rules and <style scoped> blocks. Re-implementing that is re-implementing a renderer, in any language. So Slidev renders (its own /print route, the one `slidev export` drives) and we capture what it produced: the DOM, and the CSS the browser actually applied. Measured on the served page: 0 → 2152 indexable words, 16/16 slides reachable, 0 third-party origins added, 0 broken references. Twelve traps, none of which raised an error: · The CSS must be asked of the BROWSER (17 stylesheets; index.html links 2). · Collapsing :root/html/body imports the SPA's viewport cage — it clipped the deck at 900px and swallowed 14 of 16 slides, silently. · UnoCSS's preflight, scoped, kills the glossary underline it was supposed to enable. · :not() inherits its argument's specificity — the boundary guard grew with every exemption until it reverted the slide scaling. :not(:where(…)) contributes zero. · scale(calc(100cqw / 980)) is invalid CSS and is dropped in silence; on a phone the box was 328px while the content stayed 980px and two thirds of every slide was cut. · The site's markdown renderer destroys an inline <script>: curly quotes, injected <p>, &amp;&amp;. CSS and JS go to external files — correctness, not optimisation. · The 980x552 frame CUT 12 of 16 slides (Slidev cuts them too; nobody had seen it, because the slides were PNGs of themselves). · transform: translateY(0) — an IDENTITY transform — makes <main> the containing block for position:fixed, and the zoom modal landed at top = -221px. Two views over one DOM (slider default, stacked reads with Ctrl+F), an index derived from each slide's own H1, a sticky rail, zoom, and Slidev's own copy buttons revived — they shipped in the captured DOM at 0x0, unclickable by any human. Twelve gates (A–L), and IN THE CHAIN: `just decks` in sync-full, `decks-check` in `check` (on disk), `decks-served` in `check-live` (the page). A generator outside the chain is a generator that does not run.
2026-07-14 19:41:51 +01:00
let flags = if $dry_run { ["--dry-run"] } else { [] }
let script = [(outreach) "scripts" "decks" "lib" "project.mjs"] | path join
cd (presentation)
let out = (^node $script ...$flags | complete)
if $out.exit_code != 0 {
print $out.stdout
print $"(ansi red)($out.stderr)(ansi reset)"
error make { msg: "la proyección del deck falló" }
}
let report = ($out.stdout | from json)
print ($report.decks | select id slides css assets fuentes | table)
if $dry_run { return }
for d in $report.decks {
check-artifact $d.id
}
print $"(ansi green)decks proyectados.(ansi reset) Verifica la PÁGINA servida con: nu scripts/decks/gen-deck-pages.nu check --url <url>"
}
# Gate the CSS artifact: containment, fidelity, no inbound leak, glossary decoration.
def check-artifact [id: string] {
let script = [(outreach) "scripts" "decks" "gates" "artifact.mjs"] | path join
cd (presentation)
let r = (^node $script $id | complete)
print $r.stdout
if $r.exit_code != 0 { error make { msg: $"puertas del artefacto FALLAN para ($id)" } }
}
# Gate the page as served. Verifying the file proves nothing: the site's content pipeline
# sits between this generator and the reader.
def "main check" [--url: string] {
if ($url | is-empty) { error make { msg: "check necesita --url <url-de-la-pagina>" } }
let script = [(outreach) "scripts" "decks" "gates" "served.mjs"] | path join
cd (presentation)
let r = (^node $script $url | complete)
print $r.stdout
if $r.exit_code != 0 {
print $"(ansi red)($r.stderr)(ansi reset)"
error make { msg: "puertas de la página servida FALLAN" }
}
print $"(ansi green)puertas de la página servida: OK(ansi reset)"
}