Dynamic blocks

Create your first dynamic block

Build a complete Hero block in a theme: registration, fields, secure PHP rendering, CSS, editor use, and saving.

#What you will build

The finished Hero block has editable heading, summary, image, button, and alignment fields. WordPress saves those values with the page, while render.php creates the frontend HTML on every render.

#1. Create this folder structure

TEXT
your-theme/
├── functions.php
├── inc/
│   └── voxycure.php
└── blocks/
    └── hero/
        ├── render.php
        └── style.css

Use the active theme or child theme. The examples below assume these exact paths.

#2. Load the Voxycure definitions

Add this once to the theme’s functions.php. get_theme_file_path() returns an absolute, child-theme-aware path.

PHP
<?php

defined('ABSPATH') || exit;

require_once get_theme_file_path('inc/voxycure.php');

#3. Register the block and its stylesheet

Paste this complete file into inc/voxycure.php. The registration callback receives $registry from Voxycure; you do not create the object yourself.

PHP
<?php

defined('ABSPATH') || exit;

use Voxyframe\Core\Registry;

add_action('voxycure/register', function (Registry $registry): void {
    $registry->register_block('hero', [
        'label'       => 'Hero',
        'description' => 'Primary page introduction.',
        'category'    => 'design',
        'icon'        => 'cover-image',
        'keywords'    => ['banner', 'header'],
        'template'    => get_theme_file_path('blocks/hero/render.php'),
        'editor_scope' => ['post_types' => ['page']],
        'fields'      => [
            [
                'field_key'    => 'heading',
                'label'        => 'Heading',
                'type'         => 'text',
                'default_value' => 'Build something useful',
            ],
            [
                'field_key' => 'summary',
                'label'     => 'Summary',
                'type'      => 'textarea',
            ],
            [
                'field_key' => 'image',
                'label'     => 'Image',
                'type'      => 'image',
            ],
            [
                'field_key' => 'primary_cta',
                'label'     => 'Primary action',
                'type'      => 'link_button',
            ],
            [
                'field_key'    => 'alignment',
                'label'        => 'Text alignment',
                'type'         => 'select',
                'default_value' => 'left',
                'location'     => 'inspector',
                'options'      => [
                    ['value' => 'left', 'label' => 'Left'],
                    ['value' => 'center', 'label' => 'Center'],
                ],
            ],
        ],
    ]);
});

add_action('after_setup_theme', function (): void {
    wp_enqueue_block_style('voxyframe/hero', [
        'handle' => 'my-theme-voxycure-hero',
        'src'    => get_theme_file_uri('blocks/hero/style.css'),
        'path'   => get_theme_file_path('blocks/hero/style.css'),
        'ver'    => wp_get_theme()->get('Version'),
    ]);
});

#Registration fields explained

KeyValue in this guidePurpose
heroStable block IDCreates the WordPress block name voxyframe/hero.
labelHeroName developers and editors see in the inserter.
descriptionShort explanationHelps editors choose the correct block.
categorydesignPlaces it in the Design inserter category.
iconcover-imageUses a WordPress Dashicon slug.
keywordsbanner, headerAdditional inserter search terms.
templateAbsolute PHP pathThe trusted PHP file that renders the block.
editor_scopePagesOffers this Hero only in the block editor for the page post type.
fieldsField definitionsBuilds editor controls and the block attribute schema.
locationinspectorMoves alignment to the editor settings sidebar; other fields remain in the block.

#Choose where the block appears

category only groups a block inside the inserter. It does not make a block a page, product, or WooCommerce block. Use editor_scope to control which WordPress editors offer it.

Scope keyMatches
post_typesPost, page, product, or any public custom post-type slug.
post_idsExact numeric post or page IDs.
post_slugsExact post slugs.
template_slugsA template post such as single-product, when WordPress provides that template post in the editor context.
editor_contextscore/edit-post, core/edit-site, or another WordPress block-editor context.
PHP
// Every page, but not posts or other custom post types.
'editor_scope' => [
    'post_types' => ['page'],
],

// Only one landing page. Conditions in one rule use AND logic.
'editor_scope' => [
    'post_types' => ['page'],
    'post_ids'   => [42],
],

// Projects OR products. Rules in a list use OR logic.
'editor_scope' => [
    ['post_types' => ['project']],
    ['post_types' => ['product']],
],

#4. Create the PHP render file

Paste this into blocks/hero/render.php. The framework provides $attributes; each array key matches a registered field_key.

