Thanks for contributing to the WordPress.com Special Projects Blocks Monorepo. This guide is the canonical, repo-level reference. Longer-form background lives in the project wiki.
- Before you start: do you actually need a new block?
- How the monorepo works
- Local setup
- Creating a new block
- Updating an existing block
- Coding standards
- Submitting a pull request
- Releases and auto-updates
The gold standard for block development is not creating a new block. Every block we add is maintenance we carry forever. Before proposing one, confirm the need can't be met with tools that already exist:
- Patterns — for reusable layouts of existing blocks.
- The Block Bindings API — for binding block attributes to dynamic data.
- The Interactivity API — for front-end interactivity on existing blocks.
- Block styles / variations / filters — for restyling or extending core blocks.
See How to extend a WordPress block. If a new block is genuinely required, open a New block proposal issue first so an engineering lead can sign off before development starts.
- Each top-level directory is an independent, separately-releasable plugin. There are no npm workspaces — you work inside one plugin directory at a time.
- The root
special-projects-blocks-monorepo.phpis an optional autoloader. Onplugins_loadedit includes<dir>/<dir>.phponly for directories that contain abuild/directory. Runningnpm run buildin a plugin is what makes it load. .github/workflows/make-plugin-release.ymlcuts a GitHub release for each changed top-level directory on pushes totrunk. The release tag is<dir>@<plugin-header-version>and the release body is that plugin'sreadme.txt..utilities/class-wpcomsp-blocks-self-update.phpis the shared self-update class copied into each plugin.
See the README for the full plugin inventory.
The monorepo is designed to be cloned into the wp-content/plugins/ directory of a
local WordPress install:
- Clone this repository into
wp-content/plugins/. - In wp-admin, activate WordPress.com Special Projects Blocks Monorepo Autoloader.
- For each plugin you want to work on:
cd <plugin-directory> npm ci npm run build # creates build/ — the autoloader now loads this plugin npm run start # development build with file watching
For PHP coding standards, install the root Composer dependencies once:
composer run-script packages-installScaffold from the monorepo root:
npm run new-block -- <plugin-slug> "Block Title" [--dynamic]This wraps @wordpress/create-block (pinned, --namespace a8csp) and then applies all
the monorepo conventions for you, so a new package starts correct rather than being
hand-patched. It sets up:
- the
a8cspnamespace and a matching<plugin-slug>/<plugin-slug>.phpentry file; - the canonical plugin header (Author, Author URI,
Update URI, GPL-2.0-or-later, Text Domain); - the shared self-update class in
classes/plus thewpcomsp_installed_blockswiring; - a non-boilerplate
readme.txt(with a "Building from source" section) and aCHANGELOG.md.
Use --dynamic for a server-rendered block (render.php); omit it for a static
(save.js) block.
You still own the block itself — fill in the source and follow these rules:
- Keep blocks project-agnostic. No project names, data, or project-specific styling.
- One block plugin per directory. The only exception is tightly-coupled block
families (e.g. a
tabscontainer with its childtabblock). - Keep styling minimal and structural. Blocks should read like wireframes and
render un-broken in the latest
twenty-*theme. Ship structural CSS only; project styling belongs in the project via block stylesheets. Block-level style controls (e.g. colour controls) are fine. - Add PHP filters generously for dynamically-rendered output — think about the next developer reusing this block in their project.
- Write verbose plugin and
block.jsondescriptions. These are reused in automated tooling, so be clear and specific. - Add a
screenshot.png(1200×800) at the plugin root showing the block's purpose. This is used in automated tooling. - Use
counterandtable-plusas reference plugins for structure andreadme.txt.
Blocks here can be installed on many sites, so changes must benefit all users — not just your current project. Project-specific styles and data go in the project, not the block.
- Static blocks: if you change
edit/saveoutput, add a block deprecation so existing content doesn't break. - Dynamic blocks: no deprecation needed, but reason carefully about how the change affects sites already using the block.
- Test an upgrade path: verify on
trunkfirst, then switch to your branch to simulate a plugin update, in the latesttwenty-*theme.
If you need a list of Special Projects sites using a block to test against, ask an engineering lead.
- PHP: PHPCS with WordPress-Extra plus our extras, via the root
.phpcs.xml:vendor/bin/phpcs --standard=.phpcs.xml <plugin-directory>
- JS/CSS: the tools provided by
@wordpress/scripts. Each plugin exposes:Do not add per-pluginnpm run lint:js npm run lint:css npm run format
.eslintrc/.prettierrc/.stylelintrcfiles —wp-scriptsships the shared config we rely on.
trunk is protected; every PR is reviewed. Before opening one:
- Your PR represents a single block plugin (block-family exception aside).
- PHP passes PHPCS + WPCS against
.phpcs.xml. - JS/CSS is linted and formatted with
@wordpress/scripts. - You've tested in the latest
twenty-*theme, and added deprecations for any static-blockedit/savechanges. - Bump the plugin header
Version:and add aCHANGELOG.md/readme.txtchangelog entry when releasing changes — the release workflow reads the PHP header version.
Releases are automatic on merge to trunk (see How the monorepo works).
To make a plugin self-update on installed sites:
- Add the update URI to the plugin header so its hostname matches the update filter:
Update URI: https://opsoasis.wpspecialprojects.com/<plugin-slug>/ - Copy
.utilities/class-wpcomsp-blocks-self-update.phpinto the plugin'sclasses/directory and wire it up in the entry PHP file:// If no other WPCOMSP block plugin already loaded the self-update class, load it. if ( ! class_exists( 'WPCOMSP_Blocks_Self_Update' ) ) { require __DIR__ . '/classes/class-wpcomsp-blocks-self-update.php'; $wpcomsp_blocks_self_update = WPCOMSP_Blocks_Self_Update::get_instance(); $wpcomsp_blocks_self_update->hooks(); } add_filter( 'wpcomsp_installed_blocks', function ( $blocks ) { $blocks[] = '<plugin-slug>'; // enables auto-updates for this plugin. return $blocks; } );
Registering the slug on the wpcomsp_installed_blocks filter keeps auto-updates working
even when a site has several monorepo plugins installed.