Skip to main content

Blocks and assets

How the plugin builds scripts, styles and blocks, and how each output reaches WordPress. For running the watchers see Local development; for the loader API see the framework's AssetLoader reference.

Names below use an initialized Acme Content project (acme-content-features text domain and handle prefix).

Two builds​

ScriptConfigSource → output
build:assets / start:assetswebpack.config.js (extends @wordpress/scripts)src/css/** → assets/build/css/, src/js/** → assets/build/js/, src/js/modules/** → assets/build/js/modules/ (ES modules)
build:blocks / start:blocks@wordpress/scripts default; webpack.blocks.config.js for the dev serversrc/blocks/<block>/ → assets/build/blocks/<block>/

npm run build:dev and npm run build:prod run both. Every file in src/js and src/css becomes its own entry, and the output mirrors the folder structure (src/js/admin/reports.js → assets/build/js/admin/reports.js). Files and folders starting with _ are skipped, so use _partial.scss for imports. Each script build also writes a *.asset.php manifest that AssetLoader reads for dependencies and version.

Scripts and styles​

inc/Core/Assets.php extends the framework AssetLoader and is shared through Main's container. It prefixes every handle with acme-content-features- (Assets::handle( 'frontend' )).

HandleSourceBehaviour
frontendsrc/js/main.js, src/css/main.scssRegistered and enqueued on every frontend page.
adminsrc/js/admin.js, src/css/admin.scssRegistered on admin_enqueue_scripts; enqueue it on the screens that need it.
editorsrc/js/editor.js, src/css/editor.scssRegistered on enqueue_block_editor_assets only if you create these files (none ship).
tailwindsrc/css/tailwind.cssEnqueued on the frontend when the Tailwind feature is on; see Tailwind.
@acme-content-features/modulesrc/js/modules/module.jsScript module with an @wordpress/interactivity dependency, enqueued on the frontend.

register_script() and register_style() return false when the built file is missing, so a missing build never prints a broken tag. They also raise a _doing_it_wrong notice (visible with WP_DEBUG), which is why Assets checks has_asset() before registering optional entries such as editor.

Add a script for one screen​

Create the source, for example src/js/admin/reports.js, then register and enqueue it from the class that owns the screen, using the shared Assets instance:

use Acme_Content\Features\Core\Assets;
use Acme_Content\Features\Main;

public function register_hooks(): void {
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
}

public function enqueue( string $hook_suffix ): void {
if ( 'tools_page_acme-content-features-reports' !== $hook_suffix ) {
return;
}

$assets = Main::get_instance()->get_shared( Assets::class );
$handle = $assets->handle( 'reports' );

if ( $assets->register_script( $handle, 'js/admin/reports' ) ) {
wp_enqueue_script( $handle );
}
}

Keep enqueues conditional per screen, and never print inline <script> or <style> from PHP.

Blocks​

There are two ways a block reaches WordPress, and the difference decides where you register it.

KindExamplesServer renderingRegister it in
Metadata block: registered straight from its built block.jsonexample-block (static), example-block-interactive (Interactivity API, render.php + view.js)None, or a render.php file referenced in block.jsonAdd the folder name to Assets::STATIC_BLOCKS in inc/Core/Assets.php
Class-based dynamic block: an AbstractBlock subclassexample-block-dynamic with Modules/Blocks/ExampleDynamicBlock.phpThe class's render() method; the example renders templates/block-templates/example-block-dynamic.php through Util::templates()Add the class to Modules\Blocks::get_classes()

Choose a class when rendering needs plugin services, several templates, or unit tests of the render logic; render.php is enough for simple markup. Both kinds build from src/blocks/<block>/ into assets/build/blocks/<block>/. Because rendered templates go through the template loader, a theme can override them from <theme>/acme-content-features/block-templates/.

AbstractBlock's lifecycle and overridable methods (get_block_dir(), get_block_args()) are in the framework's block reference.

Create a block​

RouteProducesThen
npx wp-tooling add wp/block-dynamic (or /scaffold)An AbstractBlock class, its test and block sourcesRegister the class in Modules\Blocks::get_classes(). See Scaffolding.
npm run create:blockAn interactive prompt for a static, dynamic or meta block; writes src/blocks/<slug>/ from the templates in bin/block-scaffold/templates/Follow the printed next step: add the slug to Assets::STATIC_BLOCKS. Meta blocks are written to src/blocks/meta-blocks/, which STATIC_BLOCKS does not cover yet.

Run npm run build:dev (or keep npm start running) and insert the block in the editor. For block editor Fast Refresh while editing components, see Live reload and block HMR.

For block.json, attributes, supports and the Interactivity API themselves, use the WordPress Block Editor Handbook.

Troubleshooting​

SymptomCheck and next action
Block missing from the inserterMetadata block: is its folder in STATIC_BLOCKS and built under assets/build/blocks/? Class block: is the class in Blocks::get_classes() and does get_block_dir() point at the build folder?
Script registered but not on the pageOnly frontend is enqueued automatically; call wp_enqueue_script() for the others.
New src/js file not builtIts name starts with _, it is inside src/js/modules/ (built as a module), or the watcher needs a restart to pick up a new entry.