• Dynamic Block Shows Validation Error Only in FSE, Works Fine in Post Editor

    I’m facing a frustrating issue with a custom dynamic block that shows “Block contains unexpected or invalid content” error, but only in the Full Site Editor (FSE). The exact same block works perfectly without any errors in the regular post/page editor.

    The block is a simple social sharing widget registered as a dynamic block with save: () => null in JavaScript and a render.php file for server-side rendering. It’s properly registered in PHP using register_block_type() with the block.json file. The attributes are three boolean values for showing/hiding different social platforms.

    What’s puzzling is that in the FSE template code editor, the block appears correctly as a self-closing comment <!-- wp:estylish/estylish-share /-->, which is the expected format for dynamic blocks. However, FSE still throws the validation error and shows “Attempt recovery” button. When I click it, nothing changes – the error persists.

    I’ve tried everything I can think of: cleared all caches, fixed character encoding issues, added deprecations, verified the save function returns null, ensured PHP and JS registrations match, deleted and re-added the block in templates, set html support to false in block.json, and rebuilt the project multiple times. The error only appears in FSE templates and template parts, never in regular posts or pages.

    Has anyone encountered this issue where a dynamic block validates correctly in the post editor but fails validation in FSE? Is there something different about how FSE handles dynamic blocks compared to the regular editor? I’m wondering if this could be a bug in WordPress or if there’s some undocumented requirement for dynamic blocks in FSE that I’m missing.

    Any help or insights would be greatly appreciated as I’ve been stuck on this for days and it’s blocking our theme development.

    // ============================================
    // FILE: index.js
    // ============================================

    import { registerBlockType } from “@wordpress/blocks”;
    import { useBlockProps, InspectorControls } from “@wordpress/block-editor”;
    import { PanelBody, ToggleControl } from “@wordpress/components”;
    import { __ } from “@wordpress/i18n”;
    import metadata from “./block.json”;

    // Register the block
    registerBlockType(metadata.name, {
    // Explicitly set all properties to match block.json
    title: metadata.title,
    description: metadata.description,
    category: metadata.category,
    icon: metadata.icon,
    supports: metadata.supports,
    attributes: metadata.attributes,
    example: metadata.example,

    edit: ({ attributes, setAttributes }) => {
        const { showTwitter, showFacebook, showPinterest } = attributes;
        const blockProps = useBlockProps({
            className: 'estylish-share-editor'
        });
    
        return (
            <>
                <InspectorControls>
                    <PanelBody title={__('Social Platforms', 'estylish')}>
                        <ToggleControl
                            label={__('Show Twitter', 'estylish')}
                            checked={showTwitter}
                            onChange={(value) => setAttributes({ showTwitter: value })}
                        />
                        <ToggleControl
                            label={__('Show Facebook', 'estylish')}
                            checked={showFacebook}
                            onChange={(value) => setAttributes({ showFacebook: value })}
                        />
                        <ToggleControl
                            label={__('Show Pinterest', 'estylish')}
                            checked={showPinterest}
                            onChange={(value) => setAttributes({ showPinterest: value })}
                        />
                    </PanelBody>
                </InspectorControls>
    
                <div {...blockProps}>
                    <div
                        className="sonchor"
                        style={{ padding: "20px", backgroundColor: "#f5f5f5" }}
                    >
                        <div className="share">
                            <span>&mdash;</span> {__('Share', 'estylish')}
                        </div>
                        <div style={{ marginTop: "10px" }}>
                            {showTwitter && (
                                <span style={{ marginRight: "10px" }}>
                                    {__('Twitter', 'estylish')}
                                </span>
                            )}
                            {showFacebook && (
                                <span style={{ marginRight: "10px" }}>
                                    {__('Facebook', 'estylish')}
                                </span>
                            )}
                            {showPinterest && (
                                <span>{__('Pinterest', 'estylish')}</span>
                            )}
                        </div>
                        <p style={{ fontSize: "12px", color: "#666", marginTop: "10px" }}>
                            {__('Social share buttons will display on frontend', 'estylish')}
                        </p>
                    </div>
                </div>
            </>
        );
    },
    
    // Dynamic block - must return null
    save: () => null

    });

    // ============================================
    // FILE: block.json
    // ============================================
    /*
    {
    “$schema”: “https://schemas.wp.org/trunk/block.json&#8221;,
    “apiVersion”: 3,
    “name”: “estylish/estylish-share”,
    “version”: “1.0.0”,
    “title”: “Estylish Share”,
    “category”: “widgets”,
    “icon”: “share”,
    “description”: “Social sharing widget with Twitter, Facebook and Pinterest”,
    “keywords”: [“share”, “social”, “twitter”, “facebook”, “pinterest”],
    “textdomain”: “estylish”,
    “example”: {
    “attributes”: {
    “showTwitter”: true,
    “showFacebook”: true,
    “showPinterest”: true
    }
    },
    “supports”: {
    “html”: false,
    “align”: false,
    “anchor”: false,
    “customClassName”: false,
    “className”: false,
    “reusable”: true,
    “inserter”: true
    },
    “attributes”: {
    “showTwitter”: {
    “type”: “boolean”,
    “default”: true
    },
    “showFacebook”: {
    “type”: “boolean”,
    “default”: true
    },
    “showPinterest”: {
    “type”: “boolean”,
    “default”: true
    }
    },
    “editorScript”: “file:./index.js”,
    “render”: “file:./render.php”
    }
    */

    // ============================================
    // FILE: render.php
    // ============================================
    /*
    <?php
    /**

    • Estylish Share Block – Server-side rendering
      • Available variables:
    • $attributes (array): Block attributes
    • $content (string): Block inner content
    • $block (WP_Block): Block instance
      */

    // Ensure we’re in WordPress context
    if (!defined(‘ABSPATH’)) {
    exit;
    }

    // Get attributes with proper defaults
    $show_twitter = isset($attributes[‘showTwitter’]) ? (bool) $attributes[‘showTwitter’] : true;
    $show_facebook = isset($attributes[‘showFacebook’]) ? (bool) $attributes[‘showFacebook’] : true;
    $show_pinterest = isset($attributes[‘showPinterest’]) ? (bool) $attributes[‘showPinterest’] : true;

    // Get current post data safely
    $post_id = get_the_ID();
    if (!$post_id) {
    // If no post context, don’t render
    return ”;
    }

    $post_url = esc_url(get_permalink($post_id));
    $post_title = get_the_title($post_id);
    $post_thumbnail_url = get_the_post_thumbnail_url($post_id, ‘large’);

    // Build sharing URLs
    $twitter_url = add_query_arg([
    ‘url’ => $post_url,
    ‘text’ => $post_title
    ], ‘https://twitter.com/intent/tweet&#8217;);

    $facebook_url = add_query_arg([
    ‘u’ => $post_url
    ], ‘https://www.facebook.com/sharer/sharer.php&#8217;);

    $pinterest_url = add_query_arg([
    ‘url’ => $post_url,
    ‘media’ => $post_thumbnail_url ?: ”,
    ‘description’ => $post_title
    ], ‘https://pinterest.com/pin/create/button/&#8217;);

    // Start output
    ?>

    */

Viewing 1 replies (of 1 total)
  • Hi @woracious,

    You’ve clearly put a ton of effort into this — kudos for the thorough setup and troubleshooting so far! Here’s a breakdown of what might be going on and a few approaches you could try: Possible Causes & Troubleshooting Steps

    1. FSE enforces stricter validation than the post editor
    The Full Site Editor (FSE) may process dynamic blocks differently, especially handling self-closing comments like <!-- wp:estylish/estylish-share /--> with more caution. Even if your dynamic block is technically correct, FSE might reject it due to stricter parsing rules or mismatches in expected block structure.

    2. Mismatched registration between JS and PHP
    Double-check that every property defined in your JavaScript — including attributes, supports, and example — aligns exactly with your PHP registration. Even minor differences in default values or attribute naming can trip validation.

    3. Block deprecation or update handling
    Since FSE may load template parts across versions, consider defining deprecated versions of the block that gracefully fallback if the structure changes over time. This often helps bridge compatibility gaps.

    4. html: false isn’t always sufficient
    While setting "html": false in block.json typically suppresses validation errors, FSE sometimes ignores this or applies it inconsistently. You might temporarily toggle this option to see if it fixes the rendering in FSE. Recommended Next Steps

    • Compare serialized output
      Copy the block HTML from a post (where it works), and do the same in FSE. Compare them character by character — whitespace, casing, and encoding can all cause FSE to flag content.
    • Log rendered output visually
      Temporarily add debug output inside your render.php to log or display what the server is outputting. It could point out unexpected inline characters or formatting.
    • Test with a minimal setup
      Create a simplified version of your dynamic block — maybe just a single ToggleControl or even none. If the minimal block validates properly in FSE, you can iteratively add features back to locate the precise trigger.
    • Look into block deprecations
      Define a deprecated version with slightly different markup to satisfy FSE’s expectations. You can list it under deprecations in your block.json.


    • Quick Example of Minimal Block for Debugging
    • // index.js registerBlockType(metadata.name, { ...metadata, supports: { html: false }, save: () => null, edit: () => <div {...useBlockProps()}>Test Block</div>, }); Sample Check in render.php
    • error_log('Estylish block render called'); return '<div class="estylish-share">Rendered correctly!</div>';
    • If FSE still balks at this minimal output, the issue is likely with editor-level expectations.
    • You’re so close — hang tight! Let me know what you discover after trying any of these steps, and we’ll keep narrowing it down until it works seamlessly in FSE.
Viewing 1 replies (of 1 total)

You must be logged in to reply to this topic.