跳到主要内容

WordPress FSE Block Validation Failed: JSON 引号缺失的隐蔽根因

· 阅读需 4 分钟

TL;DR

WordPress FSE 主题的 Pattern/Template 文件中,HTML 注释里的 JSON 属性如果某个字符串值缺少闭合引号 ",花括号数量仍然平衡,但 parse_blocks() 会静默将该块的 attrs 置为 null。Gutenberg 的 save 函数因此不输出 inline style,触发 Block validation failed。用 json.loads() 验证 JSON 合法性即可定位。

问题现象

WordPress Site Editor 打开 wishlist 模板时控制台报错:

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);...">

save 函数输出了正确的 CSS class,但完全丢失了 inline style,且内容为空。而文件中 HTML 明明包含 style 属性和子块。

根因

问题出在模板文件中 core/group 块的 HTML 注释 JSON 属性:

<!-- 原始(有误) -->
<!-- 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)"}}, <!-- 此处缺少闭合引号 -->
"border":{"radius":"var(--wp--custom--border--radius--lg)","width":"1px","style":"solid"}},
"borderColor":"neutral-200","layout":{"type":"constrained"}} -->

注意 "left" 的值 "var(--wp--preset--spacing--40)" 缺少闭合引号 ",实际写成了 "var(--wp--preset--spacing--40)

为什么花括号检查发现不了:

正确:{ "left": "value" }  → 引号成对,花括号平衡
错误:{ "left": "value } → 引号不成对,但花括号仍然平衡

} 在 JSON 解析器眼中是字符串内容的一部分(因为引号没闭合),所以花括号计数不变。parse_blocks() 解析失败后不会报错,而是将该块的 attrs 直接置为 null

// parse_blocks 返回结果
[
'blockName' => 'core/group',
'attrs' => null, // 整个属性对象被丢弃
'innerHTML' => '<div ...>', // 原始 HTML 仍在
]

Gutenberg 拿到 null attrs 调用 save() 函数,自然不会输出任何 inline style,与文件中的 HTML 不匹配,触发 Block validation failed。

这个问题的隐蔽性在于:

  • 不会白屏,页面仍能渲染(使用 innerHTML 作为 fallback)
  • 花括号数量平衡,肉眼审查容易遗漏
  • Site Editor 中表现为"块需要恢复"的提示,容易被忽略
  • 审计脚本通常只检查花括号平衡和属性对应,不验证 JSON 合法性

解决方案

1. 定位问题

用 Python 验证 JSON 合法性:

python3 -c "
import json
with open('templates/wishlist.html') as f:
content = f.read()
# 提取 JSON 注释
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]}...')
"

输出会精确定位错误:

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

2. 修复 JSON

"left" 的值后面补上缺失的闭合引号:

<!-- 修复后 -->
<!-- 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)"}}, <!-- 闭合引号已补上 -->
"border":{"radius":"var(--wp--custom--border--radius--lg)","width":"1px","style":"solid"}},
"borderColor":"neutral-200","layout":{"type":"constrained"}} -->

3. 验证修复

# 用 WP-CLI 验证 parse_blocks 正确解析
docker exec wp_cli wp eval '
$blocks = parse_blocks(file_get_contents(get_stylesheet_directory() . "/templates/wishlist.html"));
// 导航到目标块,检查 attrs 非 null
echo $blocks[...]["attrs"]["style"]["border"]["radius"];
' --allow-root

4. 预防措施

在 CI 中加入 JSON 注释合法性检查:

import json, re, sys

def check_block_json(filepath):
with open(filepath) as f:
content = f.read()
# 匹配所有 <!-- wp:xxx {...} --> 注释中的 JSON
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])

对类似需求感兴趣?联系合作