跳到主要内容

14 篇博文 含有标签「WordPress」

查看所有标签

修改 WordPress Block Theme 不生效?FSE 开发 5 大难题排查指南

· 阅读需 8 分钟

在为客户开发 WordPress Block Theme 时反复遇到这五个问题,每次排查都花了不少时间。整理成指南,帮助同样在做 FSE 开发的同学快速定位。

TL;DR

五个问题按频率排序:文件修改不生效(数据库缓存覆盖文件)、块嵌套错乱(注释未关闭)、子主题内容不渲染(缺少 post-content 块)、SVG 图标消失(WP_Filesystem 被插件污染)、WP-CLI 邮件失败(SMTP 插件在命令行不生效)。每个场景都给出可直接复用的排查命令。

场景一:改了主题文件,页面没变化

问题现象

修改了主题目录下的 theme.jsontemplates/*.htmlparts/*.html,刷新页面无变化。甚至 git pull 更新了代码,前端仍然显示旧样式。

根因

FSE 主题的模板和全局样式会被 Site Editor 保存到数据库——wp_templatewp_template_partwp_global_styles 三种自定义文章类型。WordPress 读取时数据库版本优先于文件版本。即使你改了文件,只要数据库里有对应记录,就会用数据库的。

解决方案

不同文件类型对应不同的清理方式:

修改内容清理方式
templates/*.htmlwp_template
parts/*.htmlwp_template_part
theme.jsonwp_global_styles + 缓存
patterns/*.php直接生效,无需清理

一键清理所有数据库模板缓存:

# 本地 Docker 环境
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

如果 theme.json 修改后仍不生效,先验证 JSON 格式是否有语法错误(如尾随逗号,JSON 规范不允许):

docker exec wp_cli wp eval 'echo json_encode(json_decode(file_get_contents(get_template_directory() . "/theme.json")));' --allow-root
# 返回空字符串 → JSON 有语法错误

注意事项

  • 开发期禁止在 Site Editor 中保存,避免产生数据库覆盖
  • 生产环境清理前确认 Site Editor 中没有自定义修改需要保留
  • patterns/*.php 不受此问题影响——Pattern 注册走 PHP 代码,不经过数据库
  • Site Editor 保存的 wp_global_styles 如果 JSON 损坏,会导致全站 WP_Theme_JSON_Resolver 解析错误,表现为所有页面样式崩溃

场景二:Site Editor 显示"尝试恢复",页面布局全乱了

问题现象

Site Editor 中 Pattern 显示"尝试恢复"(Attempt Recovery),保存后页面布局完全错乱。某些块被错误地嵌套在其他块内部,层级关系与源码不一致。

根因

WordPress 块编辑器使用 HTML 注释标记块的边界:

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

当容器块(如 wp:groupwp:columns)缺少关闭注释 <!-- /wp:group --> 时,Gutenberg 的 parse_blocks() 会将后续所有块视为该容器的子块。后果:

  1. 父块的 save 输出为空
  2. 触发 Block validation failed
  3. 后续所有块的嵌套关系全部错位

解决方案

排查:在浏览器控制台检查块树层级:

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

检查返回的块树结构,确认每个容器块的 innerBlocks 是否符合预期。如果一个 group 块内部包含了不应该在里面的块,大概率是前面某个容器缺少关闭注释。

修复:打开 Pattern 源文件,逐个检查每个 <!-- wp:xxx --> 都有对应的 <!-- /wp:xxx -->。建议在编辑器中搜索 <!-- wp:<!-- /wp:,计数确认数量一致。

预防技巧

使用支持括号匹配的编辑器(如 VS Code),配合 Block Comment 高亮插件,可以在编写时立即发现未关闭的注释。对于复杂的 Pattern,建议先写骨架结构(所有开闭注释配对),再填充内容。

场景三:子主题覆盖模板后,编辑器内容消失了

问题现象

创建子主题覆盖父主题模板后,在 WordPress 页面编辑器中输入的内容(文本、图片等)在前端完全空白。但模板文件中硬编码的 Pattern(如 hero 区域、CTA 区块)正常显示。

根因

FSE 模板通过 <!-- wp:post-content /--> 块来渲染页面编辑器中的 post_content。如果子主题覆盖的模板文件中没有这个块,WordPress 不知道在哪里输出页面内容。

结果就是:模板的固定结构(header、hero、sidebar)正常显示,但编辑器里写的内容全部丢失。

解决方案

确保子主题模板包含 post-content 块:

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

<!-- 模板固定结构(hero、sidebar 等) -->

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

<!-- 更多固定结构(CTA、footer 引用等) -->

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

排查"改了没效果"时,按以下顺序确认:

  1. 模板文件是否包含 <!-- wp:post-content /-->
  2. 你修改的是模板文件还是页面内容——两者控制不同的内容区域
  3. 模板中内联的 cover block 由模板文件控制,与数据库 post_content 无关

场景四:SVG 图标突然消失,但文件还在

问题现象

主题中使用 WP_Filesystem 读取 SVG 图标文件,突然所有 SVG 图标消失。直接访问 SVG 文件 URL 返回正常内容,但页面上图标位置是空的。

根因

WordPress 的 $wp_filesystem 全局变量默认使用 WP_Filesystem_Direct(直接读写本地文件)。某些插件(备份、安全类)在初始化时会将 $wp_filesystem 替换为 WP_Filesystem_ftpsocketsWP_Filesystem_SSH2

FTP/SSH 适配器通过远程连接读取文件,对本地路径(如 /var/www/html/wp-content/themes/...)无法正确访问,返回空字符串。由于替换发生在全局作用域,所有使用 WP_Filesystem 的主题和插件代码都受影响。

解决方案

第一步——诊断:检查当前 $wp_filesystem 的实际类型:

# 本地 Docker 环境
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root

# 返回 WP_Filesystem_Direct → 正常
# 返回 WP_Filesystem_ftpsockets 或其他 → 已被污染

第二步——定位污染源:逐个禁用插件,检查哪个插件替换了适配器:

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
# 重复直到返回 WP_Filesystem_Direct

第三步——代码兜底:在主题中加 file_get_contents() 作为 fallback:

function mytheme_get_svg( $path ) {
global $wp_filesystem;

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

// WP_Filesystem 失败时回退到直接读取
if ( file_exists( $path ) ) {
return file_get_contents( $path );
}

return '';
}

注意事项

  • file_get_contents() 在某些受限主机可能被 disable_functions 禁用,但 VPS 和 Docker 环境通常可用
  • 根治方案是定位并处理污染源插件,代码兜底只是临时方案
  • 此问题具有隐蔽性——SVG 文件本身完好,直接访问正常,只有在 PHP 中通过 WP_Filesystem 读取时才返回空

场景五:命令行发邮件失败,网页端正常

问题现象

通过 wp eval 在命令行调用 wp_mail() 发送邮件,始终失败。但通过 Web 请求触发的邮件(用户注册、联系表单)发送正常。WP Mail SMTP 等插件已正确配置 SMTP。

根因

SMTP 插件通过 Hook 拦截 wp_mail(),将发信通道从 PHP sendmail 切换到 SMTP 服务。但这些插件的 Hook 注册依赖 WordPress 完整启动流程——特别是 wp_loaded 之后的阶段。

WP-CLI 的 wp eval 虽然加载了 WordPress 核心,但部分插件 Hook 在 CLI 环境下不会被注册。wp_mail() 回退到 PHP sendmail,而大多数服务器没有配置 sendmail,导致发送失败。

解决方案

方法一——Web 请求测试:在主题中临时添加测试路由,通过浏览器触发:

// 临时添加到 functions.php,测试完立即删除
add_action( 'wp_loaded', function() {
if ( ! isset( $_GET['test_mail'] ) ) return;
if ( '1' !== $_GET['test_mail'] ) return;

$result = wp_mail( '[email protected]', 'SMTP Test', 'Test email body' );
var_dump( $result ); // true = 发送成功
exit;
} );

访问 https://yoursite.com/?test_mail=1 触发测试。

方法二——eval-file 确保完整加载

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

$result = wp_mail('[email protected]', '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

最佳实践

生产环境中,邮件发送验证应通过 Web 请求测试。WP-CLI 适合定时任务和批量操作,但不适合验证依赖完整 WordPress Hook 链的功能(如邮件、缓存预热等)。


WooCommerce 升级后编辑器显示异常、页面 404?Block Theme 排查指南

· 阅读需 7 分钟

在为客户构建 WooCommerce Block Theme 时遇到了这四个与 WooCommerce 相关的问题,每个都与 FSE 架构和 WooCommerce 的块系统有关。记录排查过程,帮助同样在做 WooCommerce 主题开发的同学避坑。

TL;DR

四个 WooCommerce Block Theme 开发中的常见问题:块重命名导致 core/missing(升级后块名加了 -block 后缀)、修改 shop slug 后产品归档 404(rewrite 缓存未刷新)、模板 HTML 与 Gutenberg save 不匹配(动态块验证失败)、Cart/Checkout 模板未自动分配(需要手动指定)。每个场景给出可直接复用的修复方案。

场景一:WooCommerce 升级后编辑器出现 core/missing

问题现象

升级 WooCommerce 后,Site Editor 中多个块显示为 core/missing(Block Recovery 提示)。前端渲染看起来正常,但编辑器中无法编辑这些块。

根因

WooCommerce 在新版本中将多个内部块重命名,统一添加了 -block 后缀。例如:

旧名称新名称
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

如果你的模板或 Pattern 中使用了旧名称的块注释(如 <!-- wp:woocommerce/cart-order-summary-subtotal -->),Gutenberg 找不到对应的块注册,将其渲染为 core/missing

前端渲染正常是因为 WooCommerce 的 PHP 回调函数仍然兼容旧名称,但编辑器依赖块的 JavaScript 注册信息,找不到就显示为 missing。

解决方案

诊断:在浏览器控制台确认已注册的块名称:

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

对比输出与模板中使用的名称,找出不一致的。

修复:批量替换模板和 Pattern 文件中的旧块名为新名称:

# 在主题目录下执行
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' {} +

替换后清理数据库中的模板缓存(见上一篇"修改不生效"场景),确保新名称生效。

注意事项

  • 升级 WooCommerce 前备份模板文件,块重命名通常出现在大版本升级中
  • 前端渲染正常不代表没有问题,一定要在 Site Editor 中检查
  • 订阅 WooCommerce 开发者 changelog,提前了解块名称变更

场景二:改了商店网址后,产品列表页打不开了

问题现象

修改了 WooCommerce 商店页面的 slug(如从 /shop/ 改为 /products/),或修改了固定链接结构后,访问新的商店 URL 返回 404 或显示为普通页面(没有产品列表)。

根因

WordPress 的 Rewrite API 会在首次访问时将 URL 路由规则缓存到数据库中。修改页面 slug 后,缓存中的路由映射仍然指向旧 slug。新的 URL 没有对应的路由规则,WordPress 将其渲染为普通页面而非 WooCommerce 产品归档。

解决方案

修改 slug 或固定链接结构后,必须刷新 rewrite 缓存:

# 本地 Docker 环境
docker exec wp_cli wp rewrite flush --allow-root

# 服务器环境
ssh yougu-wp "docker exec prod_cli wp rewrite flush --allow-root"

一条命令即可解决。也可以在 WordPress 后台手动操作:设置 → 固定链接 → 保存更改,保存时会自动刷新 rewrite 规则。

最佳实践

任何涉及 URL 结构变更的操作(修改 slug、变更固定链接、新增自定义文章类型)之后,都应该执行 wp rewrite flush。建议在部署脚本中加入此命令,避免遗忘。

场景三:Cart/Checkout 块报验证错误

问题现象

WooCommerce 的 Cart、Checkout 等动态块在 Site Editor 中显示 Block validation failed。错误信息提示 Expected HTML 和 Actual HTML 不一致。

根因

WooCommerce 的许多块是服务端渲染(SSR)的动态块——前端内容由 PHP 实时生成,不依赖 JavaScript 的 save() 函数。但 Gutenberg 仍然要求模板 HTML 中的块标记与 save() 输出完全匹配。

如果模板中写的 HTML 与 Gutenberg 期望的输出不一致(多了或少了 class、style、内部元素),就会触发验证错误。

WooCommerce 常见块的 save 输出对照:

save 输出
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>

解决方案

关键规则

  1. html: false 的块(save 返回 null,如 core/query-pagination)可以使用自闭合标签
  2. JS save 返回 placeholder HTML 的块禁止自闭合,必须包含完整内容
  3. core/query-pagination 禁止写 wrapper div,InnerBlocks 直接放在 open/close 注释之间

排查流程

  1. 打开 Site Editor,打开浏览器 DevTools Console
  2. 找到 Block validation failed 日志
  3. 对比 Expected(Gutenberg 生成)和 Actual(你的模板中写的)
  4. 按上方表格修复 HTML

可忽略的验证错误

以下 WooCommerce 验证错误可以安全忽略:

错误说明
woocommerce/cartwoocommerce/checkout 验证失败动态块,PHP 实时渲染,前端显示正常
woocommerce-blocktheme-css 加载错误WooCommerce 插件 CSS 问题,等待官方修复

场景四:自定义购物车/结算模板没生效

问题现象

创建了自定义的 Cart 或 Checkout 模板文件(如 templates/cart.htmltemplates/checkout.html),但对应的 WooCommerce 页面仍然使用默认模板。在 Site Editor 中能看到自定义模板,但它没有自动应用到 Cart/Checkout 页面。

根因

WooCommerce 的 Cart 和 Checkout 页面会自动匹配对应名称的模板。但如果匹配失败(如模板 slug 不完全对应,或页面创建顺序问题),就需要手动分配。

My Account 等依赖 shortcode 的页面不会自动匹配模板,必须手动分配。

解决方案

手动将模板分配给对应页面:

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

# 2. 分配模板
docker exec wp_cli wp post meta update <page_id> _wp_page_template <template_slug> --allow-root

# 示例:将 cart-block 模板分配给 Cart 页面
docker exec wp_cli wp post meta update 42 _wp_page_template cart-block --allow-root

验证分配结果:

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

模板命名建议

确保模板文件名与 WooCommerce 页面功能对应。标准命名:

  • cart.htmlcart-block.html → Cart 页面
  • checkout.htmlcheckout-block.html → Checkout 页面
  • single-product.html → 产品详情页
  • archive-product.html → 产品归档页(shop)

保持命名一致可以提高自动匹配的成功率。


Docker 容器读不到宿主机文件?匿名 Volume 覆盖 Bind Mount

· 阅读需 3 分钟

在为客户部署 WordPress 站点时遇到此问题,记录根因与解法。

TL;DR

镜像 Dockerfile 中的 VOLUME 声明会创建匿名卷,挂载优先级高于 docker-compose.yml 的 bind mount。解决方案:停止容器 → 删除匿名卷 → 重启。

问题现象

CI 部署后,新增的静态文件或 PHP 代码在容器内不存在:

  • assets/images/logo.png 宿主机存在,容器内无 → Logo 不显示
  • inc/setup.php 新增 filter 代码,容器内是旧版本 → Filter 不生效
  • git pull 后文件已更新,容器内仍是旧内容

根因

WordPress 官方镜像的 Dockerfile 包含:

VOLUME /var/www/html

即使你的 docker-compose.yml 配置了 bind mount:

volumes:
- ./wordpress/wp-content:/var/www/html/wp-content

Docker 仍会为 VOLUME 声明的路径创建匿名卷,匿名卷的挂载优先级高于 bind mount,导致:

  1. /var/www/html 被匿名卷接管
  2. 你的 bind mount 只挂载到 /var/www/html/wp-content
  3. 但匿名卷已经"占领"了父目录,bind mount 实际上被覆盖

docker inspect 验证:

docker inspect prod_wordpress --format '{{range .Mounts}}{{.Type}}: {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'

输出类似:

volume: /var/lib/docker/volumes/wp-prod_wp_html/_data -> /var/www/html# 匿名卷!
bind:/var/www/wp-prod/wordpress/wp-content -> /var/www/html/wp-content

解决方案

# 1. 停止容器
cd /var/www/wp-prod && docker compose down

# 2. 删除匿名卷
docker volume rm wp-prod_wp_html

# 3. 重启
docker compose up -d

验证

docker inspect prod_wordpress --format '{{range .Mounts}}{{.Type}}: {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'

应只显示 bind mount,无匿名 volume:

bind: /var/www/wp-prod/wordpress/wp-content -> /var/www/html/wp-content

预防

使用 bind mount 部署时,检查镜像是否声明了 VOLUME。如果声明了:

  1. 首次启动前,确认没有残留的匿名卷
  2. 或者修改 docker-compose.yml,让 bind mount 路径与 VOLUME 路径一致(挂载到同一层级)

修复后,git pull 更新代码时容器自动读取新文件,无需 docker cp 或重启容器。

注意事项

  • 删除匿名卷前确保数据已备份或可通过git pull 恢复
  • 生产环境操作前先在测试环境验证
  • 如果容器内有关键数据,先用 docker cp 备份

解决 WooCommerce 与 FSE Block Theme 的 4 个 CSS 冲突

· 阅读需 6 分钟

在为客户开发 WordPress FSE Block Theme 并集成 WooCommerce 时,遇到 4 个 WooCommerce 静默篡改主题样式的陷阱。每个都很隐蔽,排查耗时。记录根因与解法,帮助同样开发 FSE + WooCommerce 主题的开发者避坑。

TL;DR

  1. 字号预设覆盖:WooCommerce 注册 small/medium/large/x-large 四个预设覆盖 theme.json,用 h-1~h-6 自定义 slug 规避
  2. font-size class 连字符has-h-1-font-size(48px) vs has-h1-font-size(20px),手写 HTML 必须带连字符
  3. contrast 颜色反转--wp--preset--color--contrast 被 WooCommerce 改为浅色(#f8fafc),白色文字完全不可见
  4. ul.products 伪元素:WooCommerce 注入 ::before/::after 破坏 CSS Grid 布局,需 display:none 清除

陷阱一:字号预设被覆盖

问题现象

在 theme.json 中定义了字号预设:

{
"settings": {
"typography": {
"fontSizes": [
{ "slug": "small", "size": "0.875rem" },
{ "slug": "large", "size": "1.125rem" }
]
}
}
}

安装 WooCommerce 后,使用 has-large-font-size 的段落实际渲染为 36px,而非预期的 18px。H1 标题也可能从 48px 缩水到 20px。

根因

WooCommerce 通过 WP_Theme_JSON_API 注册自己的字号预设:

slugtheme.json 原值WooCommerce 覆盖值
small0.875rem (14px)13px
medium1rem (16px)20px
large1.125rem (18px)36px
x-large1.25rem (20px)42px

同 slug 的预设会被 WooCommerce 的值覆盖。但自定义 slug(如 h-1~h-6base)不受影响。

解法

theme.json 使用自定义 slug,避免与 WooCommerce 冲突:

{
"fontSizes": [
{ "slug": "h-1", "size": "3rem" },
{ "slug": "h-2", "size": "2.25rem" },
{ "slug": "h-3", "size": "1.75rem" },
{ "slug": "base", "size": "16px" }
]
}

模板中用 has-h-5-font-size 代替 has-large-font-size

如果需要用到 WooCommerce 的字号值,在 woocommerce.css 顶部定义自定义变量:

:root {
--cclee-font-sm: 13px;
--cclee-font-md: 20px;
--cclee-font-lg: 36px;
--cclee-font-xl: 42px;
}

陷阱二:font-size class 的连字符陷阱

问题现象

手写 FSE 模板 HTML 时,H1 标题只显示 20px 而非 theme.json 定义的 48px:

<!-- 错误:渲染为 20px -->
<h1 class="has-h1-font-size">标题</h1>

<!-- 正确:渲染为 48px -->
<h1 class="has-h-1-font-size">标题</h1>

两个 class 只差一个连字符,但字号差 2.4 倍。

根因

Gutenberg 将 fontSize: "h1" slug 渲染为 has-h-1-font-size(slug 中的数字前加连字符)。这是正确行为。

但当 WooCommerce 覆盖了字号系统后,has-h1-font-size(无连字符)匹配到的是 WooCommerce 的覆盖值(20px),而 has-h-1-font-size(有连字符)匹配到 theme.json 原始值(48px)。

验证方法 -- 浏览器控制台:

// 创建临时元素测试
const el = document.createElement('div');
document.body.appendChild(el);

el.className = 'has-h1-font-size';
console.log('无连字符:', getComputedStyle(el).fontSize); // 20px(错误)

el.className = 'has-h-1-font-size';
console.log('有连字符:', getComputedStyle(el).fontSize); // 48px(正确)

document.body.removeChild(el);

解法

手写 FSE 模板 HTML 时,font-size class 必须带连字符:

<!-- wp:heading {"fontSize":"h1"} -->
<h1 class="has-h-1-font-size">标题</h1>
<!-- /wp:heading -->

<!-- wp:heading {"fontSize":"h3"} -->
<h2 class="has-h-3-font-size">区块标题</h2>
<!-- /wp:heading -->

规则:has-h-{N}-font-size(带连字符),适用于所有 h-* 预设。

陷阱三:contrast 颜色反转

问题现象

contrast 背景色上使用 base(白色)文字,发现文字完全不可见:

<div class="has-contrast-background-color">
<h2 class="has-base-color">白色标题在深色背景上</h2>
</div>

预期 contrast 是深色背景 + 白色文字,实际 contrast 是浅色背景。

根因

WooCommerce 将 --wp--preset--color--contrast 注册为 #f8fafc(极浅灰),不是深色。这是 WooCommerce 的设计意图(其默认主题用 contrast 做浅色背景区域),但与大多数 FSE 主题的 contrast 语义(深色)冲突。

解法

contrast 背景上用 primary(深色)作为文字颜色:

<div class="has-contrast-background-color">
<h2 class="has-primary-color has-text-color">深色标题在浅色背景上</h2>
</div>

或者在 theme.json 中定义自己的深色 preset(如 surface),不依赖 contrast

{
"settings": {
"color": {
"palette": [
{ "slug": "surface", "color": "#0f172a", "name": "Surface" }
]
}
}
}

陷阱四:ul.products 伪元素破坏 Grid 布局

问题现象

WooCommerce 商品列表使用 CSS Grid 布局时,卡片排列错位、多出空白格:

ul.products {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}

预期 3 列等宽,实际出现空位,部分卡片换到下一行。

根因

WooCommerce 在 ul.products 上注入了 ::before::after 伪元素:

ul.products::before,
ul.products::after {
display: table;
content: '';
}

ul.products::after {
clear: both;
}

这些伪元素是为传统 float 布局设计的 clearfix。但 CSS Grid 把伪元素当作 grid items,占用了两个隐式格子位。

解法

对 Grid 布局的 ul.products 隐藏伪元素:

ul.products {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}

ul.products::before,
ul.products::after {
display: none;
}

注意事项

  • 每个陷阱都与 WooCommerce 的 WP_Theme_JSON_API 注册或 CSS 注入有关,禁用 WooCommerce 后这些覆盖会消失
  • 陷阱一和二是联动的:WooCommerce 覆盖字号预设后,连字符和无连字符的 class 映射到不同值
  • 陷阱三的 contrast 值取决于 WooCommerce 版本,升级后可能变化,建议用自定义 slug(如 surface)代替
  • 手写 FSE 模板前,先用浏览器 DevTools 检查 Gutenberg 自动渲染的 class 名称,不要想当然地缩写

正在部署 WooCommerce 站点?

推荐以下云服务器,稳定可靠


修复 FSE Block Theme 的风格预览单色块与首页白屏问题

· 阅读需 4 分钟

在为客户开发 WordPress FSE Block Theme 时,遇到风格预览显示异常和首页编辑白屏两个问题。记录根因与解法。

TL;DR

  1. Style variation 的 palette/gradients整体替换而非合并,只写 1 个颜色会丢失其余全部 -- 必须补全完整列表,只改需要变化的颜色。
  2. front-page.html 模板硬编码 pattern 会导致 Site Editor 编辑白屏,且用户无法在编辑器中调整布局 -- 应改为页面内容驱动。

坑一:Style Variation 风格预览只显示一个颜色块

问题现象

Site Editor > Browse Styles 中,默认风格显示 16 个颜色块,而第二个风格(Amber)只显示 1 个颜色块。切换到 Amber 风格后,所有引用 primarybasecontrast 等 CSS 变量的元素全部失去颜色。

根因

WordPress theme.json 的 style variation 机制中,settings.color.palettesettings.color.gradients 是**整体替换(replace)**父主题的对应声明,而非增量合并(merge)。

原始 styles/amber.json

{
"settings": {
"color": {
"palette": [
{ "slug": "accent", "color": "#b45309", "name": "Amber" }
]
}
}
}

这段配置的本意是"只把 accent 颜色换成琥珀色",但实际效果是"整个调色板只剩这一个颜色"。父主题 theme.json 中定义的 primarysecondarybasecontrastsurfaceneutral-50 ~ neutral-900 全部丢失。

gradients 同理,只声明 1 个就会替换掉父主题的 7 个渐变。

解决方案

在 variation 文件中补全完整的 palette 和 gradients 列表,只修改需要变化的值:

{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Amber",
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#0f172a", "name": "Primary" },
{ "slug": "secondary", "color": "#334155", "name": "Secondary" },
{ "slug": "accent", "color": "#b45309", "name": "Amber" },
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#f8fafc", "name": "Contrast" },
{ "slug": "surface", "color": "#0f172a", "name": "Surface" },
{ "slug": "neutral-50", "color": "#fafafa", "name": "Neutral 50" }
// ... 其余 neutral 色保持不变
],
"gradients": [
// 补全所有 7 个渐变,只修改 accent gradient 的目标色
]
}
}
}

关键原则:variation 中声明的任何数组类型配置项(palette、gradients、fontSizes、spacingSizes 等)都是整体替换,必须包含完整内容。

坑二:Site Editor 编辑首页白屏

问题现象

在 Site Editor 中通过 Pages > Home 打开首页编辑,画布完全空白。但在 Templates > Front Page 中能看到完整布局。

根因

这是两个不同的编辑入口,对应不同的数据源:

入口编辑对象数据来源
Templates > Front Pagefront-page.html 模板主题文件
Pages > Homepage_on_front 页面内容数据库 wp_posts.post_content

原始 front-page.html 在模板层面硬编码了三个 pattern:

<!-- wp:pattern {"slug":"cclee-theme/hero-centered"} /-->
<!-- wp:pattern {"slug":"cclee-theme/features-grid"} /-->

<!-- wp:group {"tagName":"main","align":"full"} -->
<main class="wp-block-group alignfull">
<!-- wp:post-content /-->
</main>
<!-- /wp:group -->

<!-- wp:pattern {"slug":"cclee-theme/cta-banner"} /-->

post-content 块渲染 page_on_front 页面的数据库内容。该页面内容为空,所以 Pages > Home 画布空白 -- 这是 WordPress 预期行为。

问题在于:模板硬编码 pattern 导致用户无法在页面编辑器中调整首页布局顺序和内容。

解决方案

将首页布局从"模板硬编码"改为"页面内容驱动"。

1. 模板只保留骨架front-page.html):

<!-- wp:template-part {"slug":"header-transparent","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","align":"full","layout":{"type":"constrained"}} -->
<main class="wp-block-group alignfull">
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer-columns","tagName":"footer"} /-->

2. 激活时自动注入默认内容functions.phpinc/setup.php):

add_action( 'after_switch_theme', function () {
$front_page_id = (int) get_option( 'page_on_front' );
if ( ! $front_page_id ) {
return;
}

$post = get_post( $front_page_id );
// 仅在内容为空时注入,不覆盖用户已有内容
if ( ! $post || ! empty( trim( $post->post_content ) ) ) {
return;
}

$default_content = '<!-- wp:pattern {"slug":"cclee-theme/hero-centered"} /-->' . "\n"
. '<!-- wp:pattern {"slug":"cclee-theme/features-grid"} /-->' . "\n"
. '<!-- wp:pattern {"slug":"cclee-theme/cta-banner"} /-->';

wp_update_post( [
'ID' => $front_page_id,
'post_content' => $default_content,
] );
} );

设计决策

  • 模板只负责 header / main(post-content)/ footer 骨架
  • 首页布局完全由页面内容驱动,用户可在页面编辑器中增删、排序 pattern
  • after_switch_theme 钩子保证主题首次激活时有默认内容
  • 内容为空判断确保不覆盖用户的自定义内容

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

修复通用 hover 选择器穿透嵌套 Group 导致的卡片悬浮错乱

· 阅读需 3 分钟

在为客户开发 WordPress FSE Block Theme 时发现:博客列表页的卡片悬浮时文字区域发生偏移,但卡片外框不动,视觉上非常违和。记录根因与解法。

TL;DR

通用卡片 hover 选择器 .wp-block-columns .wp-block-column > .wp-block-group:hover 会匹配到卡片内部嵌套的文字 group,导致悬浮时内层文字偏移而外层卡片不动。修复方式:用 .wp-block-post-template 前缀重置内层 group 的 hover 效果。

问题现象

博客列表页使用卡片式布局(外层 border group 包裹图片 + 文字 group)。鼠标悬浮卡片时:

  • 内部的标题和摘要文字产生 translateY(-4px) 位移
  • 外层卡片的 border 和 shadow 没有任何变化
  • 视觉效果像文字"飘出"了卡片

根因

FSE 中卡片 pattern 的 HTML 结构是嵌套的:

<!-- 外层卡片 group (有 border) -->
<div class="wp-block-group has-border-color ...">
<img ... /> <!-- 特色图片 -->

<!-- 内层文字 group -->
<div class="wp-block-group" style="padding:...">
<h2>文章标题</h2>
<p>摘要文字...</p>
</div>
</div>

主题中定义了一个通用 hover 特效:

.wp-block-columns .wp-block-column > .wp-block-group:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
}

这个选择器的本意是让整张卡片悬浮上移。但在博客列表页中,post-template 块内部的卡片也被 wp-block-columns 包裹,导致选择器同时命中了内层文字 group(它也是 .wp-block-column > .wp-block-group)。

由于内层 group 没有 border 和 shadow,只表现为文字位移,而外层卡片没有位移效果。

解决方案

分两步处理:重置内层 + 给外层单独加 hover

1. 重置内层 group 的 hover

.wp-block-post-template 前缀提高优先级,把内层 group 的所有 hover 效果清零:

/* Reset hover for inner groups inside post template cards */
.wp-block-post-template .wp-block-columns .wp-block-column > .wp-block-group {
transition: none;
}
.wp-block-post-template .wp-block-columns .wp-block-column > .wp-block-group:hover {
transform: none;
box-shadow: none;
}

