Blockstudio
Pages & Patterns

Pages

Pages are file-based templates that sync to the WordPress database as posts. When you update a template file, Blockstudio detects the change and updates the corresponding page.

page.json

Each page folder needs a page.json configuration file:

page.json
{
  "name": "about",
  "title": "About Us",
  "slug": "about-us",
  "postType": "page",
  "postStatus": "publish",
  "templateLock": "all"
}

Properties

PropertyTypeDefaultDescription
namestringrequiredUnique identifier for the page
titlestringAuto from nameThe page title
slugstringSame as nameURL slug for the page
postTypestring"page"WordPress post type
postStatusstring"draft"Status for new posts (publish, draft, pending, private)
postIdinteger-Pin the page to a specific WordPress post ID
blockEditingModestring-Block editing mode: "default", "contentOnly", or "disabled"
templateLockstring|false"all"Lock mode: "all", "insert", "contentOnly", or false
templateForstring|nullnullSet as default template for a post type
syncbooleantrueWhether to sync content when template changes

Block Editing Mode

The blockEditingMode property controls how blocks can be edited in the editor. Set a page-level default in page.json and override per-element in the template:

page.json
{
  "name": "about",
  "title": "About Us",
  "templateLock": "all",
  "blockEditingMode": "disabled"
}
index.php
<h1 blockEditingMode="contentOnly">Editable Title</h1>
<p>This paragraph inherits "disabled" from the page default.</p>
<block name="core/buttons" blockEditingMode="default">
  <block name="core/button" url="/contact">Fully editable button</block>
</block>
ValueDescription
"default"Full editing. Blocks can be selected, moved, and edited
"contentOnly"Text editing only. Block text is editable but settings and structure are locked
"disabled"No editing. Blocks are completely non-interactive

Per-element overrides are set as HTML attributes on any element (<h1>, <p>, <div>, etc.) or on <block> elements. Ancestor container blocks of overridden elements are automatically set to contentOnly so their children remain accessible. This cascade walks up from the overridden element to the page root, so you only need to set blockEditingMode on the elements you want to make editable.

Template Lock

The templateLock property controls how users can edit the page in the editor:

ValueAdd/RemoveMoveEdit Content
"all"NoNoNo
"insert"NoYesYes
"contentOnly"NoNoYes
falseYesYesYes

When a templateLock is set, Blockstudio disables the ability to unlock blocks via the editor UI.

Sync Behavior

Pages are synced when you visit the WordPress admin. The sync checks the file's modification time and only updates if the file has changed since the last sync.

To prevent a page from being overwritten after manual edits, set "sync": false in page.json.

Post ID Pinning

By default, WordPress auto-assigns post IDs. If a page is deleted and re-created, the ID changes, breaking external references like menus or hardcoded links.

Use postId to pin a page to a specific ID:

page.json
{
  "name": "about",
  "title": "About Us",
  "postId": 42
}

When creating the page, Blockstudio passes the ID to WordPress via import_id. If the ID is available, WordPress uses it exactly. If the ID is already taken by an unrelated post, WordPress silently auto-assigns a new ID.

When looking up existing pages, Blockstudio checks the pinned ID first, before checking meta or slug. This means if a page is deleted and re-synced, it reclaims the same ID.

Keyed Block Merging

By default, when a template file changes, Blockstudio replaces the entire post content with the new template. This means any edits made in the WordPress editor are lost.

Add a key attribute to blocks to preserve user edits across template syncs. When a template has keyed blocks, Blockstudio merges template changes with the existing post content instead of replacing it.

Adding Keys

Use the key attribute on any HTML element or <block> element:

index.php
<h1 key="title">Default Title</h1>
<p key="intro">Default intro text.</p>

<div key="features">
  <p>First feature.</p>
  <p>Second feature.</p>
</div>

<block name="core/cover" key="hero" url="https://example.com/bg.jpg">
  <h2>Hero Title</h2>
  <p>Hero description.</p>
</block>

Keys must be globally unique across the entire template. A key protects the entire block and all of its content. You don't need to key individual children inside a keyed parent. Keyed blocks can be moved to any position or nesting level between template updates and their user content will still be preserved.

How Merging Works

When a template file changes and the page re-syncs:

BlockBehavior
Keyed blockUser's content is preserved (innerHTML, innerContent, innerBlocks). Template's attributes are applied.
Unkeyed blockReplaced entirely with the template version (same as without keys)

Examples

User edits are preserved in keyed blocks:

  1. Template defines <p key="intro">Default intro.</p>
  2. User edits the paragraph in the editor to "Custom intro text."
  3. Developer updates the template, adds a new block, changes layout
  4. On sync: the paragraph keeps "Custom intro text." while the new layout is applied

Attributes from the template still apply:

  1. Template has a cover block with key="hero" and a url attribute
  2. Developer changes the cover's url attribute to a new background image
  3. On sync: the new background image is applied, but user's content edits inside the cover are preserved

Edge Cases

ScenarioBehavior
New keyed block added to templateAppears with the template's default content
Keyed block removed from templateDeleted from the post
Block type changes (same key)Template wins entirely, no content merge
Duplicate keysSecond occurrence is treated as unkeyed
No keys in templateFull replacement (same behavior as without keys)
Force syncFull replacement, keys are ignored
Locked postNo sync at all

Combining with Editing Controls

Keys, templateLock, and blockEditingMode combine to give you fine-grained control over what clients can and can't do. Here are common workflow patterns.

Fully locked page

The developer controls everything. The page is a static layout that clients can't touch at all. Useful for legal pages, terms of service, or any page that should only change through code.

