ArtisanPack UI
DocsCMS FrameworkSite Editor - Patterns

Site Editor - Patterns

CMS Framework v2.16.0

The patterns module surfaces both theme-shipped patterns (PHP files) and user-authored patterns (DB rows) through a single read API. Unlike templates...

The patterns module surfaces both theme-shipped patterns (PHP files) and user-authored patterns (DB rows) through a single read API. Unlike templates, patterns do not merge by slug — theme patterns and user patterns occupy disjoint keyspaces.

Added in 2.0.0 (H2).

Storage

Patterns occupy two disjoint sources rather than a file/DB merge:

  • Theme patterns — PHP files at themes/{active}/patterns/{slug}.php with a leading WP-style header doc-comment (Title:, Slug:, Categories:, Description:, Block Types:, Viewport Width:, Keywords:, Inserter:, Template Types:, Post Types:). Read-only at runtime; the body after the doc-comment becomes the pattern content.
  • User patterns — DB rows in block_patterns with columns id, slug, theme (nullable), title, description, source, synced (bool), categories (JSON), block_types (JSON), keywords (JSON), inserter (bool, default true), template_types (JSON), post_types (JSON), viewport_width (int, nullable), block_content (JSON via HasBlockContent), author_id, timestamps. The five pattern-header columns were added in 2.15.0; run php artisan migrate after upgrading.

User-pattern slugs carry a user/ prefix at storage time — this guarantees a theme pattern named hero and a user pattern named hero do not collide in the merged inserter map. The REST surface presents the unprefixed user-facing slug; the storage form is exposed as name on the unsynced response (mirroring WP's namespaced pattern names).

Pattern file format

Theme patterns are parsed by PatternFileParser from PHP files of the form:

<?php
/**
 * Title: Call to Action
 * Slug: cta
 * Categories: featured, cta
 * Description: A simple call-to-action block.
 * Block Types: core/group
 * Viewport Width: 1200
 * Keywords: call to action, signup
 * Inserter: yes
 * Template Types: front-page, home
 * Post Types: page
 */
?>
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
    <!-- wp:heading -->
    <h2>Get started today</h2>
    <!-- /wp:heading -->
</div>
<!-- /wp:group -->

The parser regexes are anchored — the doc-comment must lead the file. Everything after the closing ?> (or after the closing */ when there is no PHP tag) becomes the pattern content. Recognized header keys: Title:, Slug:, Categories: (comma-separated), Description:, Block Types: (comma-separated), Viewport Width:, Keywords: (comma-separated), Inserter:, Template Types: (comma-separated), Post Types: (comma-separated). Since 2.15.0 header names match case-insensitively, as WordPress's get_file_data() does, so post types: works too.

Inserter headers

Added in 2.15.0.

These headers mirror WordPress's theme pattern registration:

  • Keywords: — inserter search terms, exposed as keywords. Entries are split on commas only, so a phrase such as call to action stays one keyword (WordPress also splits on spaces).
  • Inserter: — exposed as inserter. no, false or any other value hides the pattern from the inserter; yes, true, 0 or an empty or missing header keeps it visible. Hidden patterns are still returned by the API and the filter map; the editor decides where to show them.
  • Template Types: — template types the pattern suits (front-page, 404, …), exposed as template_types.
  • Post Types: — post types the pattern is offered for, lower-cased and de-duplicated, exposed as post_types. A missing or empty header gives null: the pattern is offered for every post type. visual-editor filters its inserter by this list.

All four are exposed on the /block-patterns/patterns responses, on ResolvedPattern ($keywords, $inserter, $templateTypes, $postTypes) and in toFilterMap() entries.

Viewport width

Added in 2.13.0.

Viewport Width: mirrors WordPress's viewportWidth pattern property: the width, in pixels, the pattern is laid out at before an inserter preview scales it down. The value must be a positive whole number with an optional px suffix (1200 or 1200px). Valid values are clamped to 320–2560 (PatternFileParser::MIN_VIEWPORT_WIDTH / MAX_VIEWPORT_WIDTH). A missing or invalid header yields null, meaning "use the client's default width".

The parsed value is exposed as viewport_width on the /block-patterns/patterns responses, as ResolvedPattern::$viewportWidth, and in toFilterMap() entries.

User pattern fields

Since 2.15.0 user patterns store the same fields in block_patterns columns, and POST / PUT accept them: keywords and template_types (string lists), inserter (bool), post_types (lowercase post-type keys, [a-z0-9_-], up to 20 characters) and viewport_width (320–2560). Rows written before 2.15.0 read as keywords: [], inserter: true, template_types: [], post_types: null and viewport_width: null. An empty stored post_types list also reads as null.

Sync semantics

synced distinguishes the two pattern shapes Gutenberg recognizes:

  • synced = true — surfaced under /blocks (WP wp_block shape). Editing the pattern updates every occurrence in the editor.
  • synced = false — surfaced under /block-patterns/patterns (WP wp_block_pattern shape). Each insertion is a snapshot; later edits to the pattern do not propagate.

Theme patterns are always synced = false. Cross-state conversion happens through dedicated POST flows on each endpoint, not via PUT (a PUT to one endpoint never flips a pattern's synced bit on the other endpoint).

REST endpoints

Method Path Purpose
GET /blocks List user-source synced patterns (wp_block shape).
GET /blocks/{slug} Show single synced pattern.
POST /blocks Create a synced user pattern.
PUT /blocks/{slug} Upsert a synced user pattern.
DELETE /blocks/{slug} Delete a synced user pattern.
GET /block-patterns/patterns List theme + user-source unsynced patterns merged.
GET /block-patterns/patterns/{slug} Show single unsynced pattern (theme or user).
POST /block-patterns/patterns Create an unsynced user pattern.
PUT /block-patterns/patterns/{slug} Upsert an unsynced user pattern. 403 when targeting a theme slug.
DELETE /block-patterns/patterns/{slug} Delete a user pattern. 403 when targeting a theme slug.

A theme pattern can be cloned to an editable user pattern via PatternResolver::cloneToUser($slug) — used by the admin "edit" affordance on theme patterns. The clone copies the title, description, categories, block types, block content and, since 2.15.0, the inserter headers (keywords, inserter, template_types, post_types, viewport_width) from the theme file into a new user-source DB row. Before 2.15.0 the clone had no block content. Since 2.15.1 the copied blocks go through the ap.cmsFramework.siteEditor.blockContent filter (context block-pattern, with the theme file's blocks as $storedBlocks) before the row is written, so a Custom CSS guard such as visual-editor's keeps the theme's Custom CSS; see Hooks and Events.

Writes need the Site Editor content ability (manage-content by default) and reads the read ability; see API Reference: Authorization.

Resolver

PatternResolver returns ResolvedPattern value objects. Unlike EntityResolver (templates / parts), it surfaces a cloneToUser() affordance and toFilterMap() for filter consumers, but does not implement revert() — patterns have no override-then-revert workflow.

class PatternResolver
{
    public function resolve(string $slug): ?ResolvedPattern;
    public function all(): array;                         // map<storage-slug, ResolvedPattern>
    public function toFilterMap(): array;                 // map<storage-slug, array<string, mixed>>
    public function cloneToUser(string $themeSlug): BlockPattern;
}

The merged map keys use the storage form: theme patterns under their natural slug (hero), user patterns under user/{slug}. visual-editor's ap.visual-editor.patterns consumer expects this shape.

Theme-pattern block hydration

Added in 2.5.0 (#204).

PatternResolver::buildThemePattern() populates a parsed blocks array for every theme-shipped pattern. Previously this was hardcoded to [], causing patterns to render as "Empty pattern" placeholders in the visual-editor pattern browser and open as empty canvases until the client's hydrateBlocks() re-parsed content.raw.

The new BlockMarkupParser support class ports a subset of WordPress's parse_blocks() and produces the WP shape {blockName, attrs, innerBlocks, innerHTML, innerContent}. It covers flat blocks, nested containers, void (self-closing) blocks, freeform HTML between blocks, and forgiving JSON-attr decoding.

Scope and limits. Client-side Gutenberg parse(rawContent) remains authoritative for editor correctness — the PHP parser is a best-effort optimization for lightweight surfaces like the pattern thumbnail summary. Known limitations (unpaired braces inside JSON attribute string values, mis-nested closers) are documented on the class. Recursion is capped at depth 64 so adversarially-nested block_content can't blow the PHP call stack; PCRE errors and JSON-decode failures are logged via Log::warning; a leading UTF-8 BOM is stripped so BOM-prefixed theme files don't emit a phantom freeform sibling.

The parser is deliberately generic — sibling TemplateResolver and TemplatePartResolver closed the same blocks: [] gap in 2.7.2 (#274) via ThemeFileBlockParser, which wraps this parser and prefers visual-editor's editor-shape BlockMarkupHydrator when it is installed. See Templates and Template Parts.