Theme Patterns

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

A block pattern is a named piece of block markup that the editor offers in the inserter, in the "Choose a pattern" dialogs and in templates. Themes ship patterns so that site owners start from layouts that already match the design: a hero section, a page of starter content, a grid of books for the Query Loop. This page is for theme developers. It shows how the lumen block theme of the fictitious publisher Lumen Press registers its patterns from PHP files in /patterns, how translation and escaping work inside them, how patterns become starter content, block variations and parts of templates, and how locking keeps a section’s layout intact. Creating, syncing and overriding patterns in the admin screens, and how user patterns are stored as wp_block posts, is covered from the site owner’s side in Patterns for Site Builders.

This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the version to run in production), PHP 7.4 minimum and 8.3+ recommended, theme.json version 3 and WP-CLI 2.12.0, and checked on 2026-10-28. Every registry listing, REST response and rendered fragment below comes from a 7.1.3 site in Docker with the lumen theme and the lumen-catalog plugin active. WordPress 7.2 is in beta and due in December 2026; no change to the theme pattern API has been announced for it.

How theme patterns are loaded

On every request, WordPress runs _register_theme_block_patterns() on init. It asks the active theme, and its parent when the theme is a child theme, for the list of pattern files, and registers each one in the WP_Block_Patterns_Registry singleton, next to core’s patterns, Pattern Directory patterns and patterns registered in code. Several details of that path explain most surprises:

  • WP_Theme::get_block_patterns() scans /patterns (subfolders included) for .php files and reads their headers with get_file_data(), as WordPress does for style.css. Only the header is read at this point.

  • The parsed headers are cached in a site transient named wp_theme_files_patterns-<hash> together with the theme’s Version, for 30 minutes by default (filter wp_theme_files_cache_ttl). A file added without bumping the theme’s version stays invisible until the transient expires, unless WP_DEVELOPMENT_MODE is theme or all.

  • Since 6.5 each pattern is registered with a filePath instead of its content. The file is executed with output buffering the first time something asks for the content (the inserter’s REST request, a wp:pattern block on the page), not on every request.

  • A child theme’s pattern wins over a parent pattern with the same slug, because the child is registered first and the parent’s duplicate is skipped.

  • get_registered() and get_all_registered() run the content through apply_block_hooks_to_content(), so hooked blocks (see Block Hooks) are inserted into theme patterns too.

The Lumen pattern set

The lumen theme ships six files in patterns/: hero.php (a cover with translatable strings and a theme image), page-about.php (starter content for new pages), query-book-grid.php (a block-type pattern for core/query), template-front-page.php (the body of templates/front-page.html), submissions-callout.php (a locked, content-only section) and book-spotlight.php (overridable markup for a synced pattern). functions.php registers the category lumen/house and one more pattern, lumen/footer-colophon; the hero image lives in assets/images/hero.webp. The rest of the theme is described in Themes Overview and Templates and Template Parts.

Pattern file headers

A pattern file starts with a PHP docblock whose Field: value lines describe the pattern. Title and Slug are required; a file without either is skipped with a "called incorrectly" notice. The other headers are optional.

Header Registered property Since Meaning and format

Title

title

6.0

Name shown in the inserter. Translated with the context "Pattern title" in the theme’s text domain.

Slug

name

6.0

Unique, namespaced identifier, lumen/hero. Allowed characters: letters, digits, /, _ and -.

Description

description

6.0

Longer text for screen readers. Translated with the context "Pattern description".

Categories

categories

6.0

Comma-separated category slugs, core ones (banner, featured, call-to-action) or the theme’s own.

Keywords

keywords

6.0

Comma-separated search terms for the inserter.

Viewport Width

viewportWidth

6.0

Width in pixels of the iframe in which the preview is rendered before being scaled down.

Inserter

inserter

6.0

no or false hides the pattern from the inserter; it stays usable in templates and dialogs.

Block Types

blockTypes

6.0 (5.8 in PHP)

Blocks the pattern belongs to: core/post-content, core/query, core/template-part/header, and so on.

Post Types

postTypes

6.1

Restricts the pattern to these post types, such as page or lp_book.

Template Types

templateTypes

6.2

Template slugs the pattern suits (front-page, home, single, 404), for the "Choose a pattern" dialog when a template is created.

Comma-separated values are split with wp_parse_list(); Viewport Width is cast to an integer; Inserter is true only for yes or true (and defaults to shown when the header is absent). An invalid slug produces a notice but, in 7.1.3, the pattern is still registered; fix it anyway, because every wp:pattern reference repeats the slug.

PHP, translation and escaping in patterns

Pattern files are PHP, so they can call translation functions, build URLs and loop over arrays. The PHP runs once, when the registry first needs the content, and its output is stored as block markup. It therefore runs without the main query: conditionals such as is_page(), template tags such as the_title(), and checks on the current user or post return nothing useful. Data that must change per page belongs in blocks that render dynamically (Query Loop, post blocks) or in block bindings (see Block Bindings API), not in pattern PHP.

The lumen hero translates every visible string, escapes it for its context and points the cover image at a file in the theme with get_theme_file_uri(), which also lets a child theme replace the image by shipping the same path:

<?php
/**
 * Title: Hero with cover image
 * Slug: lumen/hero
 * Description: A full-width cover with the Lumen Press tagline and a call to action.
 * Categories: lumen/house, banner, featured
 * Keywords: hero, cover, banner, intro
 * Viewport Width: 1400
 */
