Skip to content

Add manifest-driven screenshot capture tooling#1975

Merged
cderv merged 111 commits intomainfrom
worktree-screenshot-tool
Mar 27, 2026
Merged

Add manifest-driven screenshot capture tooling#1975
cderv merged 111 commits intomainfrom
worktree-screenshot-tool

Conversation

@cderv
Copy link
Copy Markdown
Member

@cderv cderv commented Mar 24, 2026

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:

  1. Define a screenshot in manifest.json — source page, viewport size, interactions (clicks, hovers), crop/clip regions, dark mode variant
  2. Run npm run capture — Playwright renders the page, executes the interactions, takes the screenshot, post-processes it (trim, crop, compress)
  3. Light and dark variants are produced automatically for every entry

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

  • Declarative manifest with JSON Schema validation (npm run validate) and IDE autocompletion
  • Dark mode capture via JS toggle (avoids CSS visibility issues at narrow viewports)
  • Spotlight effect — dims the page with an overlay while highlighting a specific element (used for repo-actions screenshot)
  • Content-aware trim — detects background color and removes blank edges uniformly
  • Clip union — screenshots the bounding box of multiple CSS selectors
  • Quarto profile support — same example project renders different configurations (e.g., floating vs anchored sidebar)
  • CI compression workflow (.github/workflows/optimize-images.yml) — runs oxipng on changed PNGs after merge

Example 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/:

npm install
npx playwright install chromium

Then:

  • npm run capture — capture all screenshots
  • npm run capture -- --name navbar-tools — capture a specific entry
  • npm run capture -- --dry-run — preview the plan
  • npm run capture -- --list — list manifest entries
  • npm run validate — validate manifest against schema

See tools/screenshots/SETUP.md for the full guide.

Claude Code integration

A /screenshot skill 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, and capture.js replays it mechanically.

cderv and others added 30 commits March 6, 2026 18:27
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).
cderv added 3 commits March 24, 2026 11:24
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.
@github-actions github-actions Bot temporarily deployed to pull request March 24, 2026 18:47 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

📝 Preview Deployment

🔍 Full site preview: https://deploy-preview-1975.quarto.org

🔄 Modified Documents

cderv added 2 commits March 24, 2026 12:13
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.
@github-actions github-actions Bot temporarily deployed to pull request March 24, 2026 19:25 Inactive
@github-actions
Copy link
Copy Markdown
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.
@github-actions github-actions Bot temporarily deployed to pull request March 24, 2026 21:15 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

📝 Preview Deployment

🔍 Full site preview: https://deploy-preview-1975.quarto.org

🔄 Modified Documents

🖼️ Pages with Updated Screenshots

@cderv cderv force-pushed the worktree-screenshot-tool branch from 364a338 to 67cd9b9 Compare March 26, 2026 20:24
@github-actions github-actions Bot temporarily deployed to pull request March 26, 2026 20:33 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

📝 Preview Deployment

🔍 Full site preview: https://deploy-preview-1975.quarto.org

🔄 Modified Documents

🖼️ Pages with Updated Screenshots

@cderv cderv force-pushed the worktree-screenshot-tool branch from 67cd9b9 to 2607f5e Compare March 26, 2026 20:38
@github-actions github-actions Bot temporarily deployed to pull request March 26, 2026 20:46 Inactive
@github-actions
Copy link
Copy Markdown
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.
@cderv cderv force-pushed the worktree-screenshot-tool branch from 2607f5e to cf8e5a4 Compare March 26, 2026 20:52
@github-actions github-actions Bot temporarily deployed to pull request March 26, 2026 21:00 Inactive
@github-actions
Copy link
Copy Markdown
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.
@github-actions github-actions Bot temporarily deployed to pull request March 26, 2026 21:13 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

📝 Preview Deployment

🔍 Full site preview: https://deploy-preview-1975.quarto.org

🔄 Modified Documents

🖼️ Pages with Updated Screenshots

@cderv cderv merged commit b184c08 into main Mar 27, 2026
4 checks passed
@cderv cderv deleted the worktree-screenshot-tool branch March 27, 2026 19:23
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)
@github-actions
Copy link
Copy Markdown
Contributor

Successfully created backport PR for prerelease:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant