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

@humanmade/wp-pattern-library

v0.1.1

Published

Generate a Markdown pattern library, with screenshots, from a WordPress site's registered block patterns.

Readme

WP Pattern Library

Generate a browsable Markdown pattern library — with screenshots — from a WordPress site's registered block patterns.

The site serves a manifest of its registered patterns and renders each one in isolation; a Node CLI captures them with Playwright and writes an index plus one page per pattern category. A GitHub Action runs the whole thing and opens a pull request with the refreshed docs.

Two packages ship from this repository:

| Package | Install | |---------------------------------|--------------------------------------------------| | humanmade/wp-pattern-library | composer require humanmade/wp-pattern-library | | @humanmade/wp-pattern-library | npm install -D @humanmade/wp-pattern-library |

Install

1. The plugin

composer require humanmade/wp-pattern-library

The Composer package declares "type": "wordpress-plugin", so composer/installers routes it to the project's plugin directory — content/plugins/ on Altis, plugins/ on WordPress VIP — without any installer-paths change.

Activate it as you would any other plugin: through wp-admin, wp plugin activate wp-pattern-library, or your platform's code-activation helper.

// VIP: in client-mu-plugins/plugin-loader.php
wpcom_vip_load_plugin( 'wp-pattern-library' );

It belongs in the deployed environment, not in require-dev: the library is captured from the running production site, so the route has to be live there. What it adds to that site is one front-end route behind a dedicated capability, plus a WP-CLI command — no admin UI, no front-end assets, nothing on the request path for ordinary visitors.

Limit it to your own patterns, so core and plugin patterns stay out of the library:

add_filter( 'pattern_library_namespaces', fn () => [ 'my-theme/' ] );

2. A service account

The routes require a dedicated capability, view_pattern_library. The bundled WP-CLI command creates a role holding that capability and read — and nothing else, so the account cannot create or edit content:

wp pattern-library setup --login=pattern-library-bot

It prints an application password. Store it, along with the login, as CI secrets; it is not recoverable.

On multisite, roles are stored per site. Run the command with --url= for each site whose patterns you want to capture.

To use an existing user instead, run wp pattern-library setup and then wp pattern-library grant <user>.

3. Configuration

Create pattern-library.config.js in your project root:

export default {
	title: 'My Pattern Library',
	namespaces: [ 'my-theme/' ],
	outputDir: 'docs/pattern-library',
};

Credentials come from the environment, never the config file:

export PATTERN_LIBRARY_SITE="https://example.com"
export PATTERN_LIBRARY_WP_USER="pattern-library-bot"
export PATTERN_LIBRARY_WP_APP_PASSWORD="xxxx xxxx xxxx xxxx"

Usage

npx pattern-library build            # capture screenshots, then write Markdown
npx pattern-library build --dry-run  # report what would be included, write nothing
npx pattern-library capture hero     # screenshots only, for patterns matching "hero"
npx pattern-library generate         # Markdown only, from existing screenshots
npx pattern-library manifest         # print the filtered manifest as JSON

Screenshots are written only when their bytes change, so re-running against a live site does not churn images whose content merely shifted underneath them.

The run summary flags patterns that rendered empty, failed outright, or referenced resources — images, fonts — that no longer exist, so broken previews are caught at generation time rather than in review.

Configuration reference

| Key | Default | Notes | |-------------------|-------------------------------|----------------------------------------------------------| | title | Pattern Library | Index heading. | | namespaces | [] (all) | Pattern-name prefixes to include. | | outputDir | docs/pattern-library | Where pages and screenshots are written. | | indexFile | <outputDir>/README.md | Index path. | | screenshotsDir | <outputDir>/screenshots | Screenshot path. | | imageFormat | webp | webp, avif, jpeg or png. | | imageQuality | 80 | Ignored for png. | | defaultViewport | 1440 | Used when a pattern declares no Viewport Width. | | exclude | see below | What to leave out of the library. | | postTypeContext | {} | Basename to post type, for core/post-template patterns. | | classify | flat | Category placement. See below. | | animations | [ 'aos' ] | Animation libraries to settle before capture. See below. | | extraFields | [] | Extra metadata lines per pattern. See below. | | includeSkipped | true | List excluded patterns, with reasons, on the index page. | | placeholderImages | true | Placeholder featured image for posts that have none. |

exclude defaults to skipping patterns hidden from the inserter and those scoped to wp_template / wp_template_part, since template parts render meaninglessly in isolation:

exclude: {
	inserterHidden: true,
	postTypes: [ 'wp_template', 'wp_template_part' ],
	patterns: [], // Exact pattern names.
}

Patterns that render empty

A pattern built as a query-loop item template has nothing to bind to when rendered alone. Give it a post type, keyed by basename:

postTypeContext: { 'person-card': 'person' }

The CLI reports any pattern that rendered empty, so these are easy to find.

