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:
{
"name": "about",
"title": "About Us",
"slug": "about-us",
"postType": "page",
"postStatus": "publish",
"templateLock": "all"
}Properties
| Property | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique identifier for the page |
title | string | Auto from name | The page title |
slug | string | Same as name | URL slug for the page |
postType | string | "page" | WordPress post type |
postStatus | string | "draft" | Status for new posts (publish, draft, pending, private) |
postId | integer | - | Pin the page to a specific WordPress post ID |
blockEditingMode | string | - | Block editing mode: "default", "contentOnly", or "disabled" |
templateLock | string|false | "all" | Lock mode: "all", "insert", "contentOnly", or false |
templateFor | string|null | null | Set as default template for a post type |
sync | boolean | true | Whether 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:
{
"name": "about",
"title": "About Us",
"templateLock": "all",
"blockEditingMode": "disabled"
}<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>| Value | Description |
|---|---|
"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:
| Value | Add/Remove | Move | Edit Content |
|---|---|---|---|
"all" | No | No | No |
"insert" | No | Yes | Yes |
"contentOnly" | No | No | Yes |
false | Yes | Yes | Yes |
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:
{
"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:
<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:
| Block | Behavior |
|---|---|
| Keyed block | User's content is preserved (innerHTML, innerContent, innerBlocks). Template's attributes are applied. |
| Unkeyed block | Replaced entirely with the template version (same as without keys) |
Examples
User edits are preserved in keyed blocks:
- Template defines
<p key="intro">Default intro.</p> - User edits the paragraph in the editor to "Custom intro text."
- Developer updates the template, adds a new block, changes layout
- On sync: the paragraph keeps "Custom intro text." while the new layout is applied
Attributes from the template still apply:
- Template has a cover block with
key="hero"and aurlattribute - Developer changes the cover's
urlattribute to a new background image - On sync: the new background image is applied, but user's content edits inside the cover are preserved
Edge Cases
| Scenario | Behavior |
|---|---|
| New keyed block added to template | Appears with the template's default content |
| Keyed block removed from template | Deleted from the post |
| Block type changes (same key) | Template wins entirely, no content merge |
| Duplicate keys | Second occurrence is treated as unkeyed |
| No keys in template | Full replacement (same behavior as without keys) |
| Force sync | Full replacement, keys are ignored |
| Locked post | No 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.
{
"name": "terms",
"title": "Terms of Service",
"templateLock": "all"
}<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.
{
"name": "landing",
"title": "Landing Page",
"templateLock": "all",
"blockEditingMode": "disabled"
}<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.
<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><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.
{
"name": "blog",
"title": "Blog",
"sync": false
}<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:
<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:
register_post_meta( 'page', 'hero_title', array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'default' => 'Default Title',
) );Keys vs. bindings
| Keys | Bindings | |
|---|---|---|
| Content stored in | Block markup (post_content) | Post meta (wp_postmeta) |
| Survives template sync | Yes (merged by key) | Yes (data lives in meta) |
Queryable via WP_Query | No | Yes (meta_query) |
| Available in REST API | No | Yes |
| Requires registration | No | Yes (register_post_meta) |
| User can move blocks | No (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:
{
"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');