Add manifest-driven screenshot capture tooling#1975
Merged
Conversation
Thanks @cwickham ! Co-authored-by: Charlotte Wickham <charlotte.wickham@posit.co>
Generated by capture.js from manifest entries: - about-jolla: light + dark variants (1200x650 clip) - navbar-tools: light + dark variants (1440x400 clip with dropdown)
- website-about.qmd: add .include-dark, update alt text twitter → Bluesky - website-blog.qmd: add .include-dark, update alt text twitter → Bluesky - website-navigation.qmd: add .include-dark, update alt text Twitter → Bluesky
Sub-plans for Claude-first screenshot workflow using playwright-cli. Two example Quarto projects (about-pages/jolla, navbar-tools) validated interactively — render, serve, screenshot, click interaction all working. Learnings documented in 08-walkthrough-learnings.md.
Four zero-dependency Node.js scripts for the screenshot pipeline: - list.js: reads manifest.json, formats entries for Claude - render.js: quarto render wrapper (QUARTO_CMD env var override) - serve.js: static file server for rendered sites - compress.js: oxipng wrapper (skips gracefully if not installed) package.json declares type:module and npm script aliases.
Defines about-jolla and navbar-tools screenshots with source projects, viewport sizes, interaction steps, and output paths. Includes default cleanup rules for prerelease callout removal.
Reads manifest.json and replays screenshot captures using playwright-cli. Groups screenshots by source project, handles render/serve/capture/compress pipeline. Uses run-code with CSS selectors to bypass ref resolution. Supports --name filtering, --dry-run, --no-compress, and --list flags.
- screenshot.md: orchestrator command that injects manifest data and visual rules via ! preprocessing, handles create/update workflows - screenshot-capture.md: Sonnet agent for browser operations using playwright-cli with -s=screenshot session flag
CLAUDE.md: visual rules and quick workflow reference for Claude sessions. SETUP.md: colleague setup guide with three usage options (automated replay, interactive with Claude Code, manual with playwright-cli).
- scripts/open.js: opens files with OS default app (for human terminal use) - capture.js: --verify flag opens each image after capture for review - /screenshot command: shows file path and asks user to confirm visually - package.json: add npm run open script
Replace playwright-cli subprocess calls with direct Playwright API (chromium.launch, page.goto, page.evaluate, page.screenshot, etc.). The old approach spawned playwright-cli via execSync which goes through cmd.exe on Windows, corrupting complex JS strings with quotes and newlines — producing wrong screenshot dimensions. Also fix manifest clip selectors: use .quarto-navbar-tools instead of .navbar for navbar-tools, and add .quarto-about-jolla clip for about-jolla to match the old cropped output. Dependencies: add playwright and open npm packages. Rewrite scripts/open.js to use the open package.
capture.js now automatically generates -dark variants for screenshots with "dark": true in manifest. Dark mode is activated by clicking the Quarto color scheme toggle (.quarto-color-scheme-toggle). For clip screenshots, the clip region is computed as the union of light and dark bounding boxes so both variants have identical dimensions. - Add light/dark theme (cosmo/darkly) to example _quarto.yml files - Add defaults.dark config to manifest (toggle selector, ready check) - Update all docs: CLAUDE.md, SETUP.md, capture agent, screenshot command - Tighten about-jolla viewport to 650px to reduce bottom whitespace
Documents the .qmd changes needed in PR #1815 to use the dark mode screenshots: add .include-dark class to image references, update alt text from Twitter to Bluesky.
This branch is tooling-only. Screenshot outputs will be regenerated on the PR branch using capture.js.
- serve.js: Add path traversal guard (reject requests outside root)
- capture.js: Detect early server exit in startServer
- capture.js: Warn when clip selectors partially match
- capture.js: Validate --name has a value, escape glob metacharacters
- capture.js: Use manifest readyLight selector instead of string replace
- capture.js: --verify opens dark variant too, with { wait: true }
- list.js: Validate --name has a value, escape glob metacharacters
- compress.js: Guard against division by zero on empty files
- manifest.json: Add readyLight to dark config
- screenshot.md: Add server shutdown step
- screenshot-capture.md: Read cleanup from manifest instead of hardcoding
- Move browser.close() into finally block so it runs on error too - Clear startServer timeout on resolve/reject to avoid dangling timer - Wrap dark mode capture sections in try/finally to ensure switchToLight runs even if interactions or clip computation fail
- capture.js: Catch server.kill() error when process already exited, preventing ESRCH from masking the real failure - render.js: Check project directory exists before invoking quarto, giving a clear "Directory not found" instead of a cryptic quarto error - CLAUDE.md: Document manifest path conventions (output = repo-relative, source.project = tools/screenshots-relative, doc.file = repo-relative)
- Reject --name with flag-like values (e.g. --name --verify) - Guarantee server teardown even if browser.close() throws - Kill orphan serve.js process on startup timeout - Narrow server.kill() catch to only suppress ESRCH - Check isDirectory in render.js, not just existence
- Add default case warning for unknown interaction actions in capture.js - Fix stale capture.mjs reference in plan doc (now capture.js)
Extract shotUrl() helper so url-type sources use the full source URL directly instead of appending page path to baseUrl.
shotUrl() was defaulting to 'index.html' when source.page was empty, changing first-shot behavior from baseUrl to baseUrl/index.html. Restore original behavior: navigate to bare baseUrl when no page specified.
source.profile field in manifest entries maps to `quarto render --profile`. Each profile creates a separate render group in capture.js so projects with multiple profiles get independent render cycles.
…enshots Refactor about-pages to use Quarto profiles (one shared about.qmd, five _quarto-<template>.yml configs). Add sidebar-tools and myblog example projects. Add manifest entries for about-trestles, about-solana, about-marquee, about-broadside, sidebar-tools, and myblog.
render.js: validate --profile has a value before using it. about.qmd: remove hardcoded template from frontmatter so profile configs control the template selection.
Sync 5 plan files with actual code: profile support, dark mode, clip-based capture, all 8 manifest entries, simplified example projects, and validated walkthrough checklist.
Rewrite CLAUDE.md around manifest-first mental model, reorder SETUP.md to match (quick start → interactive → manual), update sub-plan 09 workflow section. Add npm run help command, --help flag to capture.js, and no-args mode to compress.js (compresses all manifest outputs). Standardize on npm run commands throughout docs.
Add post-capture image processing via sharp: - `capture.trim` — content-aware whitespace removal (samples top-left pixel for background, trims matching edges, adds uniform padding back) - `capture.cropBottom` — remove N pixels from bottom edge - `capture.maxHeight` — cap image height, crop from bottom All three run in order via `postProcess()` between screenshot and compress. Trim works well for uniform backgrounds; crop handles cases where trim can't detect edges (vertical rules, multi-colored backgrounds). Also: remove per-profile _quarto-*.yml files in favor of shared _quarto.yml with separate .qmd files per about template. Add zoom 1.15 to about-page manifest entries. Remove cleanup eval (trim handles min-height blank space).
Replace 14 plan files (.claude/plans/screenshot/) with a single tools/screenshots/DECISIONS.md that preserves architectural decisions, rejected alternatives, and non-obvious gotchas. The plans were development artifacts; DECISIONS.md captures the lasting knowledge.
Extend the existing source-type error suppression pattern to also cover interaction step conditional requires, reducing noise when a click/hover/wait/scroll is missing its selector or eval is missing its script.
Quarto auto-discovers all .qmd files in the project tree regardless of navigation config. The 25 .qmd files in tools/screenshots/examples/ were being rendered as part of quarto.org. Add project.render with explicit includes and !tools/screenshots/ exclusion. Rename 404.md and license.md to .qmd so the render list only needs .qmd and .ipynb patterns. Update all references to license.md across _quarto.yml, about.qmd, faq, preview workflow, and reference docs.
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents |
Regenerated all 17 screenshots. navbar-tools images reflect updated Quarto rendering; remaining changes are minor byte-level differences from nondeterministic rendering.
When screenshots change in a PR, the deploy preview comment now includes a section linking to the doc pages that display those images. Uses jq to map changed PNGs to doc pages via manifest.json. Images not in the manifest are silently skipped.
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents🖼️ Pages with Updated Screenshots |
Dark variant PNGs (e.g., navbar-tools-dark.png) are now normalized to their base name before manifest lookup. Use [.] character class instead of \. for portable regex escaping across shells.
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents🖼️ Pages with Updated Screenshots |
364a338 to
67cd9b9
Compare
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents🖼️ Pages with Updated Screenshots |
67cd9b9 to
2607f5e
Compare
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents🖼️ Pages with Updated Screenshots |
The underscore prefix convention (_extensions, _freeze, _site) tells Quarto to skip the directory automatically, replacing the explicit render exclusion and the render: block (defaults suffice). Also removes an orphaned freeze cache for typst.qmd which had no executable cells.
2607f5e to
cf8e5a4
Compare
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents🖼️ Pages with Updated Screenshots |
Prevents manifest entries with ../ from writing outside the repo root.
Contributor
📝 Preview Deployment🔍 Full site preview: https://deploy-preview-1975.quarto.org 🔄 Modified Documents🖼️ Pages with Updated Screenshots |
github-actions Bot
pushed a commit
that referenced
this pull request
Mar 27, 2026
Add developer tooling for capturing and maintaining documentation screenshots on quarto.org using Playwright. Screenshots are defined declaratively in a JSON manifest, then captured automatically — no manual screenshotting needed. The tooling lives under _tools/screenshots/ and includes: - capture.js: Playwright-based replay engine that renders pages, executes interactions (clicks, hovers, toggles), and captures screenshots with automatic light/dark variants - manifest.json: Declarative definitions for 17 screenshots (34 images with dark variants) covering about pages, navbar configs, sidebar layouts, breadcrumbs, reader mode, and repo action links - JSON Schema validation for manifest entries - Content-aware trim, crop, and clip-union post-processing via sharp - Spotlight effect for highlighting specific elements - Quarto profile support for rendering different configurations - Example projects under examples/ as minimal screenshot sources (includes quarto-demo as a git subtree) - CI compression workflow (.github/workflows/optimize-images.yml) for oxipng optimization after merge - /screenshot Claude Code skill for interactive screenshot authoring Developed alongside PR #1815 (Twitter → Bluesky migration), which served as real-world validation — all 19 screenshots in that PR were produced by this tool. (cherry picked from commit b184c08)
Contributor
|
Successfully created backport PR for |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this?
A developer tool for capturing and maintaining the documentation screenshots on quarto.org. Screenshots are defined declaratively in a JSON manifest, then captured automatically using Playwright — no manual screenshotting needed.
The problem it solves: quarto-web has ~30 screenshots across docs pages (about pages, navigation, sidebars, etc.). When the site design changes or examples need updating, recapturing them manually is tedious and error-prone. This tool makes it repeatable and consistent.
How it works
Everything lives under
tools/screenshots/. The workflow:manifest.json— source page, viewport size, interactions (clicks, hovers), crop/clip regions, dark mode variantnpm run capture— Playwright renders the page, executes the interactions, takes the screenshot, post-processes it (trim, crop, compress)The manifest currently defines 17 screenshots (34 images with dark variants), covering about pages, navbar configurations, sidebar layouts, breadcrumbs, reader mode, and repo action links.
Key capabilities
npm run validate) and IDE autocompletion.github/workflows/optimize-images.yml) — runs oxipng on changed PNGs after mergeExample projects
Small Quarto projects under
examples/serve as screenshot sources. Each is minimal — just enough YAML and content to demonstrate a feature.examples/quarto-demo/is a git subtree of quarto-dev/quarto-demo used for sidebar and navigation screenshots.Live use case: PR #1815
This tooling was developed alongside #1815 (Twitter → Bluesky migration), which needed 19 documentation screenshots recaptured with updated social links. That PR served as the real-world validation — every screenshot in #1815 was produced by this tool and cherry-picked from this branch.
For contributors
Setup requires Node.js and Playwright. From
tools/screenshots/:Then:
npm run capture— capture all screenshotsnpm run capture -- --name navbar-tools— capture a specific entrynpm run capture -- --dry-run— preview the plannpm run capture -- --list— list manifest entriesnpm run validate— validate manifest against schemaSee
tools/screenshots/SETUP.mdfor the full guide.Claude Code integration
A
/screenshotskill guides Claude through the full workflow: creating example projects, iterating on the visual in headed Playwright, encoding the result into the manifest. This is the intended workflow for adding new screenshots — the skill handles the interactive exploration, the manifest captures the result, andcapture.jsreplays it mechanically.