npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wp-docs-toolkit/screenshots

v0.4.0

Published

Reproducible WordPress UI screenshots for documentation: boots WordPress Playground from a blueprint and captures YAML-defined shots with Playwright.

Readme

@wp-docs-toolkit/screenshots

Reproducible WordPress UI screenshots for documentation. A YAML file per article says what to show. The engine boots WordPress Playground from a committed blueprint (pinned WordPress and PHP, a theme, demo content), drives the UI with Playwright, and writes WebP images plus a manifest into the documentation tree. When a WordPress release changes the UI, regenerate and review the diff instead of retaking screenshots by hand.

The design contract between the engine and a documentation project is in ARCHITECTURE.md.

Setup

Requirements: Node 22.19 to 25 (24 recommended; Playground's native module has no Node 26 binaries yet), about 1 GB of disk for dependencies and Chromium. No Docker, no database, no local WordPress install.

The engine is consumed from a project directory inside a documentation repo that holds shots.config.js, a Playground blueprint/ (or a reference to the bundled demo site), and YAML specs.

npm install -D @wp-docs-toolkit/screenshots
npx playwright install chromium            # once per machine
{
  "scripts": { "shots": "wp-screenshots" }
}

Then, in the project directory: npx wp-screenshots doctor. For a screenshot with nothing set up at all, or by prompt from Claude or ChatGPT, see @wp-docs-toolkit/mcp.

A project that would rather track a git tag than an npm version can use the repository as a submodule and depend on file:../toolkit/packages/screenshots; the repository README has the details.

Commands

Run from the project directory (or pass --config path/to/shots.config.js).

wp-screenshots run [--article <slug>] [--section <path>] [--suite <name>] [--shot <id>]
wp-screenshots check [filters] [--docker]   # render to a staging dir, diff against committed images, HTML report
wp-screenshots update                       # accept the images and manifest entries the last check staged
wp-screenshots lint [--fix]                 # specs, Markdown references, orphans; --fix rewrites alt text
wp-screenshots shoot --preset <name> [--<param> <value>] [--frame <frame>] [--out <file>]   # one image, no spec
wp-screenshots presets                      # the subjects a shot can be built from, with their parameters
wp-screenshots snippet --article <slug> [--from docs/path/file.md]
wp-screenshots preview [filters] [--width <px>] [--out <dir>]   # PNG copies of generated images for review
wp-screenshots dev --article <slug> --shot <id>   # headed browser, pauses in the Playwright Inspector
wp-screenshots doctor | list | vocab

shoot is the "just give me one" path: wp-screenshots shoot --preset site-editor --out site-editor.webp renders a single image from a preset without a spec file. It also needs no project: when no shots.config.js is found, shoot (and doctor, presets, vocab) fall back to the bundled demo site (@wp-docs-toolkit/demo-site) with working files in the temp directory, so it runs from any directory once Chromium is installed (npx playwright install chromium).

| Command | What it does | |---|---| | run | Renders the matching shots and writes an image only when its pixels changed, so a repeat run against the same WordPress leaves git clean. Updates the manifest. | | check | Renders to <workDir>/check/, compares with the committed images, writes <workDir>/report/index.html with before/after/diff, and exits 1 when anything changed or failed. Nothing committed is touched. | | update | Copies the staged images and their manifest entries into place. The release flow is check --docker, review, update, pull request. Never follow update with a plain run: a local render would overwrite canonical images with previews. | | lint | Validates every spec, checks that each generated image is referenced from the Markdown with the spec's alt text, and reports orphans and dangling references. Needs no browser. | | --docker | Re-runs the command inside the pinned Playwright image (dockerImage in the config) for a canonical render. |

Failures save the page state (screenshot, HTML, console log) under <workDir>/failures/.

Configuration: shots.config.js

Paths are relative to the config file.

export default {
	// Where the specs are: glob patterns relative to this file. Keep them next to their articles
	// (`docs/**/*.shots.yaml`) or in a mirrored tree (`shots/**/*.yaml`); the engine only needs the pattern.
	specs: [ 'docs/**/*.shots.yaml' ],
	shotsDir: 'shots',                      // used only when `specs` is not set: every YAML file under it
	outDir: '../docs/assets/screenshots',   // generated images and manifest.json
	docsDir: '../docs',                     // Markdown tree lint cross-checks
	docsManifest: '../docs/manifest.json',  // optional: article slugs must exist here
	blueprint: 'blueprint/blueprint.json',  // Playground blueprint (pins WordPress and PHP, logs in)
	suitesDir: 'blueprint/suites',          // optional per-suite blueprints: <suitesDir>/<name>/blueprint.json
	workDir: '.wp-screenshots',             // logs, failures, check staging, reports (gitignore it)
	wordpress: { version: '7.1.3', php: '8.3' }, // asserted after boot; the blueprint's preferredVersions wins
	port: 9400,
	format: 'webp',                         // or 'png'
	dockerImage: 'mcr.microsoft.com/playwright:v1.64.0-noble',
	style: {},                              // overrides for the tokens in src/style.js
};

Keep dockerImage, @playwright/test in this package, and your CI image at the same Playwright version.

Writing a shot

One YAML file per article, found through specs. The file name does not matter; article is the output folder (and, when docsManifest is set, must be a slug in it). Images land at <outDir>/<article>/<id>.webp.

From a preset

Most documentation screenshots are one of a few subjects. A preset names the subject and the engine supplies the scene, the steps, the capture target and the frame the house style settled on (see style/SCREENSHOTS.md in the toolkit). Any key set on the shot overrides what the preset filled in; shot steps run after the preset's.

article: image-block
shots:
  - id: canvas
    preset: block-canvas
    block: core/image
    alt: An Image block on the editor canvas, selected, showing a photo of a lake with a caption below it.
  - id: settings
    preset: block-settings
    block: core/image
    tab: settings
    alt: The Image block's Settings tab, with the Resolution setting outlined.
    capture:
      annotate:
        - outline: { control: Resolution }

| Preset | Parameters | Shows | |---|---|---| | block-canvas | block (required), post | A block on the canvas, selected, with its toolbar | | block-toolbar | block, post | The block's toolbar | | block-settings | block, post, tab (content, settings, styles), panel | The inspector on a tab | | site-editor | view | The site editor home or a view (templates, parts, patterns, navigation, pages, styles, template:…, part:…) | | post-editor | post | The whole post editor | | admin-screen | screen (required) | A wp-admin screen | | front-end | path | The public site |

Block presets open the demo site's showcase post block-<name> (block-image for core/image), which holds a representative instance of the block. post: new inserts the block into a fresh post instead; post: <slug> opens another post. wp-screenshots presets prints this table from the source of truth.

By hand

article: image-block
defaults:
  scene: new-post
  steps:
    - insertBlock: { name: core/paragraph, attributes: { content: Some lead text. } }
shots:
  - id: toolbar-replace
    alt: The Image block toolbar with the Replace button highlighted.
    steps:
      - insertBlock:
          name: core/image
          attributes: { id: "{{media.hero}}", url: "{{media.hero.url}}", alt: "{{media.hero.alt}}" }
    capture:
      target: block-toolbar
      pad: 24
      annotate:
        - outline: { button: Replace, within: block-toolbar }
  • Scenes set the starting state: new-post, new-page, post:<slug>, page:<slug>, admin:<path>, site-editor, site-editor:<route>, front, front:<path>.
  • Steps are verbs such as insertBlock, selectBlock, searchInserter, openInspector, openPanel, openBlockMenu, openToolbarDropdown, openListView, openOptionsMenu, click, hover, type, fill, select, press, wait, setViewport, setPreference, scrollTo, evaluate. wp-screenshots vocab lists them all; the table is in ARCHITECTURE.md.
  • Locator keys name elements the way a reader would: { button: Replace }, { control: Alternative text }, { menuitem: Edit as HTML }, { tab: Styles }, { text: Settings }, { role: searchbox, name: Search }, { selector: .css }. Names match the whole accessible name; add exact: false for a substring.
  • Capture targets are named regions from one registry: editor, selected-block, block-toolbar, inspector, inspector-panel:<name>, inserter, list-view, top-bar, popover, modal, admin-content, admin-screen, front, or a CSS selector. include: widens the crop (a shot's regions add to its preset's, so include: [ options-menu ] on block-canvas keeps the toolbar too), pad: adds space, fit: content trims a tall panel, frame: card puts a full-editor shot on a card.
  • Annotations anchor to elements, never to pixels: outline, arrow (with from: top | bottom | left | right | top-left | …), numbered badge, dim. WordPress blue with a white halo so they stay legible over the editor's own selection outline. They scale with the capture's displayed size (style.annotations, or capture.annotationScale per shot) so a full-editor shot's marks read as well as a tight crop's.
  • Frames put the crop on a stage with a canonical ratio: wide 16:9, standard 4:3, portrait 3:4, banner 3:1, auto (nearest to the content), none, or the content-sized card. The content is never scaled; the stage grows to the ratio. The manifest records the frame used.
  • Templating resolves against the running demo site by slug: {{media.<slug>}}, {{media.<slug>.url}}, {{media.<slug>.alt}}, {{post.<slug>}}, {{page.<slug>}}, {{site.url}}. The project's blueprint README is the contract for which slugs exist.
  • Alt text lives in the spec and is the source of truth; lint --fix copies it into the Markdown.
  • defaults.steps run before every shot that uses the default scene. A shot with another scene does not inherit them.
  • Typos fail fast. Unknown keys, verbs, scenes, targets and locator keys are rejected when the spec loads, with a "did you mean", so lint catches them in CI before anything boots.
  • Escape hatch: script: ./file.js replaces steps with a default export (ctx) => Promise<void> that receives { page, editor, admin, pageUtils, site, style }.

Setup: site state before the scene

setup creates what a shot needs on the site before its scene opens, over the REST API, so it is fast and the same every run: posts with particular blocks, notes on blocks, extra users, option values, an activated plugin. Give it a fixture name or a list of single-key operations:

shots:
  - id: list-view-long
    alt: List View on a long post.
    preset: post-editor
    post: setup-long-post
    setup: long-post
  - id: note-on-paragraph
    alt: A note on a paragraph.
    scene: post:pricing-draft
    setup:
      - user: { login: sam, display_name: Sam Ito, role: editor }
      - post: { slug: pricing-draft, title: Pricing, status: draft, content: "<!-- wp:paragraph -->\n<p>Plans start at $5.</p>\n<!-- /wp:paragraph -->" }
      - note: { post: pricing-draft, block: core/paragraph, text: Is this still right?, author: sam, replies: [ { text: Yes. } ] }

| Operation | Takes | |---|---| | post | slug, title?, content? (block markup), type?: post \| page \| wp_block, status?: draft \| publish (default publish), date?, author?: <login> | | note | post: <slug>, block: <name> or { name, nth } (nth counts from 0, nested blocks included), text, author?, date?, replies?: [ { text, author?, date? } ], resolved?: true | | user | login, role: administrator \| editor \| author \| contributor \| subscriber, display_name?, email? | | option | name, value: one of the core /wp/v2/settings keys (title, description, timezone, date_format, time_format, start_of_week, language, use_smilies, default_category, default_post_format, posts_per_page, show_on_front, page_on_front, page_for_posts, default_ping_status, default_comment_status, site_logo, site_icon); the value may be a {{page.<slug>}} template | | plugin | slug (the folder, akismet, or akismet/akismet), status?: active \| inactive (default active) |

Fixtures: long-post (post setup-long-post, about twenty varied blocks so List View scrolls), post-with-notes (post setup-post-with-notes with an open note and reply on the first paragraph and a resolved note on the third; the reviewer is the demo site's author-2, so avatar colours stay fixed), synced-pattern (synced pattern setup-synced-pattern), second-author (user setup-author, role author). The MCP vocabulary tool lists them. A fixture expands when the spec loads, so its content counts toward the shot's specHash.

  • Dates default to 2025-01-15 10:00 and fixture text is fixed, so the pixels do not drift. A post that gets notes is updated after creation, so its "Last edited" line in the Post panel reads "a few seconds ago"; other posts read "N years ago", which changes once a year.
  • Setup runs before the scene, so scene: post:<slug> and {{post.<slug>}} templates find what it created. A slug that already exists on the site is an error rather than a silent -2 suffix.
  • Cleanup is guaranteed per shot: after the shot, pass or fail, everything setup created is deleted (posts, patterns, notes, users, whose content is reassigned to the admin) and every option and plugin it changed is put back, newest first. The next shot starts from the same site.
  • defaults.setup applies to every shot, presets included; a shot's own setup replaces it (setup: [] opts out).
  • Errors name the operation and carry the REST message: setup[0] (user admin): REST POST /wp/v2/users failed (500): Sorry, that username already exists!.

Authoring loop: wp-screenshots dev --article x --shot y opens a visible browser, runs the scene and steps, and pauses in the Playwright Inspector. Its "Pick locator" tool shows the role and name to use in a step.

Determinism and the canonical render

Every run uses a 1440×900 viewport at 2x, light scheme, en-US, UTC, reduced motion, animations disabled, caret hidden and the pointer parked, against the pinned WordPress, PHP, theme and demo content. On one machine, repeat runs are byte-identical.

Fonts differ between macOS and Linux, so local renders are previews. The canonical render runs inside the pinned Playwright Docker image: --docker locally (Docker Desktop) or a CI job using the same image. The manifest marks each image canonical: true|false (set by WP_SCREENSHOTS_CANONICAL=1, which --docker exports). The host's ~/.wordpress-playground download cache is shared with the container, so after the first fetch of a WordPress version a render needs no network for WordPress itself.

Output

  • <outDir>/<article>/<id>.webp at 2x. Lossless WebP under style.webp.maxBytes (300 KB by default), lossy at style.webp.quality (90) above it. The manifest records which. format: 'png' switches everything to PNG.
  • <outDir>/manifest.json: per image, the alt text, article, WordPress, PHP and theme versions, spec and blueprint hashes, Playwright and Chromium versions, platform, canonical flag and pixel hash. An entry changes only when the pixels or the spec metadata change.

Regenerating at a WordPress release

  1. Bump preferredVersions.wp in the blueprint and wordpress.version in shots.config.js.
  2. wp-screenshots check --docker (or the project's CI job) and open <workDir>/report/index.html.
  3. Review the before/after pairs. A changed image is also a prompt to re-read the article text.
  4. wp-screenshots update, then open a pull request.

Environment contract

The engine needs from the project: a blueprint path, a free port, admin credentials (admin / password by default, overridable under user), and a login step in the blueprint so the browser arrives authenticated. If the demo site exposes a wp_screenshots: { wordpress, php, theme } object on GET /wp-json/ (a mu-plugin can add it), the engine uses it to assert the booted versions; otherwise it falls back to the generator tag.

Development

npm ci                       # at the repository root
npm test -w @wp-docs-toolkit/screenshots
npm run lint                 # ESLint (WordPress config) and Prettier (wp-prettier)