Screenshots and video
How the screenshots embedded in the user docs are captured, and how the promo MP4s are built. Both flows are automated — you should never be hand-cropping a PNG or hand-editing a video timeline.
| Script | npm command | Output |
|---|---|---|
scripts/capture-screen-docs.mjs | npm run docs:screens | 36 PNGs under doc/screenshots/ |
scripts/build-promo-video.mjs | npm run video:promo | 2 MP4s under doc/screenshots/ (wide + vertical) |
Both scripts are zero-config relative to the repo — they read from the running Next dev server and write next to the source.
Prerequisites
Before running either script:
- Node 20+
npm installin the repo root (Playwright + ffmpeg-static are devDependencies)- For
docs:screensonly: the Next dev server must be running on:3010(npm run dev) - For admin screenshots specifically:
.envhas a valid staff JWT (POSTGREST_ADMIN_JWTorPOSTGREST_STAFF_JWT_UIS) that PostgREST verifiesJWT_SECRETmatches the same secret PostgREST uses- In development this Just Works; in any non-
NODE_ENV=developmentsetup, also setADMIN_BOOTSTRAP_SESSION_FROM_ENV=1so the script can hit/api/admin/bootstrap-session - PostgREST itself must be green —
npm run smoke:adminis the canonical pre-check
If admin bootstrap fails, the script writes rwg-adm-bootstrap-failed.png and stops without capturing the admin surfaces. The public-form screenshots still get captured before the admin section.
How npm run docs:screens works
scripts/capture-screen-docs.mjs is a single ~170-line Node script using Playwright + Chromium (headless). The flow:
- Resolve
APP_URLfrom env or default tohttp://localhost:3010. - Launch headless Chromium at viewport 1440 × 900, deviceScaleFactor 1.
- Public surfaces (no auth):
/→rwg-pub-home/thank-you→rwg-pub-thank-you/thank-you?complete-membership=true→rwg-pub-thank-you-membership- Walk the wizard by clicking the Norwegian
Nestebutton, capturing each step (rwg-pub-wizard-intro,wizard-activities,wizard-about,wizard-confirmation)
- Admin login screenshot:
/admin/login?manual=1→rwg-adm-login. The?manual=1ensures the picker doesn't auto-redirect. - Bootstrap an admin session via
GET /api/admin/bootstrap-session. If this redirects away from/admin/login, the session cookie is set and admin captures proceed. If not, the script logs a warning and exits early. - Admin static list: 20 fixed URL → file pairs in the
adminShotsarray (overview, registrations, activities, additional-activities, activity-categories, activity-settings, activities-text, text-content, print-manuscript, print-form, skemadata, app-log, staff, eval-questions, eval-options, languages, membership-statuses, membership-options, no-selected-options, activities-new). - Admin detail pages: navigate to a list page, find the first
/admin/<plural>/<numeric-id>link, follow it, capture. Used forrwg-adm-registration-detail,rwg-adm-activity-detail, plus six others in thedetailRunsarray.
Per-shot wait: 450 ms before screenshot (lets late paints settle). Full-page captures (fullPage: true).
Adding a new admin surface
- Add the route + screenshot id to the
adminShotsarray inscripts/capture-screen-docs.mjs:const adminShots = [["rwg-adm-overview", "/admin"],// …existing entries…["rwg-adm-my-new-surface", "/admin/my-new-surface"],] - Run
npm run docs:screens— the new PNG appears indoc/screenshots/. - Copy it to
website/static/img/screenshots/so the docs site can serve it. - Reference it in the relevant surface page + role hubs.
Adding a new detail page
Detail pages (the per-row edit screens) follow /admin/<plural>/<id>. To add one to the capture run:
const detailRuns = [
// …existing entries…
["/admin/my-new-list", "rwg-adm-my-new-detail", /^\/admin\/my-new-list\/\d+$/],
]
The script visits the list page, finds the first link matching the regex, navigates to it, and captures.
Refreshing after a UI change
npm run dev # in one terminal
npm run docs:screens # in another, once Next is ready
The script overwrites existing PNGs in place. Review the diff visually (most file viewers show a before/after for PNG changes) before committing — a CSS change can shift a 1-pixel border and produce a huge binary diff that you'd rather not commit.
After regenerating, mirror the changed PNGs into website/static/img/screenshots/. The two paths must stay in sync.
How npm run video:promo works
scripts/build-promo-video.mjs reads PNGs from doc/screenshots/ and an in-script SLIDES array that defines:
- An intro slide (no image, ~6 seconds, two lines of caption text)
- ~7 content slides, each with
image,sec(duration), andlines[](caption text)
It uses ffmpeg-static (bundled as a devDependency — no system ffmpeg required) to:
- Render each slide to an intermediate MP4 in
doc/screenshots/.video-build/(gitignored). - Concatenate the slides per format.
- Produce two final outputs in
doc/screenshots/:railway-promo-1920-wide.mp4— 1920 × 1080, landscape, desktop/embedrailway-promo-1080x1920-vertical.mp4— 1080 × 1920, portrait, mobile-vertical
The intermediate .video-build/ directory holds the per-slide MP4s, the concat manifest (merge.txt, images.concat), and captions (captions.srt). It's safe to delete after the build:
rm -rf doc/screenshots/.video-build
Captions are English — the videos are for cross-org sharing, not local volunteers. The volunteer-facing UI in the screenshots stays Norwegian.
Refreshing the videos
Whenever the screenshots change in a way that affects the promo (a wizard step gets a new layout, a new public-form screen lands), regenerate:
npm run video:promo
The two MP4s overwrite in place. Mirror them into website/static/img/promo/ so the docs site embeds the fresh version.
Editing the script (slide order, durations, copy)
The SLIDES array in scripts/build-promo-video.mjs is the source of truth for narrative order. Adjust sec for pacing, lines for copy. The intro slide can be replaced with a different background image — pass an image instead of null and remove the special-case rendering.
Both SLIDE_SEC (3.6s) and INTRO_SEC (6s) are constants near the top of the file.
Troubleshooting
[docs:screens] Admin bootstrap failed
The session-bootstrap endpoint either returned an error or didn't set a valid cookie. Causes, in order of likelihood:
- PostgREST is unreachable or its
PGRST_JWT_SECRETis unbound — runnpm run smoke:adminto confirm. If it returns500 PGRST300 "Server lacks JWT secret"or401 role "railway_web_anon" does not exist, see INVESTIGATE PostgREST admin connection. JWT_SECRETin.envdoesn't match what PostgREST verifies — the staff JWT won't validate, bootstrap rejects it.ADMIN_BOOTSTRAP_SESSION_FROM_ENV=1is needed — if you're running against a non-dev server, this env flag is required to enable the bootstrap endpoint.
Wrong port
Both scripts default to http://localhost:3010. Override with APP_URL:
APP_URL=http://localhost:3000 npm run docs:screens
Missing ffmpeg-static binary
npm install didn't fetch the binary for your platform. Reinstall:
rm -rf node_modules/ffmpeg-static
npm install ffmpeg-static
Playwright can't find a browser
npx playwright install chromium
(Should run automatically on first npm install; rerun if it didn't.)
File-layout reference
doc/screenshots/ # source-of-truth (capture output)
├── README.md # short-form usage note
├── rwg-{adm,pub}-<surface>.png # 36 files
├── railway-promo-1920-wide.mp4 # 2 files
├── railway-promo-1080x1920-vertical.mp4
└── .video-build/ # gitignored ffmpeg intermediates
website/static/img/screenshots/ # served by Docusaurus
└── rwg-{adm,pub}-<surface>.png # mirror of source PNGs
website/static/img/promo/ # served by Docusaurus
├── railway-promo-1920-wide.mp4 # mirror of source MP4s
└── railway-promo-1080x1920-vertical.mp4
The duplication (source + static mirror) is deliberate: doc/screenshots/ is the build output; website/static/img/ is the published asset. The mirror step is manual today; if it becomes painful, automate it in the npm script.
Related
- Writing user docs — how to embed these screenshots in the user-doc tree
- Documentation — Docusaurus site mechanics
scripts/capture-screen-docs.mjsandscripts/build-promo-video.mjs— the scripts themselves