2. 给外层卡片加独立的 hover

外层卡片有 .has-border-color 类,用它做精确匹配:

/* Blog card (outer bordered group) -- hover lift + shadow */
.wp-block-post-template .wp-block-group.has-border-color {
transition:
transform 0.3s ease-out,
box-shadow 0.3s ease-out;
}
.wp-block-post-template .wp-block-group.has-border-color:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
}

为什么不直接缩小通用选择器范围?

通用选择器被多个 pattern 共享(features-grid、pricing、testimonial 等),这些 pattern 的 group 只有一层,不存在嵌套穿透问题。缩小通用选择器反而会破坏其他 pattern 的 hover 效果。

重置内层比限制外层更可靠,因为:

  • 不影响其他使用通用 hover 的 pattern
  • 精确针对有嵌套问题的场景
  • CSS 层面完全隔离,不需要修改任何 HTML/PHP

经验总结

在 FSE Block Theme 中添加 hover 特效前,务必:

  1. 检查 pattern 的实际 HTML 嵌套结构
  2. 确认选择器只命中目标元素(外层容器),不会穿透到内部 group
  3. 如果多个 pattern 共享同一选择器,优先用"重置内层"而非"限制外层"

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

修复 CSS ::before 伪元素装饰纹理覆盖按钮的问题

