• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to build custom FSE Block Themes extensions utilizing modern Shortcode API schemas

How to build custom FSE Block Themes extensions utilizing modern Shortcode API schemas

Leveraging Shortcode API Schemas for FSE Block Theme Extensions

Full Site Editing (FSE) in WordPress has fundamentally shifted theme development towards a block-based paradigm. While block development offers immense flexibility, there are scenarios where integrating existing shortcode-driven functionality or creating reusable components that bridge the gap between classic PHP-based logic and modern block interfaces becomes necessary. This post details how to build custom FSE block theme extensions by effectively utilizing and extending the Shortcode API, specifically focusing on modern schema definitions for enhanced interoperability and maintainability.

Understanding the Shortcode API and FSE Context

The WordPress Shortcode API, historically used for embedding dynamic content within posts and pages via simple tags like [my_shortcode], can be a powerful tool even in an FSE environment. The key is to register shortcodes that output HTML compatible with block rendering, or even better, shortcodes that dynamically generate block markup. FSE themes rely on blocks for structure, content, and presentation. By creating shortcodes that output valid block markup or integrate with block rendering processes, we can extend FSE capabilities without abandoning established PHP logic.

Registering a Basic Shortcode for Block Theme Integration

Let’s start by registering a simple shortcode that outputs a basic HTML structure. This shortcode will be placed within a “Custom HTML” block or a block that allows raw HTML input in the Site Editor.

Create a custom plugin or add this to your theme’s functions.php file (though a plugin is recommended for better separation of concerns).

Plugin Structure (Example)

  • /my-fse-extensions/ (Plugin directory)
    • my-fse-extensions.php (Main plugin file)
    • includes/
      • shortcodes.php

my-fse-extensions.php

<?php
/**
 * Plugin Name: My FSE Extensions
 * Description: Custom extensions for FSE themes.
 * Version: 1.0.0
 * Author: Your Name
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

// Include shortcode functionality.
require_once plugin_dir_path( __FILE__ ) . 'includes/shortcodes.php';
?>

includes/shortcodes.php

<?php
/**
 * Shortcode registration and handling.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Registers the 'featured_product' shortcode.
 */
function my_fse_extensions_register_shortcodes() {
    add_shortcode( 'featured_product', 'my_fse_extensions_render_featured_product' );
}
add_action( 'init', 'my_fse_extensions_register_shortcodes' );

/**
 * Renders the 'featured_product' shortcode output.
 *
 * @param array $atts Shortcode attributes.
 * @return string HTML output.
 */