Animation libraries

Scroll-triggered animation libraries hide content until it scrolls into view, so a capture must force everything to its finished state first. animations lists what to settle: built-in library names (currently aos), or custom { css, settle } objects for project-specific conventions:

animations: [
	'aos',
	{
		// Optional: CSS injected before capture, overriding the hidden state.
		css: '.js-reveal { opacity: 1 !important; transform: none !important; }',
		// Optional: runs in the browser to flip elements to "done". Serialized
		// into the page, so it must not close over config-file variables.
		settle: () => {
			document
				.querySelectorAll( '.js-reveal' )
				.forEach( ( el ) => el.classList.add( 'is-revealed' ) );
		},
	},
]

A handler that proves generally useful belongs in src/animations.mjs as a new built-in, so other projects can list it by name.

Extra metadata fields

Each pattern's section shows its description, categories, keywords, block types and post types by default. extraFields appends further lines: value is a manifest property name, or a function receiving the pattern:

extraFields: [
	{ label: 'Source', value: 'source' },
	{ label: 'Namespace', value: ( pattern ) => pattern.name.split( '/' )[ 0 ] },
]

Grouping categories

By default every registered category becomes one page in the output root. A project with a richer taxonomy can supply a classify() function, called once per category, returning where it belongs — or nothing, to drop it from the library:

classify: ( { slug, label } ) => {
	// A cross-cutting category of whole-page references, which should also lead
	// each section page rather than sitting in the list.
	if ( slug === 'full-page' ) {
		return { kind: 'reference', dir: 'references', label: 'Full page', leadsIn: 'section' };
	}
	if ( label.startsWith( 'Component - ' ) ) {
		return { kind: 'component', dir: 'components', label: label.slice( 12 ) };
	}
	return { kind: 'section', dir: 'sections', label };
}

kind groups pages under headings on the index, dir places the page, label sets its title, and leadsIn promotes its patterns to the top of every page of the named kind.

GitHub Action

Copy examples/refresh-pattern-library.yml into .github/workflows/. It is workflow_dispatch-triggered, takes a base branch and output path, and opens a pull request with the refreshed docs.

Capture from production. Screenshots reflect deployed code, so a pattern that exists only on the branch being documented renders as "preview pending" until it ships. Staging environments sitting behind their own HTTP Basic gate cannot be used: two Authorization: Basic headers cannot coexist on one request.

How authentication works

Requests authenticate with a standard WordPress application password over HTTP Basic. Two details are worth knowing, because both fail silently otherwise — WordPress serves the logged-out page with a 200, which would yield a run's worth of plausible but wrong screenshots:

  • wp_authenticate_application_password() ignores any request that is not REST or XML-RPC. The preview route opts itself in through the application_password_is_api_request filter, scoped to its own query var.
  • Browsers only attach Basic credentials after a 401 carrying WWW-Authenticate, and never send them preemptively. Playwright's httpCredentials.send: 'always' does not change this — it applies only to its API request context, not to page navigation. The route therefore sends a proper challenge with its 401.

As a backstop, before capturing anything the CLI navigates the browser to the manifest URL and requires a 200 — a probe that authenticates through the same browser path the captures use, yet cannot be brought down by a single pattern that fails to render.

Filters

| Filter | Purpose | |---------------------------------------|------------------------------------------------------| | pattern_library_enabled | Disable the routes entirely. | | pattern_library_namespaces | Pattern-name prefixes to expose. | | pattern_library_user_can | Override the capability check. | | pattern_library_placeholder_image | Markup of the placeholder featured image. |

Requirements

PHP 8.1+, WordPress 6.0+, Node 24+.

The PHP side runs on the WordPress site; the Node side runs wherever the library is generated, which in practice is CI or a developer machine. They do not need to be the same host, and the site does not need Node.

Contributing and releases

Both published packages — the Composer package and the npm package — are built from this one repository, from the same tree. There is no separate source for the Node CLI: bin/ and src/ are what npm publishes, inc/ and plugin.php are what Packagist serves, and action.yml wraps the npm package for GitHub Actions.

One tag drives all three, so a given version means the same code everywhere:

  1. Bump version in package.json, and the Version: header in plugin.php. Keep them equal — the plugin header is what shows in wp-admin, and a manifest served by one version being read by another is the failure mode this convention exists to prevent.
  2. Tag the release vX.Y.Z and push the tag.
  3. npm publish --access public publishes @humanmade/wp-pattern-library. Packagist picks up humanmade/wp-pattern-library from the same tag via its GitHub hook.

Consuming workflows pin the action to a released tag (humanmade/[email protected]). There is deliberately no moving @v1 tag while the package is pre-1.0.

The two halves are coupled by the manifest: inc/manifest.php produces it and src/manifest.mjs consumes it. A change to either shape is a change to both, in one commit, released under one tag.

License

GPL-2.0-or-later