· 阅读需 3 分钟

TL;DR

::before 伪元素实现容器背景装饰纹理(圆点/网格)时,必须同时设置 opacitypointer-events: nonez-index: -1。缺少 opacity 导致纹理 100% 不透明,缺少 z-index 导致纹理覆盖按钮等子元素。

问题现象

FSE 主题的 CTA 横幅区域使用 ::before 伪元素渲染装饰性圆点纹理。预期效果是微妙的背景点缀,实际效果是圆点以完全不透明状态覆盖在按钮表面:

/* 问题代码 — 纹理完全不透明且覆盖子元素 */
.has-dots-pattern::before {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
/* 缺少 opacity — 纹理 100% 不透明 */
/* 缺少 z-index — 纹理覆盖子内容 */
}

按钮表面出现密集的白色圆点,CTA 区域的渐变背景上也布满了明显的网格线。

根因

::before 伪元素实现装饰纹理时有三个关键属性,缺一不可:

1. opacity — 控制纹理透明度

radial-gradient 生成的是实心圆点,currentColor 继承文本颜色。在深色背景上,白色实心圆点会非常醒目。缺少 opacity 时默认值为 1,纹理完全不透明。

2. z-index: -1 — 将纹理推到子元素后方

::before 设置了 position: absolute,在默认层叠上下文中,定位元素的绘制顺序晚于正常流元素。当容器是 position: relative 时,z-index: -1 将伪元素推到容器背景之后、子内容之前,确保按钮等子元素显示在纹理之上。

3. pointer-events: none — 防止拦截点击

这是最容易被记住的一个,因为它直接影响交互。没有它,纹理层会拦截点击事件。

同一个项目中存在正确实现的对照代码,说明是复制时遗漏了属性:

/* 正确实现 — 独立元素方式 */
.cclee-dots-pattern {
position: absolute;
inset: 0;
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 24px 24px;
opacity: 0.08; /* 有 */
pointer-events: none;
/* 独立元素由 HTML 层级控制层叠,无需 z-index */
}

解决方案

::before 伪元素补上 opacityz-index: -1

/* 修复后 */
.has-dots-pattern {
position: relative;
}
.has-dots-pattern::before {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.08; /* 添加:8% 不透明度 */
pointer-events: none;
z-index: -1; /* 添加:退到子内容后方 */
}

网格纹理同理:

.has-grid-pattern {
position: relative;
}
.has-grid-pattern::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(currentColor 1px, transparent 1px),
linear-gradient(90deg, currentColor 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.05; /* 网格更淡,5% 不透明度 */
pointer-events: none;
z-index: -1;
}

装饰纹理伪元素的完整检查清单

属性作用缺失后果
opacity: 0.05~0.1控制纹理透明度纹理完全不透明,喧宾夺主
z-index: -1层叠在子元素后方纹理覆盖按钮等子内容
pointer-events: none不拦截鼠标事件点击穿透失败

三个属性必须同时存在,这是 ::before 装饰纹理的固定模式。


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

修复 WooCommerce FSE Cart Block 空车白屏与商品无图塌陷

· 阅读需 4 分钟

在为客户开发 WooCommerce FSE Block Theme 时遇到这两个问题:Cart Block 空车时页面白屏、商品无特色图片时卡片高度塌陷。记录根因与解法。

TL;DR

  1. Cart Block 必须显式声明 filled-cart-blockempty-cart-block 子块,否则空车时无任何内容输出。
  2. 商品无特色图片时,FSE 的 post-featured-image 块渲染为空字符串,导致卡片高度塌陷。通过 post_thumbnail_html filter 补上 WooCommerce 占位图。

问题一:Cart Block 空车白屏

问题现象

购物车页面在有商品时正常显示,清空购物车后整个页面内容区变成空白 -- 没有提示文案,没有"继续购物"按钮,用户无法自助返回。

根因

WooCommerce Cart Block (wp:woocommerce/cart) 的设计要求开发者显式声明两个子块:

  • wp:woocommerce/filled-cart-block -- 有商品时显示
  • wp:woocommerce/empty-cart-block -- 空车时显示

如果只写了 Cart Block 本体而没有嵌套这两个子块,WooCommerce 在空车状态下不知道该渲染什么,输出为空。

