Patterns as a deliverable, and the editor who stopped asking us

The ninth request for a campaign landing page in a year, each one a ticket, each one two days, and each one a rearrangement of the same eight components. The work was not development; it was arrangement, and it was going through a developer because there was no other route.

The symptom

the nine tickets, by what actually differed:

  layout                4 distinct arrangements
  copy                  9 distinct
  images                9 distinct
  colour treatment      2 distinct
  a genuinely new
    component           1 (a countdown, in March)

so: 8 of 9 tickets contained zero new code, and each
took two days because it went through a branch, a
review, a staging deploy and a release.

One new component in nine tickets. The other eight were a developer arranging existing blocks in a template file, which the editor could have done if the arrangements had been available to them.

Why it happens

Layouts that live in templates are developer artefacts by definition. An editor can change what is in a page and not how the page is arranged, so any arrangement request becomes a code change regardless of how trivial it is.

The fix

Registering patterns from files

// since 6.0, a theme's patterns/ directory is scanned
// automatically. no register call at all.
//
// patterns/campaign-hero-split.php
?>
<?php
/**
 * Title: Campaign hero, split
 * Slug: turkerdev/campaign-hero-split
 * Categories: turkerdev-campaign
 * Description: A headline and body copy beside a single
 *   image, with one call to action. Use at the top of a
 *   campaign page.
 * Keywords: hero, campaign, landing
 * Viewport Width: 1400
 */
?>
<!-- wp:columns {"verticalAlignment":"center"} -->
<!-- ... -->

The description is the field that does the work and the one everybody leaves blank. The inserter shows it, and an editor is searching for “a headline beside an image” rather than for “hero split” — writing the description as the sentence somebody would say is the difference between a pattern being found and not.

Categories an editor can navigate

add_action( 'init', function () {
    foreach ( array(
        'turkerdev-campaign' => __( 'Campaign layouts', 'turkerdev' ),
        'turkerdev-page'     => __( 'Page sections', 'turkerdev' ),
        'turkerdev-product'  => __( 'Product', 'turkerdev' ),
    ) as $slug => $label ) {
        register_block_pattern_category( $slug, array( 'label' => $label ) );
    }

    // and hide the ones from core that do not fit the theme
    remove_theme_support( 'core-block-patterns' );
} );

Removing core’s pattern library is the decision that makes the theme’s own patterns findable. With it enabled the inserter shows dozens of patterns styled for a different theme, and an editor who inserts one gets something that does not match the site — which teaches them not to use patterns at all.

Synced and unsynced, chosen per pattern

unsynced (the default for theme patterns):
  inserted as ordinary blocks, then edited freely.
  right for a layout — every campaign differs.

synced (what used to be a reusable block):
  one instance, edited everywhere at once.
  right for a legal footnote, a contact block, a
  promotional banner that runs for two weeks.

ours: 14 unsynced layouts, 3 synced fragments.

the mistake to avoid: a synced pattern used as a
layout. an editor changes one page and changes forty.

The pattern that must not be edited

<!-- wp:group {
  "lock": { "move": true, "remove": true },
  "templateLock": "contentOnly"
} -->
<!-- the group cannot be moved or removed, and its
     children accept content edits only: text and
     images change, structure does not -->

contentOnly is the setting that makes a pattern safe to hand over: an editor can rewrite every word and swap every image, and cannot break the layout. It is also the setting that produces a support request the first time somebody genuinely needs to add a column, which is why the lock is per pattern rather than global.

Writing the first four with the editor present

what an hour of watching produced:

  the inserter is not where they looked. they used
    the block list view and the / command.
  "Campaign layouts" was found immediately;
    "Page sections" was not — they called them
    "bits", and searched for "bit".
    → keywords added.
  the viewport width matters: a pattern previewed at
    the default width looked broken and was skipped.
  they inserted a pattern, then deleted half of it,
    which is exactly the intended use and felt to
    them like they were doing it wrong.
    → said so in the description.

The last one is the most useful finding: an editor treating a pattern as a starting point felt like misuse to them because nothing said otherwise. One sentence in the description — “insert this and remove what you do not need” — changed how they used the whole library.

Verifying it worked

# three campaigns after the change
  Aug  developer hours: 0    pages: 2
  Sep  developer hours: 0    pages: 1
  Oct  developer hours: 3    pages: 2
       (the 3 hours: a new pattern, added to the
        library, reusable)

$ wp eval 'echo count( WP_Block_Patterns_Registry::get_instance()
           ->get_all_registered() );'
17

$ wp post list --post_type=page --format=count
  before 41   after 46      # 5 pages, no deploys

Five pages published with no deploy is the outcome, and the three hours in October are the healthy case: a genuinely new arrangement became a pattern rather than a page, so the next one is free.

What this costs

Patterns drift from the design system unless somebody reviews them. An editor who inserts a pattern and adjusts a colour has made a page that is nearly on-brand, and there are now five of those — none of which are wrong enough to complain about and all of which are drift.

The library also has to be maintained against the theme. A pattern is block markup with attributes baked in, so a change to a block’s attribute schema breaks every pattern that used it — and the failure is a pattern that inserts something subtly wrong rather than an error. A test that inserts every pattern and diffs the rendered output would catch it, and we have not written one.