Blockstudio
Back to Blog

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.

block.json
{
  "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:

index.twig
<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.

block.json
{
  "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.

db.php
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 via dbDelta(). Adding fields to the schema adds columns on the next page load. The production default.
  • sqlite: A single SQLite file in the block's db/ 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 with WP_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 with get, set, invalidate, and clear. Shared across all query and mutation calls.
  • bs.query(key, fn, options): Wraps any async function with caching, request deduplication, and configurable staleTime. 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.

rpc.php
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.

cron.php
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/enabled

All 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:

functions.php
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.

blockstudio.json
{ "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() and str_replace() deprecation warnings caused by passing null to add_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-block output 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.

On this page