这个问题在经典主题中不存在,因为经典主题使用 PHP 模板 cart.php,其中已经内置了空车处理逻辑。但 FSE 的 HTML 模板是声明式的,必须完整声明所有状态。

解决方案

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">
<!-- 有商品时的完整布局:商品列表 + 汇总 -->
<!-- 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">
<!-- 空车提示 + 继续购物按钮 -->
<!-- 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 -->

关键点filled-cart-blockempty-cart-block 必须是 wp:woocommerce/cart 的直接子块,WooCommerce 通过这两个子块实现有货/空车的条件渲染。

问题二:商品无特色图片时卡片塌陷

问题现象

在商品列表页(archive-product)中,没有设置特色图片的商品卡片只显示文字部分,没有图片区域。与有图卡片混排时,布局高度不统一,视觉上非常突兀。

根因

FSE 的 wp:post-featured-image 块在文章/商品没有缩略图时直接渲染为空字符串,不会输出占位图。经典主题中通常在 PHP 模板里手动处理这种情况(has_post_thumbnail() 判断),但 FSE HTML 模板无法嵌入这种条件逻辑。

解决方案

在主题的 functions.php 或 WooCommerce 集成文件中添加 filter:

/**
* 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 );

注意

  • wc_placeholder_img_src() 依赖 WooCommerce 插件激活,用 function_exists() 做防护
  • object-fit: cover 确保占位图和正常特色图片的裁剪行为一致
  • 只针对 product 类型生效,不影响博客文章等其他 post type

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

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])

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

修复 Gutenberg Gradient Class 命名变更导致的 Block 验证失败

· 阅读需 2 分钟

在为客户开发 WordPress FSE 主题时遇到此问题,记录根因与解法。

TL;DR

Gutenberg 升级后,gradient 的 CSS class 命名规则从 has-{slug}-gradient 变为 has-{slug}-gradient-background。手写 Pattern HTML 中的旧 class 与 Gutenberg 验证逻辑不匹配,导致 Site Editor 报错。解决方案是批量替换 class 名称。

问题现象

所有历史 Pattern 在 Site Editor 中显示错误:

Block contains unexpected or invalid content
  • 新建 Pattern 无此问题
  • 前台渲染正常
  • 点击 "Attempt Recovery" 可恢复显示

根因分析

通过 DevTools Console 查看 Block validation failed 日志,对比 Expected 与 Actual:

Expected(Gutenberg 生成)

<div class="wp-block-group has-accent-gradient-background has-background">

Actual(Pattern 中手写)

<div class="wp-block-group has-accent-gradient has-background">

Gutenberg 版本升级后,gradient class 命名规则变更:

旧命名新命名
has-{slug}-gradienthas-{slug}-gradient-background

Block 验证时,Gutenberg 会根据块注释中的 JSON 属性(如 "gradient":"accent-gradient")重新计算期望的 HTML,与实际 HTML 比对。class 不匹配即报验证失败。

解决方案

1. 确认影响范围

# 查找使用旧 class 的文件
grep -r "has-[a-z0-9-]*-gradient " patterns/ --include="*.php"

2. 批量替换

# macOS/Linux 通用
sed -i '' 's/has-\([a-z0-9-]*\)-gradient /has-\1-gradient-background /g' patterns/*.php

# Linux (GNU sed)
sed -i 's/has-\([a-z0-9-]*\)-gradient /has-\1-gradient-background /g' patterns/*.php

注意

  • 只替换 HTML 标签中的 class 属性
  • 块注释中的 "gradient":"accent-gradient" 声明不需要改动
  • 正则末尾有空格,避免误匹配 has-accent-gradient-background

3. 验证修复

  1. 清理 WordPress 缓存:wp cache flush
  2. 刷新 Site Editor,确认错误消失
  3. 抽查几个 Pattern,验证前后台显示正常

预防措施

  1. 优先使用块注释属性:通过 JSON 属性(如 "gradient":"accent-gradient")设置样式,让 Gutenberg 自动生成 class,避免手写
  2. 关注 Gutenberg 更新日志:升级前检查 Breaking Changes
  3. 开发环境先验证:升级后在开发环境测试所有 Pattern,确认无验证错误再部署

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