function my_fse_extensions_render_featured_product( $atts ) {
    // Default attributes.
    $atts = shortcode_atts(
        array(
            'id'    => 0,
            'title' => __( 'Featured Product', 'my-fse-extensions' ),
        ),
        $atts,
        'featured_product'
    );

    $product_id = absint( $atts['id'] );
    $title      = sanitize_text_field( $atts['title'] );

    if ( ! $product_id ) {
        return '<p>' . esc_html__( 'Please specify a product ID.', 'my-fse-extensions' ) . '</p>';
    }

    // In a real scenario, you'd fetch product data here (e.g., from WooCommerce).
    // For this example, we'll simulate it.
    $product_data = array(
        'name'        => 'Awesome Gadget',
        'price'       => '$99.99',
        'description' => 'This is an amazing gadget that will change your life.',
        'image_url'   => 'https://via.placeholder.com/150', // Placeholder image.
    );

    ob_start();
    ?>
    <div class="featured-product-shortcode">
        <h3><?php echo esc_html( $title ); ?></h3>
        <img src="<?php echo esc_url( $product_data['image_url'] ); ?>" alt="<?php echo esc_attr( $product_data['name'] ); ?>" width="150" height="150" />
        <h4><?php echo esc_html( $product_data['name'] ); ?></h4>
        <p class="price"><?php echo esc_html( $product_data['price'] ); ?></p>
        <p><?php echo esc_html( $product_data['description'] ); ?></p>
        <a href="#" class="button"><?php esc_html_e( 'View Product', 'my-fse-extensions' ); ?></a>
    </div>
    <style>
        .featured-product-shortcode {
            border: 1px solid #ccc;
            padding: 20px;
            margin-bottom: 20px;
            text-align: center;
            background-color: #f9f9f9;
        }
        .featured-product-shortcode h3 {
            margin-top: 0;
            color: #333;
        }
        .featured-product-shortcode .price {
            font-weight: bold;
            color: #0073aa;
        }
        .featured-product-shortcode .button {
            display: inline-block;
            margin-top: 10px;
            padding: 10px 20px;
            background-color: #0073aa;
            color: #fff;
            text-decoration: none;
            border-radius: 4px;
        }
    </style>
    


With this plugin activated, you can now go to the Site Editor, add a "Custom HTML" block, and insert [featured_product id="123" title="Our Latest Deal"]. The output will be rendered as HTML within the block.

Schema-Driven Shortcodes for Block Generation

The real power comes when shortcodes can generate actual block markup. This allows them to be treated more like native blocks within the FSE context, respecting block attributes and rendering more dynamically. We can achieve this by having our shortcode render a string that represents block JSON.

Modifying the Shortcode to Output Block Markup

Let's refactor the my_fse_extensions_render_featured_product function to output a block's JSON representation. This requires understanding the structure of block attributes and the block's `save` function output.

<?php
/**
 * Renders the 'featured_product' shortcode output as block markup.
 *
 * @param array $atts Shortcode attributes.
 * @return string Block markup string.
 */
function my_fse_extensions_render_featured_product_as_block( $atts ) {
    // Default attributes.
    $atts = shortcode_atts(
        array(
            'id'    => 0,
            'title' => __( 'Featured Product', 'my-fse-extensions' ),
        ),
        $atts,
        'featured_product'
    );

    $product_id = absint( $atts['id'] );
    $title      = sanitize_text_field( $atts['title'] );

    if ( ! $product_id ) {
        return '<p>' . esc_html__( 'Please specify a product ID.', 'my-fse-extensions' ) . '</p>';
    }

    // Simulate fetching product data.
    $product_data = array(
        'name'        => 'Awesome Gadget',
        'price'       => '$99.99',
        'description' => 'This is an amazing gadget that will change your life.',
        'image_url'   => 'https://via.placeholder.com/150',
    );

    // Construct block attributes.
    $block_attributes = array(
        'id'          => $product_id,
        'title'       => $title,
        'productName' => $product_data['name'],
        'price'       => $product_data['price'],
        'description' => $product_data['description'],
        'imageUrl'    => $product_data['image_url'],
        // Add any other attributes that your block's save function expects.
    );

    // The block name for our hypothetical block.
    // This assumes you have a block registered with this name.
    // If not, you'd need to register a block type first.
    // For demonstration, let's assume a block named 'my-fse-extensions/featured-product'.
    $block_name = 'my-fse-extensions/featured-product';

    // Generate the block's inner HTML.
    // This part is crucial: it mimics what a block's save function would output.
    // In a real scenario, this would be the output of your block's save function.
    $inner_html = '<div class="featured-product-block-content">';
    $inner_html .= '<h3>' . esc_html( $block_attributes['title'] ) . '</h3>';
    $inner_html .= '<img src="' . esc_url( $block_attributes['imageUrl'] ) . '" alt="' . esc_attr( $block_attributes['productName'] ) . '" width="150" height="150" />';
    $inner_html .= '<h4>' . esc_html( $block_attributes['productName'] ) . '</h4>';
    $inner_html .= '<p class="price">' . esc_html( $block_attributes['price'] ) . '</p>';
    $inner_html .= '<p>' . esc_html( $block_attributes['description'] ) . '</p>';
    $inner_html .= '<a href="#" class="button">' . esc_html__( 'View Product', 'my-fse-extensions' ) . '</a>';
    $inner_html .= '</div>';

    // Construct the block's JSON representation.
    $block_json = array(
        'blockName'    => $block_name,
        'attrs'        => $block_attributes,
        'innerBlocks'  => array(), // No inner blocks for this example.
        'innerHTML'    => $inner_html, // The rendered HTML for the block.
        'innerContent' => array( $inner_html ), // For compatibility.
    );

    // Encode the block as JSON and wrap it in a comment for WordPress to parse.
    // This is a common technique for dynamic blocks that are rendered via shortcodes.
    // The comment `` is how WordPress identifies blocks.
    return '<!-- wp:block ' . wp_json_encode( $block_json ) . '-->';
}

// Re-register the shortcode to use the new rendering function.
// In a real scenario, you might want to use a different shortcode name
// or conditionally load this if you're targeting FSE specifically.
remove_shortcode( 'featured_product' );
add_shortcode( 'featured_product', 'my_fse_extensions_render_featured_product_as_block' );

// Note: For this to work seamlessly, you would ideally have a corresponding
// block type registered with the name 'my-fse-extensions/featured-product'.
// The shortcode acts as a bridge, allowing shortcode attributes to populate
// a block's attributes and render its structure.
?>

In this modified version:

  • We define $block_attributes that mirror the expected attributes of a hypothetical block named my-fse-extensions/featured-product.
  • We construct $inner_html, which represents the *saved* HTML output of that block. This is what the block's save function would return.
  • We then create a $block_json array, conforming to the WordPress block structure (blockName, attrs, innerHTML).
  • Finally, we wrap this JSON in a comment <!-- wp:block ... -->. WordPress's block parser recognizes this format and treats it as a rendered block.

When you use [featured_product id="123" title="Our Latest Deal"] in a block context (like a "Custom HTML" block within the Site Editor), WordPress will render this as a fully recognized block. This means it can be styled by theme.json, potentially manipulated by block editor JavaScript, and generally treated as a first-class block citizen.

Advanced: Shortcodes as Block Templates

A more advanced pattern involves using shortcodes to dynamically generate complex block structures, effectively acting as shortcode-driven block templates. This is particularly useful for creating reusable sections with dynamic content that can be inserted into various parts of an FSE theme.

Example: Dynamic Call to Action Section

Let's create a shortcode that generates a "Call to Action" section composed of multiple blocks (e.g., a Heading block, a Paragraph block, and a Button block).

<?php
/**
 * Registers the 'cta_section' shortcode.
 */
function my_fse_extensions_register_cta_shortcode() {
    add_shortcode( 'cta_section', 'my_fse_extensions_render_cta_section_as_blocks' );
}
add_action( 'init', 'my_fse_extensions_register_cta_shortcode' );

/**
 * Renders a CTA section composed of multiple blocks.
 *
 * @param array $atts Shortcode attributes.
 * @return string Block markup string.
 */
function my_fse_extensions_render_cta_section_as_blocks( $atts ) {
    $atts = shortcode_atts(
        array(
            'heading'     => __( 'Ready to Get Started?', 'my-fse-extensions' ),
            'description' => __( 'Sign up today and experience the difference.', 'my-fse-extensions' ),
            'button_text' => __( 'Sign Up Now', 'my-fse-extensions' ),
            'button_url'  => '#',
        ),
        $atts,
        'cta_section'
    );

    $heading     = sanitize_text_field( $atts['heading'] );
    $description = sanitize_textarea_field( $atts['description'] );
    $button_text = sanitize_text_field( $atts['button_text'] );
    $button_url  = esc_url_raw( $atts['button_url'] );

    // Define the blocks we want to generate.
    $blocks = array();

    // Heading Block
    $blocks[] = array(
        'blockName' => 'core/heading',
        'attrs'     => array(
            'level' => 2,
            'content' => $heading,
        ),
        'innerBlocks' => array(),
        'innerHTML'   => '<h2>' . esc_html( $heading ) . '</h2>',
    );

    // Paragraph Block
    $blocks[] = array(
        'blockName' => 'core/paragraph',
        'attrs'     => array(
            'content' => $description,
        ),
        'innerBlocks' => array(),
        'innerHTML'   => '<p>' . esc_html( $description ) . '</p>',
    );

    // Button Block
    $blocks[] = array(
        'blockName' => 'core/button',
        'attrs'     => array(
            'text' => $button_text,
            'url'  => $button_url,
            'linkTarget' => '_blank', // Example attribute
        ),
        'innerBlocks' => array(),
        // The innerHTML for a button block is typically just its text content,
        // but the actual rendering is handled by the block's save function.
        // For simplicity here, we'll render a basic link.
        'innerHTML'   => '<a href="' . esc_url( $button_url ) . '" class="wp-block-button__link">' . esc_html( $button_text ) . '</a>',
    );

    // Wrap the blocks in a container block for better structure and styling.
    // We'll use a 'core/group' block.
    $container_block = array(
        'blockName'   => 'core/group',
        'attrs'       => array(
            'className' => 'wp-block-my-fse-cta-section', // Custom class for styling
            'layout'    => array( // Example layout attribute
                'type' => 'constrained',
            ),
        ),
        'innerBlocks' => $blocks, // Our previously defined blocks go here.
        // innerHTML will be generated by WordPress when it parses the innerBlocks.
    );

    // Encode the container block and its inner blocks into a single comment.
    // This comment represents the entire structure as a single block.
    return '<!-- wp:group ' . wp_json_encode( $container_block ) . '-->';
}
?>

When you use [cta_section heading="Join Our Community" description="Connect with like-minded individuals." button_text="Explore Forums" button_url="/forums/"] in a "Custom HTML" block within the Site Editor, WordPress will parse this comment and render it as a group block containing a heading, paragraph, and button block, all populated with your shortcode's attributes.

Schema Validation and Attribute Handling

For robust shortcodes that generate block markup, it's crucial to treat shortcode attributes as if they were block attributes. This means:

  • Sanitization: Always sanitize all attribute values using appropriate WordPress functions (e.g., sanitize_text_field, esc_url_raw, wp_kses_post).
  • Validation: Validate attribute types and values. For example, ensure numeric IDs are integers, URLs are valid, etc.
  • Defaults: Provide sensible default values for all attributes, mimicking block defaults.
  • Type Casting: Cast attributes to their expected types (e.g., absint() for integers).

The shortcode_atts() function is your primary tool for merging user-provided attributes with defaults and performing basic sanitization. For more complex validation, you might need custom logic.

Integrating with Block Registration (Optional but Recommended)

While the shortcode-to-block-markup approach works, it's often more maintainable and user-friendly to register a proper custom block type alongside your shortcode. The shortcode can then act as a fallback or an alternative way to insert the block's content, especially if you have existing shortcode-based workflows.

If you were to register a block named my-fse-extensions/featured-product, its save function would ideally produce the same HTML structure as the $inner_html we generated in the shortcode. This ensures consistency.

Example Block Registration Snippet (blocks.php)

<?php
/**
 * Registers custom blocks.
 */
function my_fse_extensions_register_blocks() {
    // Register the Featured Product block.
    register_block_type( 'my-fse-extensions/featured-product', array(
        'attributes' => array(
            'id'          => array( 'type' => 'number', 'default' => 0 ),
            'title'       => array( 'type' => 'string', 'default' => __( 'Featured Product', 'my-fse-extensions' ) ),
            'productName' => array( 'type' => 'string', 'default' => '' ),
            'price'       => array( 'type' => 'string', 'default' => '' ),
            'description' => array( 'type' => 'string', 'default' => '' ),
            'imageUrl'    => array( 'type' => 'string', 'default' => '' ),
        ),
        'render_callback' => 'my_fse_extensions_render_featured_product_block', // For dynamic rendering
        // If you have a static save function, you'd define it here.
        // For simplicity, we'll use a render_callback.
    ) );

    // Register the CTA Section block.
    register_block_type( 'my-fse-extensions/cta-section', array(
        'attributes' => array(
            'heading'     => array( 'type' => 'string', 'default' => __( 'Ready to Get Started?', 'my-fse-extensions' ) ),
            'description' => array( 'type' => 'string', 'default' => __( 'Sign up today and experience the difference.', 'my-fse-extensions' ) ),
            'button_text' => array( 'type' => 'string', 'default' => __( 'Sign Up Now', 'my-fse-extensions' ) ),
            'button_url'  => array( 'type' => 'string', 'default' => '#' ),
            'className'   => array( 'type' => 'string', 'default' => 'wp-block-my-fse-cta-section' ),
        ),
        'render_callback' => 'my_fse_extensions_render_cta_section_block',
    ) );
}
add_action( 'init', 'my_fse_extensions_register_blocks' );

/**
 * Render callback for the Featured Product block.
 */
function my_fse_extensions_render_featured_product_block( $attributes ) {
    $product_id = absint( $attributes['id'] );
    $title      = sanitize_text_field( $attributes['title'] );
    $name       = sanitize_text_field( $attributes['productName'] );
    $price      = sanitize_text_field( $attributes['price'] );
    $desc       = sanitize_textarea_field( $attributes['description'] );
    $img_url    = esc_url_raw( $attributes['imageUrl'] );

    if ( ! $product_id ) {
        return '<p>' . esc_html__( 'Please specify a product ID.', 'my-fse-extensions' ) . '</p>';
    }

    // Fetch actual product data here if needed, or use attributes directly.
    // For this example, we'll use the attributes.

    ob_start();
    ?>
    <div class="featured-product-block">
        <h3><?php echo esc_html( $title ); ?></h3>
        <img src="<?php echo esc_url( $img_url ); ?>" alt="<?php echo esc_attr( $name ); ?>" width="150" height="150" />
        <h4><?php echo esc_html( $name ); ?></h4>
        <p class="price"><?php echo esc_html( $price ); ?></p>
        <p><?php echo esc_html( $desc ); ?></p>
        <a href="#" class="button"><?php esc_html_e( 'View Product', 'my-fse-extensions' ); ?></a>
    </div>
    <style>
        /* Styles for the block */
        .featured-product-block { border: 1px solid #eee; padding: 15px; text-align: center; }
        .featured-product-block .price { font-weight: bold; color: #0073aa; }
        .featured-product-block .button { /* ... */ }
    </style>
    
    <div class="<?php echo esc_attr( $class_name ); ?>">
        <h2><?php echo esc_html( $heading ); ?></h2>
        <p><?php echo esc_html( $description ); ?></p>
        <a href="<?php echo esc_url( $button_url ); ?>" class="wp-block-button__link"><?php echo esc_html( $button_text ); ?></a>
    </div>
    <style>
        /* Styles for the CTA block */
        .wp-block-my-fse-cta-section { text-align: center; padding: 30px; background-color: #eaf2f8; border-radius: 8px; }
        .wp-block-my-fse-cta-section h2 { color: #0056b3; }
        .wp-block-my-fse-cta-section p { color: #333; }
        .wp-block-my-fse-cta-section .wp-block-button__link { /* ... */ }
    </style>
    


By registering actual block types, you gain the benefits of the block editor's UI, attribute management, and integration with theme.json. The shortcode then becomes a convenient way to insert these blocks with pre-defined attributes, especially when migrating legacy content or building complex dynamic layouts.

Conclusion

Extending FSE block themes with custom functionality doesn't always require a full dive into JavaScript block development. By intelligently leveraging the Shortcode API and understanding how to generate block markup, you can integrate existing PHP logic, create dynamic content components, and build powerful extensions that work seamlessly within the Full Site Editing environment. Prioritizing schema-driven attribute handling, sanitization, and validation ensures these extensions are robust, secure, and maintainable.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in online course lessons
  • WordPress Development Recipe: Secure token-based API authentication for Twilio SMS Gateway in custom plugins
  • Troubleshooting PHP-FPM child process pool exhaustion in production when using modern Carbon Fields custom wrappers wrappers
  • WordPress Development Recipe: Secure token-based API authentication for OpenAI Completion API in custom plugins
  • How to construct high-throughput import engines for large custom subscription logs sets using custom XML/JSON parsers

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (653)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (869)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (637)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (319)
  • WordPress Theme Development (357)

Recent Posts

  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in online course lessons
  • WordPress Development Recipe: Secure token-based API authentication for Twilio SMS Gateway in custom plugins
  • Troubleshooting PHP-FPM child process pool exhaustion in production when using modern Carbon Fields custom wrappers wrappers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (869)
  • Debugging & Troubleshooting (653)
  • Security & Compliance (637)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala