Written by
Dennis
At
Sat Mar 14 2026
Blockstudio 7.1
RPC, database, cron, components, and more new features turning blocks into full-stack applications.
Blockstudio 7.1 is the first feature release since the v7 rewrite. Where v7 rebuilt the foundation, 7.1 adds the pieces that turn blocks into something more: a database layer, server functions, scheduled tasks, and a CLI. A block folder can now be a complete application.
Here's everything that's new.
Components
There's always been a tension between blocks that editors use in the inserter and blocks that developers use as building blocks for other things. A card, a badge, a stat widget, a form input. They need the full Blockstudio pipeline (attributes, templates, assets, Tailwind) but have no business showing up in the inserter.
7.1 introduces components to solve this. Set "component": true in your
block.json and the block goes through the entire build pipeline but never gets
registered with WordPress's block type registry. It doesn't appear in the
inserter, doesn't show up in the editor data sent to JavaScript, but is fully
available for programmatic rendering.
{
"name": "my-theme/card",
"title": "Card",
"blockstudio": {
"component": true,
"attributes": [
{ "id": "title", "type": "text" },
{ "id": "description", "type": "textarea" }
]
}
}Render them from PHP:
bs_render_block([
'name' => 'my-theme/card',
'data' => ['title' => 'My Card', 'description' => 'Card content.'],
]);Or using the new HTML tag syntax:
<bs:my-theme-card title="My Card" description="Card content." />Block tags
Blocks can now be embedded as HTML tags using two interchangeable syntaxes:
<bs:acme-hero title="Welcome" />
<block name="acme/hero" title="Welcome" />Both produce the same output. Both work with Blockstudio blocks and core WordPress blocks. Inside block templates, they render automatically without any opt-in. This means you can compose entire layouts from other blocks directly in your template:
<div class="my-page">
<bs:mytheme-card title="Featured" />
<block name="core/separator" />
<bs:core-heading level="2">Section Title</bs:core-heading>
<block name="core/group">
<block name="core/paragraph">Inside a core group block.</block>
<bs:mytheme-cta variant="primary" />
</block>
</div>The two syntaxes nest inside each other freely. Blockstudio blocks render through the full pipeline (templates, Tailwind, assets). Core blocks render through WordPress using the same renderers that power Pages and Patterns.
For page-level rendering (post content, widget areas), enable it in
blockstudio.json:
{ "blockTags": { "enabled": true } }Allow and deny lists control which blocks can render via tags using wildcard
patterns. data-* and html-* attributes pass through to the rendered
block's root HTML element. Custom renderers can be registered via the
blockstudio/block_tags/renderers filter.
Under the hood, the tag parser is a new lightweight string scanner that
replaces DOMDocument entirely. It handles nested tags with depth tracking,
recursive container blocks, and mixed syntax in a single pass. In benchmarks,
it parses 1,000 flat tags in 2.4ms and 100 levels of nesting in 3.7ms,
consistently faster than both DOMDocument and WordPress's
WP_HTML_Tag_Processor. The same parser also handles raw HTML element
parsing for Pages and Patterns, replacing the
DOMDocument-based parser that previously handled <p>, <div>, and other
HTML elements.
New field types
Block
A new block field type that lets you reference another Blockstudio block as
an attribute. The referenced block's fields expand inline using idStructure
and overrides, exactly like custom fields. The difference: in the template,
accessing the value returns the rendered block output.
{
"blockstudio": {
"attributes": [
{
"id": "card",
"type": "block",
"block": "mytheme/card",
"idStructure": "card_{id}"
}
]
}
}If mytheme/card has heading and content fields, the host block gets
card_heading and card_content in its sidebar. In the template, {{ a.card }}
outputs the rendered card. Works with both regular blocks and components.
HTML tag
Every theme has blocks where the heading level should change depending on
context. An H1 on the homepage, an H2 on inner pages. Instead of building this
as a select field with hardcoded options every time, there's now a dedicated
html-tag field type with built-in presets.
{
"id": "tag",
"type": "html-tag",
"default": "h2",
"tags": "heading",
"exclude": ["h5", "h6"]
}Presets: heading (h1-h6), text (headings + p, span, div), structural
(div, section, article, aside, main, header, footer, nav), all. Or pass a
custom array. exclude removes specific tags from any preset.
<{{ a.tag }}>{{ a.title }}</{{ a.tag }}>Database
This is the centerpiece of the release. Add a db.php file to any block
directory and Blockstudio generates five REST CRUD endpoints, validates every
write against the schema, manages the storage backend automatically, and
provides both a JavaScript client (bs.db()) and a PHP API (Db::get()).
No migration files, no controller classes, no route registration.
return [
'subscribers' => [
'storage' => 'table',
'capability' => [
'create' => true,
'read' => true,
'update' => 'edit_posts',
'delete' => 'manage_options',
],
'fields' => [
'email' => [
'type' => 'string',
'format' => 'email',
'required' => true,
'validate' => function ($value) {
if (str_ends_with($value, '@spam.com')) {
return 'This domain is blocked.';
}
return true;
},
],
'name' => ['type' => 'string', 'maxLength' => 100],
'plan' => ['type' => 'string', 'enum' => ['free', 'pro']],
],
],
];That schema alone produces a fully working API with create, list, get, update, and delete endpoints, all with input validation.
Storage
Five storage backends, each suited for different use cases:
table: Custom MySQL table managed viadbDelta(). Adding fields to the schema adds columns on the next page load. The production default.sqlite: A single SQLite file in the block'sdb/folder. Real SQL queries, WAL mode for concurrent reads, and completely portable. Copy the folder, copy the database.jsonc: One JSON object per line in a flat file. Human-readable, version-controllable, git-friendly.meta: JSON array in WordPress post meta, tied to a specific post.post_type: Each record becomes a WordPress post in a custom post type. Fields are stored as individual post meta entries, fully queryable withWP_Query.
Clients
On the JavaScript side, bs.db() gives you a complete CRUD client:
const db = bs.db('my-theme/block', 'subscribers');
const record = await db.create({ email: 'a@b.com', plan: 'pro' });
const pros = await db.list({ plan: 'pro' });
await db.update(record.id, { plan: 'free' });
await db.delete(record.id);Three companion utilities ship alongside it:
bs.cache: An in-memory query cache withget,set,invalidate, andclear. Shared across all query and mutation calls.bs.query(key, fn, options): Wraps any async function with caching, request deduplication, and configurablestaleTime. Multiple calls with the same key while a request is in flight return the same promise.bs.mutate(options): Handles optimistic mutations with auto-rollback. Pass a reactive state object, an action (create,update,delete), and optimistic data. The UI updates instantly. If the server call fails, the state rolls back to its pre-mutation snapshot.
Together these cover the full read/write cycle: bs.query for cached reads,
bs.mutate for optimistic writes, bs.cache for manual control.
On the PHP side, the Db class provides the same operations:
use Blockstudio\Db;
$db = Db::get('my-theme/block', 'subscribers');
$db->create(['email' => 'a@b.com', 'plan' => 'pro']);
$db->list(['plan' => 'pro']);Validation
Validation errors come back as per-field error arrays, inspired by Zod's
fieldErrors format. Multiple rules can fail on the same field, and the
structure makes it easy to map errors to form fields on the frontend:
{
"data": {
"status": 400,
"errors": {
"email": ["Must be a valid email address.", "This domain is blocked."],
"plan": ["Must be one of: free, pro."]
}
}
}Custom validate callbacks run server-side for anything the built-in rules
can't cover: domain blocking, cross-field validation, external lookups.
User-scoped data
For per-user data like todos, favorites, or drafts, set userScoped to true
and Blockstudio handles the rest. It adds a user_id column, sets it
automatically on create, and filters every query to the current user. Updates
and deletes check ownership. No manual user_id field, no before_create
hook, no filtering in RPC. One line of config. When using post_type storage,
userScoped maps to post_author instead of a separate column.
return [
'storage' => 'sqlite',
'userScoped' => true,
'fields' => [ /* ... */ ],
];Realtime polling
Set 'realtime' => true in your schema and the client automatically polls
for changes. When something changes on the server, the Interactivity API store
updates and the UI re-renders without any JavaScript changes on your end.
Configure with 'realtime' => ['key' => 'todos', 'interval' => 5000] for
custom state keys and intervals. Polling uses hash comparison to keep requests
lightweight and pauses when the browser tab is hidden.
Security, hooks, portability
Public endpoints are protected by an auto-injected X-BS-Token CSRF header.
Use 'open' when you need truly unauthenticated access, for example for
incoming webhooks.
Lifecycle hooks fire before and after every write operation. Define them inline
in db.php or globally via WordPress actions.
Fields can define a component key for schema-driven form rendering via
bs_db_form().
With SQLite or JSONC storage, the entire application lives in one folder: the code, the logic, the access control, and the data. Copy it to deploy.
RPC
Sometimes CRUD isn't enough. You need custom server logic: toggling a todo,
sending a notification, aggregating stats. That's what rpc.php is for. Define
a PHP function, call it from the frontend with bs.fn(), and Blockstudio
handles the REST endpoint, authentication, CSRF protection, and JSON
serialization. The pattern is inspired by tRPC: procedures
defined server-side, called from the client by name.
return [
'subscribe' => function (array $params): array {
$email = sanitize_email($params['email']);
return ['success' => true];
},
];const result = await bs.fn('subscribe', { email: 'user@example.com' });Inline scripts (script-inline.js) auto-detect the block name from the
data-block attribute on their script tag, so you don't need to pass it
manually. Module scripts pass it as the third argument.
Access control options include public (with CSRF), 'open' (no protection),
capability strings, or arrays. Per-function HTTP method control, lifecycle
hooks, and cross-block calling from both JS and PHP are all supported.
Cron
Background tasks are defined in a cron.php file. Set a schedule and a
callback, and Blockstudio registers them with WordPress Cron on init. When the
plugin is deactivated, everything is unscheduled cleanly.
return [
'cleanup' => [
'schedule' => 'daily',
'callback' => function () {
$db = Db::get('my-theme/app', 'logs');
// delete old entries
},
],
'sync' => [
'schedule' => 'hourly',
'callback' => function () {
// pull from external API
},
],
];All built-in WordPress schedules are supported, plus custom intervals via the
cron_schedules filter.
CLI
A new wp bs command covers the full Blockstudio feature set: blocks,
database, RPC, cron, settings, Tailwind compilation, SCSS compilation, custom
fields, and asset management.
wp bs blocks list --components
wp bs db schemas
wp bs db create my-theme/app subs --email=a@b.com
wp bs db list my-theme/app subs --plan=pro --format=json
wp bs rpc call my-theme/app subscribe --email=a@b.com
wp bs cron run my-theme/app cleanup
wp bs tailwind compile --file=template.html
wp bs scss compile --file=style.scss
wp bs fields list
wp bs assets list --type=global
wp bs settings get tailwind/enabledAll commands support --format=json for scripting.
Repeater improvements
textMinimized for object fields
Collapsed repeater rows now handle object field values properly. Previously,
pointing textMinimized at a link field showed [object Object]. Now
Blockstudio auto-reads the right sub-property based on the field type: title
for links, value for colors and gradients, icon for icons. You can also set
an explicit key property to read any sub-property you want.
{
"type": "repeater",
"textMinimized": { "id": "href", "fallback": "Add a link" },
"attributes": [{ "id": "href", "type": "link" }]
}RichText and MediaPlaceholder
Both template components now work inside repeater fields with bracket notation to target the correct row. Inline-editable headings, paragraphs, and media upload zones in each row of a repeater.
{% for item in a.items %}
<div class="card">
<MediaPlaceholder attribute="items[{{ loop.index0 }}].image" />
<RichText tag="h2" attribute="items[{{ loop.index0 }}].heading" />
<RichText tag="p" attribute="items[{{ loop.index0 }}].content" />
</div>
{% endfor %}Custom field conditions with idStructure
When using a custom field with idStructure, conditions inside the field
definition now automatically rewrite their IDs to match the expanded names. If
your font-size field group has a condition on enable, and you expand it with
idStructure: "title_font_{id}", the condition now correctly references
title_font_enable instead of the original enable.
Reference-level conditions are also supported. Add conditions on the custom
field reference to apply them to every expanded field at once.
Dynamic populate arguments
Populate arguments can now reference other block attributes using
{attributes.*} syntax. When the referenced attribute changes, the select
field re-fetches its options automatically. Chain selects together: one for
choosing a post type, another that loads posts of that type.
{
"id": "selected_post",
"type": "select",
"populate": {
"type": "query",
"query": "posts",
"arguments": {
"post_type": "{attributes.post_type}"
}
}
}Change post_type to "page" and the second select loads pages. Change it to
"post" and it loads posts. Works with any populate type and any attribute.
Composer
Blockstudio now works when loaded from anywhere under wp-content/, not just
the plugins directory. Asset URLs are resolved via a BLOCKSTUDIO_URL constant
instead of plugins_url(), so theme-bundled installs work out of the box:
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/vendor/blockstudio/blockstudio/blockstudio.php';The composer.json runtime dependencies have been cleaned up. Scoped packages
(TailwindPHP, ScssPhp, Minify) that are already bundled in lib/ are no
longer listed as runtime dependencies. Only php >= 8.2 and
plugin-update-checker remain.
All three install methods (plugin zip, plugin via Composer, theme-bundled via Composer) are tested in CI with full WordPress activation, block registration, and frontend rendering verification.
See the Composer documentation for setup instructions.
Performance profiler
Add ?blockstudio-perf to any page URL and a debug panel appears at the
bottom with per-block render times, phase breakdowns, and cache hit rates.
Timing data is also sent as Server-Timing headers, visible in the browser
DevTools Network tab.
{ "dev": { "perf": true } }Enable it permanently via settings to profile every page load. Only visible to
logged-in users with edit_posts capability.
Self-closing block tags are also cached in memory now. When the same block with identical attributes appears multiple times on a page, subsequent renders skip the entire template pipeline.
Bug fixes
- PHP 8.4 deprecation warnings: Fixed
strpos()andstr_replace()deprecation warnings caused by passingnulltoadd_submenu_page().
More
- SCSS code fields: Code fields support
language: "scss". Values are compiled through ScssPhp in both the editor and on the frontend. - Standard WordPress blocks: Blockstudio directories can contain
@wordpress/create-blockoutput alongside Blockstudio blocks. They get auto-registered without any separate plugin code.
If you want to see the full-stack pattern in action, the Full-Stack Blocks guide walks through building a per-user todo app and a public newsletter signup from scratch.