?>
<!-- wp:cover {"url":"<?php echo esc_url( get_theme_file_uri( 'assets/images/hero.webp' ) ); ?>","dimRatio":50,"minHeight":520,"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-cover alignfull" style="min-height:520px"><span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span><img class="wp-block-cover__image-background" alt="" src="<?php echo esc_url( get_theme_file_uri( 'assets/images/hero.webp' ) ); ?>" data-object-fit="cover"/><div class="wp-block-cover__inner-container"><!-- wp:heading {"textAlign":"center","level":1} -->
<h1 class="wp-block-heading has-text-align-center"><?php esc_html_e( 'Independent books, carefully made', 'lumen' ); ?></h1>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center"><?php esc_html_e( 'Literary fiction, essays and books for young readers since 1998.', 'lumen' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="<?php echo esc_url( home_url( '/books/' ) ); ?>"><?php esc_html_e( 'Browse the catalogue', 'lumen' ); ?></a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons --></div></div>
<!-- /wp:cover -->

Rules that keep pattern PHP safe and valid:

  • Escape at output, for the context: esc_html_e() or esc_html( __() ) for text, esc_attr_e() for attributes, esc_url() for URLs. Translations are third-party input; an unescaped string can break the markup or inject HTML.

  • The image URL appears twice, in the block comment’s JSON and in the <img> tag. Both must change together, or the editor reports the block as invalid ("This block contains unexpected or invalid content").

  • Images belong in the theme (assets/images/) or the Media Library; hot-linking them from another server is a privacy and availability problem.

  • Prefix every PHP variable a pattern defines ($lumen_values); WPCS and Theme Check flag unprefixed names.

Pattern titles and descriptions are translated by core at registration with the contexts "Pattern title" and "Pattern description"; the strings in the body are ordinary __() calls. wp i18n make-pot picks up both:

wp i18n make-pot wp-content/themes/lumen wp-content/themes/lumen/languages/lumen.pot --slug=lumen
grep -B3 'msgid "Hero with cover image"' wp-content/themes/lumen/languages/lumen.pot

The grep shows the entry #: patterns/hero.php, msgctxt "Pattern title", msgid "Hero with cover image", next to the body strings with their line numbers.

Loading the translations and translating themes in general is covered in Internationalization.

Pattern categories

Core registers about twenty categories (banner, buttons, call-to-action, columns, featured, footer, gallery, header, media, posts, query, text, about, contact and others) regardless of whether core’s own patterns are loaded. A theme can add its own with register_block_pattern_category() on init. The slug may contain a /, which is a convenient way to namespace it; the label and description are translatable. lumen keeps all its patterns in one Lumen Press category so site owners find them first, and also lists them in core categories where they fit. The same functions.php registers the one pattern that is defined in PHP rather than a file (described in the next section):

<?php
defined( 'ABSPATH' ) || exit;

function lumen_register_patterns() {
	register_block_pattern_category(
		'lumen/house',
		array(
			'label'       => __( 'Lumen Press', 'lumen' ),
			'description' => __( 'House patterns of the Lumen Press site.', 'lumen' ),
		)
	);

	$lumen_year = (int) wp_date( 'Y' );

	register_block_pattern(
		'lumen/footer-colophon',
		array(
			'title'         => __( 'Footer colophon', 'lumen' ),
			'description'   => __( 'Copyright line and distributor credit for the footer.', 'lumen' ),
			'categories'    => array( 'lumen/house', 'footer' ),
			'keywords'      => array( 'copyright', 'colophon' ),
			'blockTypes'    => array( 'core/template-part/footer' ),
			'viewportWidth' => 1200,
			'source'        => 'theme',
			'content'       => sprintf(
				'<!-- wp:group {"layout":{"type":"constrained"}} --><div class="wp-block-group"><!-- wp:paragraph {"fontSize":"small"} --><p class="has-small-font-size">%s</p><!-- /wp:paragraph --></div><!-- /wp:group -->',
				esc_html(
					sprintf(
						/* translators: %d: current year. */
						__( '© 1998–%d Lumen Press. Distributed by Northwind Books.', 'lumen' ),
						$lumen_year
					)
				)
			),
		)
	);
}
add_action( 'init', 'lumen_register_patterns' );
add_action( 'after_setup_theme', static function () { remove_theme_support( 'core-block-patterns' ); } );
add_filter( 'should_load_remote_block_patterns', '__return_false' );

A category with no patterns is not shown in the inserter. unregister_block_pattern_category() removes a category, core’s included. User pattern categories created in the admin are terms of the wp_pattern_category taxonomy and are a different mechanism (see Patterns for Site Builders).

Registering patterns in PHP

register_block_pattern( $pattern_name, $pattern_properties ) (5.5) is the PHP API behind the folder: the folder loader calls it with the parsed headers. Called directly, it takes the same properties in camelCase (blockTypes, postTypes, templateTypes, viewportWidth) with arrays instead of comma lists, plus either content (a string of block markup) or, since 6.5, filePath. It returns false and raises a notice when the name is not a string, when title is missing, or when neither content nor filePath is given. source is informational; theme is the value for theme code. A pattern must be registered once, by the folder or by code, not both.

Registering in code is the right choice when the markup is generated (the colophon above computes the year at registration), when a pattern is conditional (registered only when lumen-catalog is active, for example), or when a plugin ships the pattern. Everything else is easier to maintain as a file. Registration belongs on init. Older code registered patterns on admin_init or current_screen; the registry still tracks those so the post editor page can receive them, but the block-patterns REST endpoint the Site Editor uses never runs those actions and does not list them.

WordPress 7.0 added no new pattern registration API; its "PHP-only" feature concerns blocks: a block registered with register_block_type(), a render_callback and the autoRegister support appears in the editor without any JavaScript (see block.json and Registration). For patterns, register_block_pattern() and the /patterns folder remain the only APIs in 7.0 and 7.1.

Three switches remove patterns that other code registered. remove_theme_support( 'core-block-patterns' ) on after_setup_theme drops core’s bundled patterns and the "core" patterns of the Pattern Directory. Returning false from the should_load_remote_block_patterns filter stops every Pattern Directory request. unregister_block_pattern( 'slug' ) on init at priority 20 or later removes one pattern, for example a parent theme’s pattern in a child theme. lumen uses the first two (last lines above). The top-level patterns array of theme.json (6.0) is the opposite switch: it lists Pattern Directory slugs to show next to the theme’s own, and the filter disables it too.

Starter page patterns

A pattern with Block Types: core/post-content is offered as starter content. When a user creates a page, the editor opens a dialog with the patterns whose Post Types include page (or that list no post types); choosing one fills the page. Starter patterns have existed since 6.0, and Post Types was added in 6.1 to keep page layouts out of the dialog for posts and custom post types. They are post content, so they must not contain a site header or footer.

<?php
/**
 * Title: About Lumen Press
 * Slug: lumen/page-about
 * Description: Starter content for the About page: history, team and a contact call to action.
 * Categories: lumen/house, about
 * Block Types: core/post-content
 * Post Types: page
 * Viewport Width: 1200
 */

$lumen_values = array(
	__( 'We read every manuscript we receive.', 'lumen' ),
	__( 'We print in small runs on recycled paper.', 'lumen' ),
	__( 'We pay authors twice a year, on time.', 'lumen' ),
);
?>
<!-- wp:heading -->
<h2 class="wp-block-heading"><?php esc_html_e( 'Who we are', 'lumen' ); ?></h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p><?php esc_html_e( 'Lumen Press is an independent publisher of fiction, essays and children’s books.', 'lumen' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:list -->
<ul class="wp-block-list"><?php foreach ( $lumen_values as $lumen_value ) : ?><!-- wp:list-item -->
<li><?php echo esc_html( $lumen_value ); ?></li>
<!-- /wp:list-item --><?php endforeach; ?></ul>
<!-- /wp:list -->
<!-- wp:pattern {"slug":"lumen/submissions-callout"} /-->

The list is built from an array in PHP, which keeps the strings translatable without repeating the block markup. The last line includes another pattern; the REST endpoint that feeds the inserter (/wp/v2/block-patterns/patterns) resolves it to blocks and returns the headers as block_types: ["core/post-content"] and post_types: ["page"].

A starter template pattern works the same way for the Site Editor: a pattern with Template Types is offered in the "Choose a pattern" dialog when a site owner adds a template of that type. Setting Inserter: no keeps a full-page layout out of the post editor’s inserter.

Block-type patterns

Block Types with any other block name turns a pattern into an alternative layout for that block. The editor shows it where the block offers patterns:

  • core/query: the Query Loop’s Choose button on insertion, and Replace in its toolbar.

  • core/template-part/header and core/template-part/footer: Replace in a header or footer template part’s options in the Site Editor. Custom template part areas are not supported yet.

  • Other blocks, such as core/cover or core/columns: a Patterns submenu under the block’s transform menu.

The Lumen book grid is a Query Loop of lp_book posts in a three-column grid. It sets "inherit":false so that it always lists books, whatever the template’s main query is:

<?php
/**
 * Title: Book grid
 * Slug: lumen/query-book-grid
 * Description: Three columns of the latest books with cover, title and excerpt.
 * Categories: lumen/house, query
 * Block Types: core/query
 * Viewport Width: 1200
 */
?>
<!-- wp:query {"queryId":7,"query":{"perPage":6,"postType":"lp_book","order":"desc","orderBy":"date","inherit":false},"align":"wide","layout":{"type":"default"}} -->
<div class="wp-block-query alignwide"><!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
<!-- wp:post-featured-image {"isLink":true,"aspectRatio":"2/3"} /-->
<!-- wp:post-title {"level":3,"isLink":true} /-->
<!-- wp:post-excerpt {"excerptLength":20} /-->
<!-- /wp:post-template -->
<!-- wp:query-no-results -->
<!-- wp:paragraph -->
<p><?php esc_html_e( 'No books in the catalogue yet.', 'lumen' ); ?></p>
<!-- /wp:paragraph -->
<!-- /wp:query-no-results --></div>
<!-- /wp:query -->

The pattern names the post type, but it does not register it: post types are plugin territory, and lumen-catalog registers lp_book. Without the plugin the grid renders its "no results" paragraph. The colophon registered in functions.php is a block-type pattern for core/template-part/footer, offered when the site owner replaces the footer.

Patterns in templates: wp:pattern

A template or template part can include a pattern with the Pattern block, <!-- wp:pattern {"slug":"lumen/hero"} /-->. The block (core/pattern, since 5.9) has no inserter entry; it exists for theme files. It is the way to use translatable strings and theme URLs in templates, which are plain HTML files and cannot run PHP. lumen keeps the front page template minimal and puts its body in a pattern with Template Types and Inserter: no:

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group"><!-- wp:pattern {"slug":"lumen/template-front-page"} /--></main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
<?php
/**
 * Title: Front page
 * Slug: lumen/template-front-page
 * Description: Hero, latest books and the submissions box, used by templates/front-page.html.
 * Categories: lumen/house
 * Template Types: front-page, home
 * Inserter: no
 */
?>
<!-- wp:pattern {"slug":"lumen/hero"} /-->
<!-- wp:group {"tagName":"section","align":"wide","layout":{"type":"constrained","wideSize":"1200px"}} -->
<section class="wp-block-group alignwide"><!-- wp:heading -->
<h2 class="wp-block-heading"><?php esc_html_e( 'New from Lumen Press', 'lumen' ); ?></h2>
<!-- /wp:heading -->
<!-- wp:pattern {"slug":"lumen/query-book-grid"} /--></section>
<!-- /wp:group -->
<!-- wp:pattern {"slug":"lumen/submissions-callout"} /-->

What happens to a wp:pattern block depends on where it is read:

  • On the front end, render_block_core_pattern() renders the pattern’s blocks in place, with no wrapper element (a wrapper is only added for the legacy syncStatus attribute). Patterns may nest; a pattern that includes itself, directly or through another, stops with "[block rendering halted for pattern …​]" when WP_DEBUG_DISPLAY is on and renders nothing otherwise. An unknown slug renders nothing, silently.

  • In the Site Editor, the templates and block-patterns REST endpoints call resolve_pattern_blocks() (6.6), which replaces each wp:pattern with the pattern’s blocks. Since 7.0, when the pattern has a single root block, that block receives metadata.patternName plus the pattern’s title, description and categories. The editor treats such a block as a pattern instance: content-only editing with an Edit pattern button (see Patterns for Site Builders). A multi-root pattern such as lumen/template-front-page is inlined without that metadata.

  • After a save, the template is stored as a wp_template post with the resolved blocks. From then on, changes to the pattern file no longer reach that template until the site owner resets it. This is the same rule as for any edited template (see Templates and Template Parts).

Locking a section

Block markup can carry two locking attributes. lock (5.9) applies to the block itself; templateLock applies to the children of a container block (Group, Cover, Columns, Column, Navigation and blocks that declare the attribute):

Attribute Effect

"lock":{"move":true,"remove":true}

The block itself cannot be moved (move) or deleted (remove); each key works alone.

"templateLock":"all"

Children cannot be moved, removed or added; their content and styles remain editable.

"templateLock":"insert"

Children cannot be added or removed but can be reordered. No editor UI sets it.

"templateLock":"contentOnly" (6.1)

Only content (text, links, media) of the children is editable; structural and design controls are hidden and the sidebar shows a Content panel.

"templateLock":false

Explicitly unlocked, overriding a lock inherited from a parent or from the post type’s template.

The Lumen submissions box combines both: the group cannot be moved or removed, and inside it only text and the link change.

<?php
/**
 * Title: Submissions call-out
 * Slug: lumen/submissions-callout
 * Description: A locked box inviting manuscripts; authors can change the text and link only.
 * Categories: lumen/house, call-to-action
 */
?>
<!-- wp:group {"templateLock":"contentOnly","lock":{"move":true,"remove":true},"metadata":{"name":"Submissions call-out"},"layout":{"type":"constrained"},"style":{"spacing":{"padding":{"top":"var:preset|spacing|40","bottom":"var:preset|spacing|40","left":"var:preset|spacing|40","right":"var:preset|spacing|40"}}}} -->
<div class="wp-block-group" style="padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading"><?php esc_html_e( 'Submissions are open', 'lumen' ); ?></h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p><?php esc_html_e( 'We accept unagented fiction and essay manuscripts until 31 January.', 'lumen' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="<?php echo esc_url( home_url( '/submissions/' ) ); ?>"><?php esc_html_e( 'Read the guidelines', 'lumen' ); ?></a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons --></div>
<!-- /wp:group -->

Locks are guidance for the editing interface, not permissions. Any user who can edit the post can unlock a block from its toolbar unless the site restricts that with the canLockBlocks editor setting (filter block_editor_settings_all), and the stored markup can always be changed through the code editor or the REST API.

Since 7.0, every unsynced pattern inserted from the inserter, and every single-root pattern resolved from a template, is edited in content-only mode by default even without templateLock. An explicit "templateLock":"contentOnly" still matters: it survives the site-wide opt-out (disableContentOnlyForUnsyncedPatterns) and it also locks the section on sites still running 6.9. Content-only mode only exposes attributes that a block declares with "role": "content" in block.json, so a custom block without that role cannot be edited there. The Buttons, List, Social Icons and Navigation blocks received specific content-only behaviour in 7.0 and are worth checking in every pattern that uses them.

Overridable markup for synced patterns

Theme patterns are never synced: inserting one copies its blocks. Pattern overrides (6.6) only work inside a synced pattern, a wp_block post that a site owner creates. A theme can still prepare a pattern for overrides by shipping the binding metadata that the editor’s Enable overrides button would write. When a site owner inserts the theme pattern and turns it into a synced pattern (Create pattern, synced), the override slots are already defined:

<?php
/**
 * Title: Book spotlight
 * Slug: lumen/book-spotlight
 * Description: Title, blurb and buy button for one book; the three blocks are ready for pattern overrides.
 * Categories: lumen/house
 */
?>
<!-- wp:group {"metadata":{"name":"Book spotlight"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"metadata":{"name":"Book title","bindings":{"__default":{"source":"core/pattern-overrides"}}}} -->
<h2 class="wp-block-heading"><?php esc_html_e( 'Book title', 'lumen' ); ?></h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"metadata":{"name":"Blurb","bindings":{"__default":{"source":"core/pattern-overrides"}}}} -->
<p><?php esc_html_e( 'One or two sentences about the book.', 'lumen' ); ?></p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"metadata":{"name":"Buy link","bindings":{"__default":{"source":"core/pattern-overrides"}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="<?php echo esc_url( home_url( '/books/' ) ); ?>"><?php esc_html_e( 'Find it at your local bookshop', 'lumen' ); ?></a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons --></div>
<!-- /wp:group -->

Each overridable block needs a metadata.name that is unique inside the pattern, because it keys the per-page values, and a __default binding to the core/pattern-overrides source, which covers every attribute the block supports for bindings (content for headings and paragraphs; url, text, linkTarget and rel for buttons; the image attributes for images). Since 7.0 a custom block can take part by declaring its attributes through the block_bindings_supported_attributes filter. Override names are user-facing in the editor’s sidebar, so they should be readable words; they are not translated.

To check the markup, a test run stored the pattern’s content as a wp_block post (ID 8) and created a page whose content is <!-- wp:block {"ref":8,"content":{"Book title":{"content":"The Glass Orchard"},...}} /-->, the form the editor saves for an instance with overrides. https://lumenpress.test/spring-catalogue/ then renders the per-page values inside the theme’s layout:

<div class="wp-block-group is-layout-constrained wp-block-group-is-layout-constrained">
<h2 class="wp-block-heading">The Glass Orchard</h2>
<p class="wp-block-paragraph">A botanist inherits an orchard that only fruits in winter.</p>
<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://lumenpress.test/books/the-glass-orchard/">Buy The Glass Orchard</a></div>
</div>
</div>

How site owners store and edit instance values is described in Patterns for Site Builders. A theme must not create wp_block posts itself: content creation is plugin territory.

Verifying the patterns

The checks below run against a site with lumen and lumen-catalog active and three published books, such as the stack in Local Development Environments. The first lists what the registry holds for the theme:

wp eval 'foreach ( WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $p ) {
  if ( 0 === strpos( $p["name"], "lumen/" ) ) {
    printf( "%-28s %-38s %s\n", $p["name"], implode( ",", $p["categories"] ?? array() ),
      implode( ",", array_merge( $p["blockTypes"] ?? array(), $p["templateTypes"] ?? array(), $p["postTypes"] ?? array() ) ) );
  }
}
echo count( WP_Block_Patterns_Registry::get_instance()->get_all_registered() ), " patterns registered\n";'
lumen/book-spotlight         lumen/house
lumen/hero                   lumen/house,banner,featured
lumen/page-about             lumen/house,about                      core/post-content,page
lumen/query-book-grid        lumen/house,query                      core/query
lumen/submissions-callout    lumen/house,call-to-action
lumen/template-front-page    lumen/house                            front-page,home
lumen/footer-colophon        lumen/house,footer                     core/template-part/footer
7 patterns registered

Seven is the whole registry: remove_theme_support( 'core-block-patterns' ) and the should_load_remote_block_patterns filter leave no core or directory patterns. A single entry printed with var_export() before anything has asked for its content still carries 'filePath' => '/var/www/html/wp-content/themes/lumen/patterns/hero.php' instead of markup. The front page, fetched with curl -s https://lumenpress.test/, renders the hero, the heading, the three books from the Query Loop and the submissions box in place inside <main>, with every string and URL from the pattern PHP filled in (src="https://lumenpress.test/wp-content/themes/lumen/assets/images/hero.webp", href="https://lumenpress.test/books/").

The rendered HTML carries no trace of the locks or of the pattern names: they are editor attributes in the block comments. They do appear in what the Site Editor loads. Requesting the template through REST as an administrator (/wp/v2/templates/lumen//front-page?context=edit) and printing the attributes of the blocks inside <main> gives:

core/cover   {"metadata":{"patternName":"lumen/hero","name":"Hero with cover image","description":"A full-width cover with the Lumen Press tagline and a call to action.","categories":["lumen/house","banner","featured"]}}
core/group   []
core/group   {"templateLock":"contentOnly","lock":{"move":true,"remove":true},"metadata":{"name":"Submissions call-out","patternName":"lumen/submissions-callout","description":"A locked box inviting manuscripts; authors can change the text and link only.","categories":["lumen/house","call-to-action"]}}

The middle group, written directly in lumen/template-front-page, has no pattern metadata; its nested Query Loop has "patternName":"lumen/query-book-grid". The pattern’s title replaced the group’s own metadata.name, because pattern metadata takes precedence over the block’s.

Pattern cache and development mode

Because parsed headers are cached per theme version, a new pattern file is not registered until one of these happens: the Version in style.css changes, the 30-minute transient expires, the transient is deleted, or the site runs in theme development mode. On the test site, adding patterns/events-list.php left it unregistered until the transient was deleted:

wp eval 'var_dump( WP_Block_Patterns_Registry::get_instance()->is_registered( "lumen/events-list" ) );'
wp transient list --network --search='wp_theme_files_patterns*' --fields=name,expiration --human-readable
wp transient delete --all --network
wp eval 'var_dump( WP_Block_Patterns_Registry::get_instance()->is_registered( "lumen/events-list" ) );'
bool(false)
name	expiration
wp_theme_files_patterns-a70de31fef8150d9e8f2fc68b3dfcb5e	29 minutes
Success: 2 transients deleted from the database.
bool(true)

During theme development, define( 'WP_DEVELOPMENT_MODE', 'theme' ); in wp-config.php disables the cache (and the theme.json caches) so every request rescans the folder; with WP_DEBUG on, header errors then appear at once, such as Function get_block_patterns was called incorrectly. Could not register file ".../patterns/no-title.php" as a block pattern ("Title" field missing).

Development mode must not be enabled in production (see wp-config.php Reference). For releases, bumping the theme version is the reliable way to invalidate the cache, and a persistent object cache (Redis) stores the transient there instead of in wp_options.

Version history

Version Change for theme patterns

5.5, 5.8

Block Patterns API and core-block-patterns support (5.5); blockTypes and Pattern Directory patterns in the inserter (5.8).

5.9

core/pattern block for templates; block lock attribute.

6.0

Automatic registration of /patterns/*.php with file headers; starter page patterns; patterns key in theme.json.

6.1

Post Types header; templateLock value contentOnly.

6.2

Template Types header; starter patterns for new templates.

6.3, 6.4

Reusable blocks renamed synced patterns (6.3); header parsing in WP_Theme::get_block_patterns() with a pattern cache (6.4).

6.5

filePath property: pattern files are executed only when their content is needed.

6.6

Pattern cache stored in transients on every environment; resolve_pattern_blocks() in REST responses; pattern overrides for synced patterns.

6.8

theme_block_pattern_files filter (add or drop pattern files before registration); the Patterns screen for classic themes with editor-styles or theme.json.

7.0

Inserted unsynced patterns and resolved single-root patterns are content-only by default and carry metadata.patternName; overrides open to custom blocks.

7.1

No change to the pattern API.

Where older books are outdated

None of the three older books teaches theme patterns. Pro WordPress treats only classic themes, where reusable sections are widgets, PHP template parts or shortcodes; in a block theme they are pattern files and template parts. WordPress All-in-One For Dummies covers patterns only from the editor. Ultimate WordPress Handbook still says "reusable blocks" (synced patterns since 6.3) and calls full site editing still maturing, although the /patterns API has been stable since 6.0. None covers overrides (6.6), filePath loading (6.5) or the 7.0 content-only default.

Common errors and pitfalls

Symptom Cause and fix

A new pattern file does not appear

The header cache: bump Version in style.css, delete the wp_theme_files_patterns-* transient, or use WP_DEVELOPMENT_MODE theme locally.

"Could not register file …​ ("Title" field missing)" or "("Slug" field missing)"

The docblock header is incomplete or not at the top of the file. Both fields are required.

"invalid slug" notice

The slug has spaces or other characters outside letters, digits, /, _ and -. The pattern still registers in 7.1.3; fix the slug and every reference to it.

Template shows nothing where the pattern should be

wp:pattern references an unregistered slug (typo, pattern from an inactive plugin). Unknown slugs fail silently.

"This block contains unexpected or invalid content" after inserting

The markup in the file does not match what the block saves: an image URL changed in the comment but not in the HTML, a class or inline style missing. Copy markup from the editor’s code view.

is_page(), the_title() or the current user give wrong values in a pattern

Pattern PHP runs once at registration time, without the main query. Use dynamic blocks or block bindings.

Editing patterns/*.php changes nothing on a live template

The template was saved in the Site Editor and is now a wp_template post with resolved blocks. Reset the template or export it again.

Custom block text disappears inside a locked section

The block’s attribute lacks "role": "content" in block.json.

A pattern registered on admin_init is missing in the Site Editor

The block-patterns REST endpoint, which the Site Editor uses, never runs admin_init. Register on init.