page.json
{
  "name": "terms",
  "title": "Terms of Service",
  "templateLock": "all"
}
index.php
<h1>Terms of Service</h1>
<p>Last updated: January 2025</p>
<p>These terms govern your use of our service...</p>

No keys needed. The template is the single source of truth. Every sync overwrites the page entirely.

Locked layout with editable content

The most common pattern. Lock the page structure so clients can't add, remove, or rearrange blocks, but let them edit text in specific places. Keys ensure their edits survive template updates.

page.json
{
  "name": "landing",
  "title": "Landing Page",
  "templateLock": "all",
  "blockEditingMode": "disabled"
}
index.php
<block name="core/cover" key="hero" url="https://example.com/bg.jpg">
  <h1 blockEditingMode="contentOnly">Edit This Title</h1>
  <p blockEditingMode="contentOnly">Edit this description.</p>
</block>

<div key="features">
  <h2>Features</h2>
  <p blockEditingMode="contentOnly">Editable feature intro.</p>
</div>

<block name="core/buttons">
  <block name="core/button" url="/contact">Contact Us</block>
</block>

The client can edit the hero title, description, and feature intro. Nothing else. The button text, URL, and cover background image are developer-controlled. When the developer updates the template (e.g., adds a second button), keyed sections keep the client's text.

Evolving template with preserved content

For pages that grow over time. The developer adds new sections to the template, and each sync adds the new content without touching what the client has already customized.

index.php (initial)
<h1 key="title">Welcome</h1>
<p key="intro">Our introduction.</p>
<div key="services">
  <h2>Our Services</h2>
  <p>We offer great things.</p>
</div>
index.php (updated later)
<h1 key="title">Welcome</h1>
<p key="intro">Our introduction.</p>
<div key="services">
  <h2>Our Services</h2>
  <p>We offer great things.</p>
</div>
<div key="testimonials">
  <h2>Testimonials</h2>
  <p>What our clients say.</p>
</div>
<block name="core/buttons">
  <block name="core/button" url="/contact">Get in Touch</block>
</block>

The new testimonials section and button appear on sync. The client's edits to the title, intro, and services are untouched.

One-time scaffold

Create a page with a starting structure, then hand it off entirely. Set sync to false so the template is only used for the initial creation. After that, the client owns the page completely.

page.json
{
  "name": "blog",
  "title": "Blog",
  "sync": false
}
index.php
<h1>Our Blog</h1>
<p>Welcome to our blog. Start writing!</p>

The page is created once. After that, the client can add, remove, and rearrange blocks freely. Template changes are ignored.

Block Bindings

An alternative to keyed merging is using WordPress core's Block Bindings API to connect blocks to post meta. This is not a Blockstudio feature, it is a native WordPress API. Blockstudio simply passes the metadata attribute through to the generated block markup. The content lives in post meta instead of block markup, so template syncs can freely overwrite the markup because the editor reads from meta, not from innerHTML.

Pass the metadata attribute on any HTML element or <block> element:

index.php
<h1 metadata='{"bindings":{"content":{"source":"core/post-meta","args":{"key":"hero_title"}}}}'>
  Default Title
</h1>

<p metadata='{"bindings":{"content":{"source":"core/post-meta","args":{"key":"hero_description"}}}}'>
  Default description text.
</p>

<img metadata='{"bindings":{"url":{"source":"core/post-meta","args":{"key":"hero_image"}},"alt":{"source":"core/post-meta","args":{"key":"hero_image_alt"}}}}' />

The meta keys must be registered with show_in_rest enabled:

functions.php
register_post_meta( 'page', 'hero_title', array(
    'show_in_rest'  => true,
    'single'        => true,
    'type'          => 'string',
    'default'       => 'Default Title',
) );

Keys vs. bindings

KeysBindings
Content stored inBlock markup (post_content)Post meta (wp_postmeta)
Survives template syncYes (merged by key)Yes (data lives in meta)
Queryable via WP_QueryNoYes (meta_query)
Available in REST APINoYes
Requires registrationNoYes (register_post_meta)
User can move blocksNo (template re-imposes structure)No (template re-imposes structure)

Both approaches can be used in the same template. Use keys for sections where the block structure matters (e.g., a group with multiple children). Use bindings for individual fields where you also want the data accessible outside the block editor.

Post Type Templates

Use templateFor to set a page as the default template for a post type:

page.json
{
  "name": "product-template",
  "title": "Product Template",
  "templateFor": "product",
  "templateLock": "insert"
}

Any new posts of that type will start with the defined block structure.

Custom Paths

By default, Blockstudio scans get_template_directory() . '/pages'. Add additional paths with the filter:

add_filter( 'blockstudio/pages/paths', function( $paths ) {
    $paths[] = get_stylesheet_directory() . '/custom-pages';
    $paths[] = MY_PLUGIN_DIR . '/pages';
    return $paths;
} );

PHP API

// Get all registered pages
$pages = Blockstudio\Pages::pages();

// Get a specific page
$page = Blockstudio\Pages::get_page('about');

// Get the WordPress post ID for a page
$postId = Blockstudio\Pages::get_post_id('about');

// Force sync a page (ignores modification time)
Blockstudio\Pages::force_sync('about');

// Force sync all pages
Blockstudio\Pages::force_sync_all();

// Lock a page to prevent automatic updates
Blockstudio\Pages::lock('about');

// Unlock a page
Blockstudio\Pages::unlock('about');
Guide: Building File-Based PagesTemplate locking, block editing modes, keyed merging, and controlling what clients can edit.

On this page