Skip to main content

4 posts tagged with "Block Theme"

View all tags

WordPress Block Theme Changes Not Taking Effect? FSE Development Troubleshooting Guide

· 7 min read

Encountered these five issues repeatedly while developing WordPress Block Themes for clients. Each one took significant debugging time. This guide covers the root causes and provides ready-to-use solutions.

TL;DR

Five issues ranked by frequency: file changes not applying (database cache overrides files), block nesting errors (unclosed comments), child theme content not rendering (missing post-content block), SVG icons disappearing (WP_Filesystem polluted by plugins), and WP-CLI mail failures (SMTP plugins don't hook in CLI). Each scenario includes copy-paste diagnostic commands.

Scenario 1: Changed Theme Files, But the Page Looks the Same

Problem

You modified theme.json, templates/*.html, or parts/*.html in your theme directory, but the page shows no change. Even after git pull updates the code, the frontend still renders the old version.

Root Cause

FSE themes have their templates and global styles saved to the database via the Site Editor—stored as wp_template, wp_template_part, and wp_global_styles custom post types. The database version takes priority over file versions. Even if you update the file, as long as a database record exists, WordPress uses that instead.

Solution

Different file types require different cleanup:

Modified contentCleanup method
templates/*.htmlClear wp_template
parts/*.htmlClear wp_template_part
theme.jsonClear wp_global_styles + flush cache
patterns/*.phpTakes effect immediately, no cleanup needed

One-command cleanup for all database template caches:

# Local Docker environment
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_template --format=ids --allow-root) --force --allow-root'
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_template_part --format=ids --allow-root) --force --allow-root'
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_global_styles --format=ids --allow-root) --force --allow-root'
docker exec wp_cli wp cache flush --allow-root

If theme.json changes still don't apply, verify the JSON has no syntax errors (e.g., trailing commas are invalid in JSON):

docker exec wp_cli wp eval 'echo json_encode(json_decode(file_get_contents(get_template_directory() . "/theme.json")));' --allow-root
# Empty output → JSON syntax error

Important

  • Never save in the Site Editor during development—this prevents database overrides
  • Before clearing in production, verify no custom modifications need to be preserved from the Site Editor
  • patterns/*.php is unaffected—Pattern registration runs through PHP code, not the database
  • Corrupted wp_global_styles JSON from the Site Editor can cause site-wide WP_Theme_JSON_Resolver errors, breaking all page styles

Scenario 2: Site Editor Shows "Attempt Recovery" and the Layout Breaks

Problem

The Site Editor shows "Attempt Recovery" for a Pattern. After saving, the page layout is completely broken. Some blocks are incorrectly nested inside others, and the hierarchy doesn't match the source code.

Root Cause

WordPress block editor uses HTML comments to mark block boundaries:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- content -->
<!-- /wp:group -->

When a container block (like wp:group or wp:columns) is missing its closing comment <!-- /wp:group -->, Gutenberg's parse_blocks() treats all subsequent blocks as children of that container. The consequences:

  1. The parent block's save output is empty
  2. Block validation fails
  3. All subsequent blocks have incorrect nesting

Solution

Diagnose: Check block tree hierarchy in the browser console:

wp.data.select('core/block-editor').getBlocks()

Inspect the returned block tree. If a group block contains blocks that shouldn't be inside it, a previous container is likely missing its closing comment.

Fix: Open the Pattern source file and verify every <!-- wp:xxx --> has a matching <!-- /wp:xxx -->. Search for <!-- wp: and <!-- /wp: and count to confirm the numbers match.

Prevention Tip

Use an editor with bracket matching (like VS Code) with a Block Comment highlight extension to catch unclosed comments immediately. For complex Patterns, write the skeleton first (all open/close comment pairs), then fill in the content.

Scenario 3: Child Theme Override Hides Page Editor Content

Problem

After creating a child theme to override a parent template, content entered in the WordPress page editor (text, images, etc.) is completely blank on the frontend. However, hardcoded Patterns in the template (hero sections, CTAs) display normally.

Root Cause

FSE templates render the page editor's post_content through the <!-- wp:post-content /--> block. If the child theme's template file doesn't include this block, WordPress has nowhere to output the page content.

The result: the template's fixed structure (header, hero, sidebar) renders correctly, but everything written in the editor is lost.

Solution

Ensure your child theme template includes the post-content block:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">

<!-- Template fixed structure (hero, sidebar, etc.) -->

<!-- wp:post-content {"layout":{"type":"constrained"}} /-->

<!-- More fixed structure (CTA, footer reference, etc.) -->

</div>
<!-- /wp:group -->

When troubleshooting "changes not showing", confirm in this order:

  1. Does the template file include <!-- wp:post-content /-->?
  2. Are you modifying the template file or the page content? They control different content areas
  3. Inline cover blocks in the template are controlled by the template file, not the database post_content

Scenario 4: SVG Icons Disappeared But the Files Are Still There

Problem

Your theme uses WP_Filesystem to read SVG icon files. Suddenly all SVG icons disappear. Accessing the SVG file URL directly returns normal content, but the icon positions on pages are empty.

Root Cause

WordPress's $wp_filesystem global defaults to WP_Filesystem_Direct (direct local file access). Some plugins (backup, security) replace $wp_filesystem with WP_Filesystem_ftpsockets or WP_Filesystem_SSH2 during initialization.

FTP/SSH adapters read files through remote connections. For local paths (like /var/www/html/wp-content/themes/...), they can't access the files correctly and return empty strings. Since the replacement happens in the global scope, all theme and plugin code using WP_Filesystem is affected.

Solution

Step 1 — Diagnose: Check the actual $wp_filesystem type:

# Local Docker environment
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root

# Returns WP_Filesystem_Direct → normal
# Returns WP_Filesystem_ftpsockets or other → polluted

Step 2 — Identify the source: Disable plugins one by one to find which one replaces the adapter:

docker exec wp_cli wp plugin deactivate <plugin-name> --allow-root
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root
# Repeat until it returns WP_Filesystem_Direct

Step 3 — Code fallback: Add file_get_contents() as a safety net in your theme:

function mytheme_get_svg( $path ) {
global $wp_filesystem;

// Prefer WP_Filesystem
if ( $wp_filesystem && method_exists( $wp_filesystem, 'get_contents' ) ) {
$content = $wp_filesystem->get_contents( $path );
if ( $content ) {
return $content;
}
}

// Fallback to direct file read
if ( file_exists( $path ) ) {
return file_get_contents( $path );
}

return '';
}

Important

  • file_get_contents() may be disabled via disable_functions on restricted hosts, but VPS and Docker environments typically support it
  • The root fix is to identify and handle the polluting plugin; the code fallback is a temporary measure
  • This issue is deceptive—the SVG files are intact and accessible via URL, but return empty only when read through WP_Filesystem in PHP

Scenario 5: Email Sending Fails from Command Line, Works from Browser

Problem

Calling wp_mail() via wp eval on the command line always fails. However, emails triggered by web requests (user registration, contact forms) send normally. SMTP plugins like WP Mail SMTP are correctly configured.

Root Cause

SMTP plugins intercept wp_mail() via hooks to switch the sending channel from PHP sendmail to an SMTP service. But these hooks depend on WordPress's full bootstrap sequence—particularly stages after wp_loaded.

WP-CLI's wp eval loads the WordPress core, but some plugin hooks don't register in the CLI environment. wp_mail() falls back to PHP sendmail, and most servers don't have sendmail configured, causing the failure.

Solution

Method 1 — Web request test: Add a temporary test route in your theme, trigger via browser:

// Add to functions.php temporarily, remove after testing
add_action( 'wp_loaded', function() {
if ( ! isset( $_GET['test_mail'] ) ) return;
if ( '1' !== $_GET['test_mail'] ) return;

$result = wp_mail( 'your@email.com', 'SMTP Test', 'Test email body' );
var_dump( $result ); // true = sent successfully
exit;
} );

Visit https://yoursite.com/?test_mail=1 to trigger the test.

Method 2 — eval-file with full bootstrap:

cat > /tmp/test-smtp.php << 'EOF'
<?php
require_once ABSPATH . 'wp-load.php';
do_action('wp_loaded');

$result = wp_mail('your@email.com', 'CLI SMTP Test', 'Test body');
echo $result ? "Sent\n" : "Failed\n";
EOF

docker exec wp_cli wp eval-file /tmp/test-smtp.php --allow-root

Best Practice

In production, always test email sending through web requests. WP-CLI is great for cron jobs and batch operations, but not for verifying features that depend on the full WordPress hook chain (email, cache warming, etc.).


WooCommerce Blocks Showing core/missing After Upgrade? Block Theme Troubleshooting Guide

· 7 min read

Ran into these four WooCommerce-specific issues while building Block Themes for clients. Each relates to how FSE architecture interacts with WooCommerce's block system. Documenting the troubleshooting process to help others working on WooCommerce theme development.

TL;DR

Four common issues in WooCommerce Block Theme development: block rename causing core/missing (block names gained -block suffix after upgrade), product archive 404 after shop slug change (rewrite cache not flushed), template HTML mismatch with Gutenberg save (dynamic block validation failure), and Cart/Checkout templates not auto-assigning (manual assignment required). Each scenario includes copy-paste fixes.

Scenario 1: Blocks Show as core/missing After WooCommerce Upgrade

Problem

After upgrading WooCommerce, several blocks in the Site Editor display as core/missing (Block Recovery prompt). The frontend renders correctly, but these blocks can't be edited in the editor.

Root Cause

WooCommerce renamed multiple internal blocks in newer versions, adding a -block suffix:

Old nameNew name
cart-order-summary-subtotalcart-order-summary-subtotal-block
cart-order-summary-shippingcart-order-summary-shipping-block
cart-order-summary-taxescart-order-summary-taxes-block
cart-order-summary-totaltotals-block
proceed-to-checkoutproceed-to-checkout-block

If your templates or Patterns use old block comment names (e.g., <!-- wp:woocommerce/cart-order-summary-subtotal -->), Gutenberg can't find the corresponding block registration and renders it as core/missing.

Frontend rendering works because WooCommerce's PHP callbacks still support old names. But the editor depends on JavaScript block registration info—when it's not found, the block shows as missing.

Solution

Diagnose: Confirm registered block names in the browser console:

wp.blocks.getBlockTypes()
.map(b => b.name)
.filter(n => n.includes('cart-order-summary'))

Compare the output with the names used in your templates to find mismatches.

Fix: Batch replace old block names in template and Pattern files:

# Run in your theme directory
find . -name "*.html" -exec sed -i 's/woocommerce\/cart-order-summary-subtotal/woocommerce\/cart-order-summary-subtotal-block/g' {} +
find . -name "*.html" -exec sed -i 's/woocommerce\/cart-order-summary-shipping/woocommerce\/cart-order-summary-shipping-block/g' {} +
find . -name "*.html" -exec sed -i 's/woocommerce\/cart-order-summary-taxes/woocommerce\/cart-order-summary-taxes-block/g' {} +
find . -name "*.html" -exec sed -i 's/woocommerce\/cart-order-summary-total/woocommerce\/totals-block/g' {} +
find . -name "*.html" -exec sed -i 's/woocommerce\/proceed-to-checkout/woocommerce\/proceed-to-checkout-block/g' {} +

After replacing, clear the database template cache (see the previous article's "file changes not applying" scenario).

Important

  • Back up template files before upgrading WooCommerce—block renames typically appear in major version upgrades
  • Normal frontend rendering doesn't mean there's no issue—always check in the Site Editor
  • Subscribe to WooCommerce developer changelogs to learn about block name changes in advance

Scenario 2: Changed the Shop URL, Now the Product Page Is Gone

Problem

After changing the WooCommerce shop page slug (e.g., from /shop/ to /products/) or modifying the permalink structure, visiting the new shop URL returns 404 or displays as a plain page without the product listing.

Root Cause

WordPress's Rewrite API caches URL routing rules in the database on first access. After changing a page slug, the cached routing still maps to the old slug. The new URL has no matching route, so WordPress renders it as a regular page instead of a WooCommerce product archive.

Solution

After changing slugs or permalink structures, flush the rewrite cache:

# Local Docker environment
docker exec wp_cli wp rewrite flush --allow-root

# Server environment
ssh your-server "docker exec prod_cli wp rewrite flush --allow-root"

One command fixes it. You can also do this in the WordPress admin: Settings → Permalinks → Save Changes. Saving automatically flushes rewrite rules.

Best Practice

Run wp rewrite flush after any URL structure change—modifying slugs, changing permalinks, or adding custom post types. Consider adding it to your deployment script to avoid forgetting.

Scenario 3: Cart/Checkout Blocks Show Validation Errors

Problem

WooCommerce dynamic blocks like Cart and Checkout show Block validation failed in the Site Editor. The error message says Expected HTML and Actual HTML don't match.

Root Cause

Many WooCommerce blocks are server-side rendered (SSR) dynamic blocks—frontend content is generated by PHP in real time, not dependent on JavaScript's save() function. However, Gutenberg still requires the block markup in templates to exactly match the save() output.

If the HTML in your template doesn't match what Gutenberg expects (extra or missing classes, styles, or inner elements), validation errors are triggered.

Reference for WooCommerce block save output:

Blocksave output
product-price<div class="is-loading"></div>
price-filter<div class="wp-block-woocommerce-price-filter is-loading"><span aria-hidden="true" class="wc-block-product-categories__placeholder"></span></div>
woocommerce/cart<div class="wp-block-woocommerce-cart alignwide is-loading"></div>
woocommerce/checkout<div class="wp-block-woocommerce-checkout alignwide wc-block-checkout is-loading"></div>
cart-order-summary-*-block<div class="wp-block-woocommerce-{block-name}"></div>
proceed-to-checkout-block<div class="wp-block-woocommerce-proceed-to-checkout-block"></div>

Solution

Key rules:

  1. Blocks with html: false (save returns null, like core/query-pagination) can use self-closing tags
  2. Blocks where JS save returns placeholder HTML must not be self-closing—they need complete content
  3. core/query-pagination must not have a wrapper div; InnerBlocks go directly between open/close comments

Troubleshooting steps:

  1. Open the Site Editor and browser DevTools Console
  2. Find the Block validation failed log entry
  3. Compare Expected (Gutenberg-generated) vs. Actual (from your template)
  4. Fix the HTML using the reference table above

Safe to Ignore

These WooCommerce validation errors can be safely ignored:

ErrorExplanation
woocommerce/cart or woocommerce/checkout validation failureDynamic blocks rendered by PHP in real time; frontend displays correctly
woocommerce-blocktheme-css loading errorWooCommerce plugin CSS issue; wait for official fix

Scenario 4: Custom Cart/Checkout Template Not Showing Up

Problem

You created custom Cart or Checkout template files (e.g., templates/cart.html, templates/checkout.html), but the corresponding WooCommerce pages still use the default template. The custom template is visible in the Site Editor but hasn't been applied to the Cart/Checkout pages.

Root Cause

WooCommerce Cart and Checkout pages auto-match templates by name. But if matching fails (e.g., template slug doesn't exactly correspond, or page creation order issues), manual assignment is needed.

Pages that rely on shortcodes (like My Account) will never auto-match templates and always require manual assignment.

Solution

Manually assign templates to their corresponding pages:

# 1. Find the Cart page ID
docker exec wp_cli wp post list --post_type=page --fields=ID,post_title,post_name --allow-root | grep -i cart

# 2. Assign the template
docker exec wp_cli wp post meta update <page_id> _wp_page_template <template_slug> --allow-root

# Example: assign cart-block template to the Cart page
docker exec wp_cli wp post meta update 42 _wp_page_template cart-block --allow-root

Verify the assignment:

docker exec wp_cli wp post meta get <page_id> _wp_page_template --allow-root

Template Naming Tips

Ensure template file names correspond to WooCommerce page functions. Standard naming:

  • cart.html or cart-block.html → Cart page
  • checkout.html or checkout-block.html → Checkout page
  • single-product.html → Product detail page
  • archive-product.html → Product archive page (shop)

Consistent naming increases the auto-matching success rate.


Fix WooCommerce FSE Cart Block Blank Page and Product Image Collapse

· 4 min read

Encountered these two issues while developing a WooCommerce FSE Block Theme for a client: Cart Block renders a blank page when empty, and product cards collapse when no featured image is set. Here's the root cause and solution.

TL;DR

  1. Cart Block requires explicit filled-cart-block and empty-cart-block inner blocks -- without them, empty cart renders nothing.
  2. FSE's post-featured-image block renders an empty string when no thumbnail exists, causing card height collapse. Fix with post_thumbnail_html filter to inject WooCommerce placeholder.

Issue 1: Cart Block Blank Page on Empty Cart

Symptom

The cart page works fine with products, but after clearing the cart the entire content area becomes blank -- no message, no "Continue Shopping" button, users are stuck.

Root Cause

WooCommerce Cart Block (wp:woocommerce/cart) requires developers to explicitly declare two inner blocks:

  • wp:woocommerce/filled-cart-block -- shown when cart has items
  • wp:woocommerce/empty-cart-block -- shown when cart is empty

If you only add the Cart Block without these inner blocks, WooCommerce doesn't know what to render for an empty cart and outputs nothing.

This issue doesn't occur in classic themes because their PHP templates (cart.php) have built-in empty cart handling. But FSE HTML templates are declarative -- you must declare all states.

Solution

Correct structure in cart.html:

<!-- wp:woocommerce/cart {"className":"cclee-cart"} -->
<div class="wp-block-woocommerce-cart alignwide is-loading">

<!-- wp:woocommerce/filled-cart-block -->
<div class="wp-block-woocommerce-filled-cart-block">
<!-- Full layout for items: product list + totals -->
<!-- wp:columns -->
<div class="wp-block-columns">
<!-- wp:column {"width":"65%"} -->
<div class="wp-block-column" style="flex-basis:65%">
<!-- wp:woocommerce/cart-items-block -->
<div class="wp-block-woocommerce-cart-items-block"></div>
<!-- /wp:woocommerce/cart-items-block -->
</div>
<!-- /wp:column -->
<!-- wp:column {"width":"35%"} -->
<div class="wp-block-column" style="flex-basis:35%">
<!-- wp:woocommerce/cart-totals-block -->
<div class="wp-block-woocommerce-cart-totals-block">
<!-- wp:woocommerce/cart-order-summary-block /-->
<!-- wp:woocommerce/proceed-to-checkout-block /-->
</div>
<!-- /wp:woocommerce/cart-totals-block -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
</div>
<!-- /wp:woocommerce/filled-cart-block -->

<!-- wp:woocommerce/empty-cart-block -->
<div class="wp-block-woocommerce-empty-cart-block">
<!-- Empty cart message + continue shopping button -->
<!-- wp:paragraph {"align":"center","textColor":"neutral-500"} -->
<p class="has-text-align-center has-neutral-500-color has-text-color">Your cart is currently empty.</p>
<!-- /wp:paragraph -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons">
<!-- wp:button {"backgroundColor":"accent","textColor":"base"} -->
<div class="wp-block-button"><a href="/shop/" class="wp-block-button__link has-base-color has-accent-background-color has-text-color has-background wp-element-button">Browse Products</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
<!-- /wp:woocommerce/empty-cart-block -->

</div>
<!-- /wp:woocommerce/cart -->

Key point: filled-cart-block and empty-cart-block must be direct children of wp:woocommerce/cart. WooCommerce uses them for conditional rendering based on cart state.

Symptom

In product listing pages (archive-product), products without featured images only show the text area -- no image placeholder. Mixed with products that have images, the layout heights are inconsistent and visually broken.

Root Cause

FSE's wp:post-featured-image block renders an empty string when a post/product has no thumbnail. Classic themes handle this in PHP templates with has_post_thumbnail() checks, but FSE HTML templates cannot embed conditional logic.

Solution

Add a filter in your theme's functions.php or WooCommerce integration file:

/**
* Product placeholder image when no featured image is set.
*
* FSE post-featured-image block renders empty when no thumbnail,
* causing card height collapse. This filter injects the WooCommerce
* placeholder image for product post type.
*
* @param string $html The post thumbnail HTML.
* @param int $post_id The post ID.
* @return string
*/
add_filter( 'post_thumbnail_html', function ( $html, $post_id ) {
// Already has image or not a product -- skip.
if ( $html || get_post_type( $post_id ) !== 'product' ) {
return $html;
}

// Use WooCommerce's built-in placeholder.
$src = function_exists( 'wc_placeholder_img_src' )
? wc_placeholder_img_src()
: '';

if ( ! $src ) {
return '';
}

return sprintf(
'<img src="%s" alt="%s" loading="lazy" decoding="async" style="width:100%%;height:100%%;object-fit:cover;">',
esc_url( $src ),
esc_attr( get_the_title( $post_id ) )
);
}, 10, 2 );

Notes:

  • wc_placeholder_img_src() depends on WooCommerce being active -- use function_exists() as a guard
  • object-fit: cover ensures the placeholder matches the cropping behavior of regular featured images
  • Only applies to product post type, does not affect blog posts or other post types

Interested in similar solutions? Get in touch

WordPress FSE Block Validation Failed: The Hidden Cause of Missing JSON Quotes

· 4 min read

TL;DR

In WordPress FSE themes, if a JSON attribute in a Pattern/Template HTML comment has a missing closing quote ", the brace count remains balanced, but parse_blocks() silently sets the block's attrs to null. Gutenberg's save function then produces no inline styles, triggering Block validation failed. Validate JSON with json.loads() to catch this.

The Problem

Opening the wishlist template in WordPress Site Editor shows a console error:

Block validation: Block validation failed for `core/group`

Content generated by `save` function:
<div class="wp-block-group has-border-color has-neutral-200-border-color"></div>

Content retrieved from post body:
<div class="wp-block-group has-border-color has-neutral-200-border-color"
style="border-style:solid;border-width:1px;border-radius:var(--wp--custom--border--radius--lg);
padding-top:var(--wp--preset--spacing--40);...">

The save function outputs correct CSS classes but completely loses inline styles, and the content is empty — even though the file clearly contains style attributes and child blocks.

Root Cause

The issue is in the JSON attributes of a core/group block HTML comment:

<!-- Broken -->
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var(--wp--preset--spacing--40)",
"right":"var(--wp--preset--spacing--40)",
"bottom":"var(--wp--preset--spacing--40)",
"left":"var(--wp--preset--spacing--40)"}}, <!-- missing closing quote -->
"border":{"radius":"var(--wp--custom--border--radius--lg)","width":"1px","style":"solid"}},
"borderColor":"neutral-200","layout":{"type":"constrained"}} -->

The "left" value "var(--wp--preset--spacing--40)" is missing its closing quote ". It's written as "var(--wp--preset--spacing--40).

Why brace counting misses this:

Correct: { "left": "value" }  → quotes paired, braces balanced
Broken: { "left": "value } → quotes unpaired, but braces still balanced

When the closing quote is missing, the } characters are treated as string content by the JSON parser (the quote never closed), so the brace count stays balanced.

parse_blocks() doesn't throw an error — it silently sets attrs to null:

// What parse_blocks returns
[
'blockName' => 'core/group',
'attrs' => null, // entire attribute object discarded
'innerHTML' => '<div ...>', // raw HTML still present
]

Gutenberg calls save() with null attrs, produces no inline styles, and the mismatch triggers Block validation failed.

Why this is hard to spot:

  • No white screen — the page still renders (falls back to innerHTML)
  • Braces are balanced, so visual inspection easily misses it
  • Site Editor shows a subtle "block needs recovery" notice
  • Audit scripts typically check brace balance and attribute correspondence, not JSON validity

Solution

1. Locate the Problem

Validate JSON with Python:

python3 -c "
import json
with open('templates/wishlist.html') as f:
content = f.read()
marker = 'wp:group {'
start = content.index(marker) + len(marker) - 1
end = content.index(' -->', start)
json_str = content[start:end]
try:
json.loads(json_str)
print('JSON OK')
except json.JSONDecodeError as e:
print(f'Error at position {e.pos}: {e.msg}')
print(f'Context: ...{json_str[max(0,e.pos-20):e.pos+20]}...')
"

Output pinpoints the exact error:

Error at position 196: Expecting ',' delimiter
Context: ...g--40)}},"border":{"...

2. Fix the JSON

Add the missing closing quote after the "left" value:

<!-- Fixed -->
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var(--wp--preset--spacing--40)",
"right":"var(--wp--preset--spacing--40)",
"bottom":"var(--wp--preset--spacing--40)",
"left":"var(--wp--preset--spacing--40)"}}, <!-- closing quote added -->
"border":{"radius":"var(--wp--custom--border--radius--lg)","width":"1px","style":"solid"}},
"borderColor":"neutral-200","layout":{"type":"constrained"}} -->

3. Verify the Fix

# Verify parse_blocks correctly parses the block
docker exec wp_cli wp eval '
$blocks = parse_blocks(file_get_contents(get_stylesheet_directory() . "/templates/wishlist.html"));
echo $blocks[...]["attrs"]["style"]["border"]["radius"];
' --allow-root

4. Prevention

Add a JSON comment validity check to CI:

import json, re, sys

def check_block_json(filepath):
with open(filepath) as f:
content = f.read()
for m in re.finditer(r'<!-- wp:\w+ (\{.*?\}) -->', content):
try:
json.loads(m.group(1))
except json.JSONDecodeError as e:
print(f"{filepath}: JSON error at comment position {m.start()}: {e}")
sys.exit(1)

check_block_json(sys.argv[1])

Interested in similar solutions? Get in touch