Pattern Builder 2.0: theme pattern files become the single source of truth - #46
Conversation
…truth Removes the 1.x DB-mirror + REST-hijacking architecture entirely and rebuilds pattern management on mechanisms core already uses: - Theme patterns are file-backed REST entities: a rowless pb_pattern post type (the wp_template model — zero DB rows) hangs a string-ID controller off core routing at /pattern-builder/v1/patterns. Reads come from the pattern files (child + parent theme); writes go back to the files. The block editor auto-creates a matching client entity, so theme patterns get entity-powered editing (undo, dirty tracking, save flow) for free. - Synced patterns work through the core/pattern content attribute — the runtime vendored from Synced Patterns for Themes 2.0. Inserted copies are plain wp:pattern references with per-instance overrides; no post ID anywhere. When the companion plugin is active it owns the runtime and Pattern Builder defers (plugins_loaded class_exists gate, plus an attribute-presence guard in the filter itself). - New editing surfaces: a full-screen editor on Appearance → Pattern Builder (public EditorProvider + BlockCanvas bound to the entity, with browse/search/create) and in-context entity editing in the post editor. - Full metadata management: title, description, categories, keywords, block/post/template types, viewport width (previously lost on save), inserter visibility, and synced status all round-trip through the pattern file header. New Metadata panel; Bindings panel re-enabled as the slot-authoring UI. - Conversions both ways: POST with fromWpBlock converts a user pattern to a theme file; PUT with source=user converts a theme pattern to a wp_block post (images exported to the media library). - One-time migration: wp:block refs to 1.x mirror posts are rewritten to wp:pattern slugs in post content and theme files while the mirror rows still exist as the ID map; then the rows and 1.x capabilities are removed. uninstall.php added. - Deleted: the tbell_pattern_block CPT, all four /wp/v2/blocks interception filters (including the unauthenticated delete and edit-context read paths), the per-request registry rewrite and DB upserts (TWE-369), and syncedPatternFilter.js. - Fixes along the way: Pattern_Builder_Security namespace fatal, parent theme patterns ignored (TWE-373), create-time theme choice being cosmetic (TWE-370), sidebar not refreshing after create (TWE-374), broken drag payload, slug -x-x- encoding (TWE-371), user pattern list truncated at 10, and the fixture theme's template referencing patterns from the wrong namespace. - Tests: REST suite rewritten for the new routes (auth included), migration suite added, the companion's runtime suites vendored alongside the runtime classes, JS tests for the override transforms. 92 PHP tests and 14 JS tests, verified in wp-env; editor flows verified in a real browser, alone and with the companion active. - Requires WordPress 6.8 / PHP 7.4. Version 2.0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
CI status: the The job errored before producing a review: I'm re-running the check once to confirm it reproduces, and will keep the PR watched. For reference, the change itself is fully validated outside this check: 92 PHP tests / 276 assertions and 14 JS tests green in wp-env, phpcs/eslint/stylelint clean, and the editor flows verified in a real browser (details in the PR description). Generated by Claude Code |
|
Correction to the above: I don't have permission to re-run the failed job (the API returns 403 for this integration), so the single confirming re-run isn't available to me. The failure is deterministic in any case — a revoked OAuth token will 401 on every run until the secret is refreshed. Once an admin rotates the token, re-running the Generated by Claude Code |
Feedback round on the 2.0 editing surfaces: - The Appearance → Pattern Builder page keeps its browse grid, but its edit mode now boots core's own edit-post editor (wp.editPost.initializeEditor) against the pb_pattern entity — the genuine post-editor chrome (header, list view, inspector, document panels, shortcuts) saving straight to the pattern file. The custom EditorProvider/BlockCanvas editor is deleted. - A history.replaceState guard keeps the page's own URL (the editor assumes post.php, where a string id would 404), and a MainDashboardButton fill turns the fullscreen close button into a Back button that returns to the server-validated back URL. - User patterns now stay in the Site Editor everywhere: in place via its /wp_block route when editing from within the Site Editor, and deep-linked into that canvas from the browse grid and anywhere else without an entity-swap callback. - Theme patterns opened from the Site Editor carry a back URL, so the editor's Back button returns to the exact Site Editor screen. (The Site Editor canvas itself cannot bind a pb_pattern: core hard-codes its editable entity types and keeps route registration private.) - The REST response now advertises wp:action-publish (and wp:action-unfiltered-html) links, so the editor's save button reads Save/Update instead of degrading to Submit for Review. Verified in wp-env (Playwright): browse → theme pattern → real editor → save writes the theme file with intact header, reload survives, Back returns to origin; Site Editor → theme pattern round-trip; Site Editor → user pattern stays in place; browse → user pattern lands in the Site Editor canvas; post-editor in-context editing and override isolation regressions pass. PHP suite 92 tests / 278 assertions; JS 14 tests; eslint/stylelint/phpcs clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
- Unsynced → synced now saves for user patterns. Core registers the
wp_pattern_sync_status meta with enum [partial, unsynced], so writing
an empty string failed REST validation and the save 400ed. Synced is
the absence of the meta; the panel now writes null, the REST meta
API's delete.
- Inserting a synced USER pattern from the sidebar now lands as core's
native synced block — core/block {ref} — instead of a core/pattern
naming a slug the registry doesn't have (which rendered nothing).
Synced THEME patterns keep inserting as core/pattern {slug}; the drag
payload follows the same rule.
- New Edit Pattern button in the block toolbar of a synced theme
pattern instance (core/pattern), next to the runtime's Reset/Detach.
It renders only when the slug resolves to a pb_pattern entity and
opens the pattern the way the sidebar's Edit does: in-context in the
post editor, the Pattern Builder editor elsewhere. (Synced user
instances are core/block, where core provides Edit original.)
Verified in wp-env (Playwright): synced user pattern inserts as
core/block ref 25 and renders in the canvas; the sync flip on an
unsynced wp_block saves and the collection endpoint reports it synced;
Edit Pattern appears on selection and swaps the pb_pattern entity
in-context. JS 14 tests, eslint/stylelint/phpcs clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The Appearance → Pattern Builder browse mode becomes a Site-Editor-style library: - A category rail on the left — All patterns, My patterns (user-created), every category in use (registered labels when available), and Uncategorized — each with a count. - Pattern cards render at a uniform 1:1 size (the preview is cropped to a square) with the title and source/sync badges beneath. Cards carry no actions: clicking selects. - Selecting a pattern opens a details sidebar with the same panels the editor shows for a pattern document (Source with conversion, Synced Status, and for theme patterns Metadata and Associations), staged on the entity — plus Save (enabled when dirty, persists via the entity save flow) and Edit (opens the pattern's editor). - Snackbar notices now render on the page, so save results and the conversion errors the panels already dispatched are visible. Verified in wp-env (Playwright): rail counts and filtering, square cards with no edit buttons, details panels for theme and user patterns, Save disabled-when-clean / enabled-when-dirty, a saved description landing in the pattern file, an unsynced user pattern flipped to synced from the sidebar, and Edit opening the core editor. JS 14 tests, eslint/stylelint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Grid tracks are now minmax(200px, 250px) instead of stretching to fill a sparse row, so pattern cards keep a consistent size at every screen width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The container was sized 100vh minus the admin bar, ignoring whatever the admin renders above it (notices, update nags), so the page scrolled and the bottom of the browser sat below the fold. The boot script now measures the container's real offset and sizes it to end at the viewport bottom, re-measuring on resize and whenever #wpbody-content's height changes (a notice appearing or being dismissed — the admin body itself keeps a viewport-locked height, so it's the wrong observe target). The admin footer and bottom padding are suppressed on this screen so nothing renders below the app. Verified in wp-env: no page scroll and a flush bottom edge with no notice, with a notice injected after load, and after dismissing it; the grid pane keeps its internal scroll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The same release workflow synced-patterns-for-themes ships:
- bin/ship.sh — npm run plugin-ship (and :dry-run): preflight checks
(version agreement across the plugin header, package.json, and
readme.txt's Stable tag; clean git tree; free tag), builds, syncs the
.distignore-defined ship set into svn/trunk via a clean staging hop,
refreshes the wp.org assets, copies trunk to tags/{version}, and
commits all of it in one go after confirmation. Differences from the
companion's copy: the plugin slug, the built-bundle existence checks
(three bundles instead of index.js), and a SHIP_SVN_URL override for
rehearsing against a local SVN repository.
- bin/ship-reset.sh — npm run plugin-ship:reset, verbatim from the
companion.
- .distignore — the ship set: build/, includes/, index.php,
pattern-builder.php, readme.txt, uninstall.php.
- .wordpress-org/ — plugin icon (SVG plus 128/256 PNGs) in the
companion's brand palette.
Rehearsed end-to-end against a local svnadmin repository: trunk ends up
holding exactly the ship set, assets and the 2.0.0 tag land, and the
release commits with 'Release 2.0.0 (git …)'. Versions already agree at
2.0.0 everywhere (header, PATTERN_BUILDER_VERSION, package.json,
Stable tag), so no bump was needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The two-plugin coordination (Pattern Builder's owns_runtime class_exists gate, the companion's provider-deference guard, the patternHostAmended setValues marker) collapses into one check in one place: Synced Patterns for Themes 2.0.1 now sees PATTERN_BUILDER_VERSION at plugins_loaded and stays entirely unloaded. Pattern Builder always registers the full stack, and the vendored runtime copies drop the now-pointless guards to stay logic-identical to the companion's. The companion cache flush becomes a direct transient delete — the companion's classes are never loaded alongside, but its week-long per-theme cache may survive from before, and clearing it on every file write keeps the companion current if Pattern Builder is ever deactivated. Verified in wp-env with both plugins active: the companion's classes are not loaded, this plugin's Pattern_Block owns the core/pattern render callback, and insertion, canvas and front-end rendering, and per-instance override isolation all pass; deactivating Pattern Builder hands rendering back to the companion identically. PHP suite 91 tests / 276 assertions; JS 14 tests; lints clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
The 250px cap kept cards from getting smaller when the pane narrowed, so only two fit beside the details sidebar. Tracks are now minmax(160px, 1fr): more columns fit at any width, and the cards stretch to share the row evenly instead of leaving a ragged gap. Verified in wp-env: three columns with the details sidebar open (194px cards), five without it (171px), previews square in both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Pattern Builder 2.0
This is the 2.0 upgrade following the architecture Synced Patterns for Themes 2.0 proved out: no custom post type hiding the patterns, no REST interception — theme pattern files are the single source of truth, and every 1.x feature is rebuilt on mechanisms WordPress core already uses.
Companion PR: Twenty-Bellows/synced-patterns-for-themes#6 — the companion now stands down entirely when this plugin is active.
The architecture
Theme patterns are file-backed REST entities — the
wp_templatemodel. A rowlesspb_patternpost type (zero DB rows, ever) hangsPattern_Builder_REST_Patterns_Controlleroff core routing at/pattern-builder/v1/patterns, with string IDs (theme-slug/pattern-name). Reads come from the pattern files (child and parent theme); writes go back to the files. Because the type isshow_in_rest, the block editor auto-creates a matching client-side entity from/wp/v2/types— undo, dirty tracking, and the save flow all come from core's entity layer, with zero masquerade.Synced patterns ride the
core/patterncontent runtime vendored from Synced Patterns for Themes 2.0:core/patterngets acontentattribute +pattern/overridescontext (the exact shapecore/blockhas), a render callback attaches the pattern's blocks as inner blocks, and core's owncore/pattern-overridesbinding source resolves overrides. An inserted synced theme pattern is a plain pattern block:<!-- wp:pattern {"slug":"my-theme/hero","content":{...}} /-->No post ID anywhere, and per-instance overrides stay isolated. Synced user patterns stay core-native: they insert as
core/blockreferences.Coexistence is one check in one place. Pattern Builder always registers the full stack — the vendored runtime (kept logic-identical to the companion's) plus the editing layer. When both plugins are installed, the companion sees
PATTERN_BUILDER_VERSIONatplugins_loadedand stays entirely unloaded; deactivate Pattern Builder and the companion takes over with identical rendering. Pattern Builder also clears the companion's per-theme transient after file writes, so the companion never wakes to a stale cache.Editing surfaces — always the WordPress editor, never a custom one:
pb_pattern(orwp_block) entity into the canvas without leaving the post; the entity is pre-resolved first to avoid an empty-content race./wp_blockroute. Theme patterns cannot bind to the Site Editor canvas — core hard-codes the entity types its canvas resolves and keeps route registration private — so Edit opens core's own edit-post editor bound to the pattern file entity, and its Back button returns to the exact Site Editor screen you left (server-validatedbackURL).wp.editPost.initializeEditor) against thepb_patternentity — the genuine post-editor chrome saving straight to the file. Deep-linkable via&pattern={id}; ahistory.replaceStateguard keeps the page's URL, and the app pins its bottom edge to the viewport so the panes scroll internally.Feature work
wp_pattern_sync_statusmeta with enum[partial, unsynced], so "synced" is the absence of the meta; the panel writesnull(the REST meta delete) instead of an enum-rejected empty string.core/pattern) on the canvas offers Edit Pattern next to the runtime's Reset/Detach — in-context in the post editor, the Pattern Builder editor elsewhere. Shown only when the slug resolves to a file-backed pattern. (Synced user instances arecore/block, where core provides Edit original.)POST /patterns {fromWpBlock: N}converts user→theme (images imported as theme assets, post deleted);PUT /patterns/{id} {source:"user"}converts theme→user (images exported to the media library, file deleted).wp:block {"ref":N}references to 1.x mirror posts are rewritten towp:patternslugs in post content and theme files while the rows still exist as the ID→slug map, then the mirror posts and 1.x capabilities are removed.uninstall.phpadded (1.x had none).npm run plugin-ship/:dry-run/:reset— preflight version agreement,.distignore-defined ship set, wp.org assets in.wordpress-org/, SVN trunk + tag + assets committed in one go. Rehearsed end-to-end against a local SVN repository.Inserter:header (1.x hid them all and re-listed them as fakewp_blocks).wp:action-publish, so the editor's save button reads Save/Update instead of degrading to "Submit for Review".Removed / fixed
tbell_pattern_blockCPT, its capability family, and the per-request DB upserts (TWE-369 — the performance issue is gone by construction)./wp/v2/blocksinterception filters — including the unauthenticated file deletion and unauthenticated edit-context read paths that roderest_pre_dispatchahead of permission callbacks. Every route now has a permission callback (edit_postsread /edit_theme_optionswrite), and a test pins that norest_pre_dispatchfilter remains.Pattern_Builder_Securitywas declared in the global namespace but called from namespaced code — a latent fatal on every file write. Fixed.-x-x-slug encoding), TWE-373 (parent theme ignored), TWE-374 (sidebar not refreshing after create), the broken drag payload, the user-pattern list truncated at 10, and the fixture theme's template referencing patterns from the wrong namespace.WP_Block::refresh_context_dependents(); 1.x already called a 6.8-only function despite its 6.6 header).Testing
lint:js,lint:css, andphpcsall clean..phpfile on disk updated with an intact header; page reload survives; Back returns to the origin screen; no page scrolling.backURL → Back lands on the exact Site Editor screen. User patterns edited in place in the Site Editor (/wp_blockroute), from within it and deep-linked from the grid.core/pattern(not a copy) and renders on the front end through the runtime; a synced user pattern lands ascore/block {ref}and renders; in-contextpb_patternentity editing; Edit Pattern toolbar button navigates in-context.contentattribute; a second instance of the same pattern stays isolated.Pattern_Blockowns thecore/patternrender callback; deactivating Pattern Builder hands rendering back to the companion identically.Notes for reviewers
includes/class-pattern-block.php,class-pattern-resolver.php,class-block-markup.php,class-inner-html-processor.php,class-synced-patterns.php,class-editor-support.phpandsrc/runtime/*are vendored from synced-patterns-for-themes (namespace/identifier renames only) and should be kept logic-identical to it — that release-time invariant is what makes the coexistence hand-off invisible.docs/architecture.mdis marked historical; the new design is indocs/architecture-2.0.md.lockPostAutosavingis dispatched; the editor's one autosave probe 404s harmlessly).🤖 Generated with Claude Code
https://claude.ai/code/session_01W5Y7NJdKZ2BenQmGVtDbVB