PHP
<?php
/**
 * Hero dynamic block template.
 *
 * Available variables:
 * - $attributes: Saved block field values.
 * - $block_id:   Voxycure block ID ("hero").
 * - $block_title: Human-readable block label.
 */

defined('ABSPATH') || exit;

$heading   = (string) ($attributes['heading'] ?? '');
$summary   = (string) ($attributes['summary'] ?? '');
$image     = (array) ($attributes['image'] ?? []);
$link      = (array) ($attributes['primary_cta'] ?? []);
$alignment = (string) ($attributes['alignment'] ?? 'left');

if (!in_array($alignment, ['left', 'center'], true)) {
    $alignment = 'left';
}
?>
<section <?= get_block_wrapper_attributes([
    'class' => 'vc-hero vc-hero--' . $alignment,
]) ?>>
    <div class="vc-hero__content">
        <?php if ($heading !== '') : ?>
            <h2 class="vc-hero__heading"><?= esc_html($heading) ?></h2>
        <?php endif; ?>

        <?php if ($summary !== '') : ?>
            <p class="vc-hero__summary"><?= esc_html($summary) ?></p>
        <?php endif; ?>

        <?php if (!empty($link['url']) && !empty($link['text'])) : ?>
            <a class="vc-hero__button" href="<?= esc_url($link['url']) ?>">
                <?= esc_html($link['text']) ?>
            </a>
        <?php endif; ?>
    </div>

    <?php if (!empty($image['id'])) : ?>
        <div class="vc-hero__media">
            <?= wp_get_attachment_image((int) $image['id'], 'large') ?>
        </div>
    <?php endif; ?>
</section>

#Why the render code is safe

  • esc_html() escapes visible text.
  • esc_url() escapes the link destination.
  • wp_get_attachment_image() renders responsive WordPress image markup from a trusted attachment ID.
  • The alignment is checked against an allow-list before becoming a CSS class.
  • get_block_wrapper_attributes() preserves WordPress-generated block classes and attributes.

WordPress output escaping guide · wp_get_attachment_image()

#5. Add the block styles

Paste this into blocks/hero/style.css. The earlier wp_enqueue_block_style() call makes it available in both the editor and frontend; themes that load block assets on demand can load it only when the Hero is rendered.

CSS
.vc-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(18rem, .8fr);
    gap: clamp(2rem, 6vw, 6rem);
    align-items: center;
    padding-block: clamp(3rem, 8vw, 7rem);
}

.vc-hero--center .vc-hero__content {
    text-align: center;
}

.vc-hero__heading {
    margin: 0;
    font-size: clamp(2.5rem, 6vw, 5.5rem);
    line-height: 1;
}

.vc-hero__summary {
    max-width: 42rem;
    font-size: 1.125rem;
}

.vc-hero__button {
    display: inline-flex;
    padding: .8rem 1.2rem;
    border-radius: .4rem;
    color: #fff;
    background: #1d49c3;
    text-decoration: none;
}

.vc-hero__media img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 1rem;
}

@media (max-width: 700px) {
    .vc-hero {
        grid-template-columns: 1fr;
    }
}

WordPress: wp_enqueue_block_style()

#6. Insert and save the block

  1. Open Pages → Add New or edit an existing page.
  2. Click the + block inserter and search for Hero, banner, or header.
  3. Enter the heading and summary, choose an image, and fill in the button.
  4. Open the block settings sidebar to choose text alignment.
  5. Click Publish or Update, then view the page.

#How the values are saved

Each field becomes a typed block attribute. The Voxycure editor calls WordPress setAttributes(); WordPress serializes the attributes into the block comment inside post_content. Autosaves, revisions, undo, and the normal page update flow continue to work. Voxycure does not submit a second request or store the values in a custom table.

#Change an existing block later

  • Markup change: edit render.php; all existing Hero blocks use it immediately.
  • Design change: edit style.css.
  • New field: add its definition and read the new key with a fallback in render.php.
  • Removed field: stop rendering it first. Old saved attributes may remain harmlessly in page content.

#If the block does not appear

  • Confirm Voxycure Framework is installed and active.
  • Confirm functions.php loads inc/voxycure.php.
  • Do not place the registration inside init; use voxycure/register.
  • Confirm blocks/hero/render.php exists and the filename case matches.
  • Open the browser console and WordPress debug log for PHP or JavaScript errors.

#Official WordPress references

#When to use block.json instead

Voxycure is well suited to PHP-rendered theme sections with shared fields. Use a native block.json build when a block needs custom editor JavaScript, variations, bindings, advanced supports, or the Interactivity API.

Framework version2.0.0
PHP requirement8.0+
Documentation updatedAugust 2026