Add full-site PDF export - #4
Conversation
`npm run export:pdf` renders every generated route to a single PDF. Run it after `npm run generate`. Output: 92 pages across 88 sections, ~40MB. Text stays selectable rather than being rasterized. Approach: serve dist/ statically, drive headless Chromium over each route with page.pdf(), merge with pdf-lib. Screen media is emulated so the capture stays faithful to the site as it renders in a browser; the PDF width is pinned to the 1440px viewport so nothing reflows, while leaving height unset lets long routes flow onto continuation pages. Route discovery reads dist/**/index.html rather than re-deriving routes from pages/, so dynamic /tour/_region/_video routes come along for free and new videos under content/tour/*/videos/ need no script changes. Routes are then ordered for reading rather than alphabetically, which would otherwise split the two tour regions. /learn is a single route hiding ~20 screens. Its quiz state is local component data on pages/learn.vue, with showFeedback and moduleComplete computed from currentIndex, so the walk sets moduleStarted and currentIndex directly instead of simulating correct answers. It also resets the module afterwards so the beforeRouteLeave confirm() cannot wedge the run. This is the one part coupled to component internals; it fails loudly if learn.vue's data shape changes. Capture waits are deliberate: document.fonts.ready for Typekit, explicit image and video-poster decoding (plyr initializes lazily in mounted(), so capturing early gave black rectangles), and a scroll pass to trip vue-lazyload, without which eight thumbnails on /tour/videos exported as blank placeholders. Verified: no blank pages, no untexted pages, video posters render real frames, all 18 quiz screens match the content files, and the fixed nav does not repeat on continuation pages. Claude-Session: https://claude.ai/code/session_01YYw1jCDqP22swZPmSqcUtn
The module hides content behind two kinds of interaction that a plain state walk missed. Learn screens go from 18 to 39; the document from 92 to 113 pages. Answer reveals: showAnswer is computed as userRequestedAnswer && userAnsweredQuestion, both pre-declared per question by helpers/formatLearnQuestions.js and therefore reactive. Setting them exposes the correct answer in the input and, for non-matching questions, the explanation panel beside it. All 8 questions gain a revealed screen. Feedback panel reveals: the section-feedback components mount interactive widgets that start fully collapsed. Timeline, OverdoseStats and AddressingStigma step through a `sections` array via `sectionIndex`; StigmaImpacts switches on an `activeContent` key. Section 3's panel says "Click on the icons on the left to explore" and showed nothing at all before this. That is 13 further screens. Reveals are discovered by probing the live component tree rather than being hardcoded, so a feedback panel that grows a fifth step, or a new panel built on the same `sectionIndex` shape, needs no changes here. The one exception is StigmaImpacts: its keys exist only as literals in the template and cannot be read from component data, so they are listed in a constant tied back to that file by comment. walkLearn now drives the module with a capture callback instead of returning a precomputed state list, since reveal steps are only knowable once a screen is mounted. Verified: 113 pages, none blank or untexted, and no two consecutive learn screens are textually identical, so every reveal genuinely adds content. Claude-Session: https://claude.ai/code/session_01YYw1jCDqP22swZPmSqcUtn
Added: interactive reveals in the learn moduleThe module hid content behind two kinds of interaction the original state walk missed. Learn screens go from 18 → 39; the document from 92 → 113 pages. Answer reveals (+8). Feedback panel reveals (+13). The section-feedback components mount interactive widgets that start fully collapsed:
Section 3's panel is the clearest case — it reads "Click on the icons on the left to explore how stigma…" and rendered nothing below that before this change. Discovery is dynamic, not hardcoded. The walk probes the live component tree, so a panel that grows a fifth step — or a new panel built on the same
Verified: 113 pages, none blank or untexted, and no two consecutive learn screens are textually identical — so every reveal genuinely adds content rather than re-capturing the same view. |
npm run export:pdfrenders every generated route into a single PDF. Run it afternpm run generate.Output: 92 pages across 88 sections, ~40 MB. Text stays selectable rather than rasterized.
Approach
Serve
dist/statically → drive headless Chromium over each route withpage.pdf()→ merge withpdf-lib. Screen media is emulated so the capture stays faithful to the site as it renders in a browser. PDF width is pinned to the 1440px viewport so nothing reflows; leaving height unset lets long routes flow onto continuation pages.index.mjsroutes.mjslearn.mjsrender.mjsserver.mjsRoute discovery reads
dist/**/index.htmlrather than re-deriving routes frompages/, so the dynamic/tour/_region/_videoroutes come along for free and new videos undercontent/tour/*/videos/need no script changes. Routes are then ordered for reading rather than alphabetically, which would otherwise split the two tour regions.The
/learnwalk is the one interesting bit. It's a single route hiding ~20 screens, and the quiz state is local component data onpages/learn.vue— withshowFeedbackandmoduleCompletecomputed fromcurrentIndex. So the walk setsmoduleStartedandcurrentIndexdirectly instead of simulating correct answers, then resets the module afterwards so thebeforeRouteLeaveconfirm()can't wedge the run.Waits that turned out to matter
Each of these was a real defect caught during verification, not speculative hardening:
document.fonts.ready— otherwise Typekit hasn't resolved and you capture fallback fonts.mounted(), so there's a real async gap. Capturing early gave black rectangles.vue-lazyload, and nothing scrolls during an export. Without this, eight thumbnails on/tour/videosexported as blank grey placeholders.Verified
Sweep across all 92 pages: no blank pages, no untexted pages. Video pages render real frames with plyr controls. All 18 quiz screens match the content files exactly (6 sections → 9 questions, 9 feedback, plus objectives and completion). The fixed
<nav-bar />does not repeat on continuation pages — the artifact I'd flagged as likely didn't materialize, so no print stylesheet was needed.Notes
*.pdfadded to.gitignore; the 40 MB output is not committed.playwrightandpdf-libas devDependencies. Playwright needsnpx playwright install chromiumonce.learn.vueinternals by necessity. It throws a descriptive error rather than silently exporting the wrong screens if that data shape changes.https://claude.ai/code/session_01YYw1jCDqP22swZPmSqcUtn