本文约 3,680 字,预计阅读 10 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:改在插件目录里,一次升级全部归零
做外贸独立站的同学,大概都干过这样一件事:商品详情页想加一段”发货时效说明”,直接打开 wp-content/plugins/woocommerce/templates/single-product.php 改了两行,上线效果不错。结果某天 WooCommerce 自动更新,改动被覆盖,后台”WooCommerce → 状态 → 模板”还挂出一排红色提示,说模板版本过时。更尴尬的是,很多人从此不敢点更新,站点一直带着已知漏洞在线上跑。
正确做法是把插件模板”搬”到主题里再改。WordPress 自身有页面模板机制,WooCommerce 又额外提供了一套模板覆写机制,两者叠加时,新手极易搞混查找路径——文件放对了目录却不生效,八成是目录名或文件头不对。本文把这两套机制拆开讲,并给出只改一小段时的钩子替代方案。
原理分析:两套模板查找逻辑各走各的
WordPress 的模板层级是一条”从具体到通用”的回退链:单页优先找 page-{slug}.php、page-{id}.php,再找带 Template Name: 头注释的自定义页面模板,最后回退到 page.php、singular.php、index.php。查找时先扫子主题目录(get_stylesheet_directory()),没找到再扫父主题(get_template_directory())。
WooCommerce 是另一套逻辑:它默认在自己的 templates/ 目录取文件,但会通过 wc_get_template() / wc_get_template_part() 里的 woocommerce_locate_template 过滤器,让”主题目录/woocommerce/”下的同名文件优先。注意这里是主题目录,子主题优先于父主题。
| 你想改什么 | 文件应该放在 | 生效前提 |
|---|---|---|
| 给某个页面套单独版式 | 子主题根目录 template-xxx.php |
文件头有 Template Name: |
| 商品详情页 | 子主题/woocommerce/single-product.php |
目录名必须是 woocommerce |
| 商品列表里的单个卡片 | 子主题/woocommerce/content-product.php |
同上,且被 wc_get_template_part 调用 |
| 商品分类归档页 | 子主题/woocommerce/archive-product.php |
同上 |
| 面包屑、分页等小片段 | 子主题/woocommerce/global/、loop/ 对应子目录 |
相对路径需与插件内保持一致 |
保姆级实操
- 建目录:在
wp-content/themes/你的子主题/下新建woocommerce/,拼写与大小写必须完全一致,Linux 服务器区分大小写。 - 复制而非新建:从
wp-content/plugins/woocommerce/templates/复制对应文件过来,保留原文件顶部的if ( ! defined( 'ABSPATH' ) ) exit;和@version x.x.x注释,只改你要动的那几行。版本注释丢了,后台会一直误报”模板过时”。 - 页面模板单独处理:在子主题根目录新建 PHP 文件,头部写模板名与适用的文章类型,保存后到页面编辑器右侧”模板”下拉里选择即可。
- 能挂钩子就别复制:如果只是追加一段内容,优先用
woocommerce_single_product_summary这类 action 挂载,维护成本远低于复制整份模板(见下方代码)。 - 核对覆盖状态:改完去”WooCommerce → 状态 → 模板”,确认目标文件被标记为”已覆写”,且没有”需要更新”的版本提示。
- 清缓存再看效果:开了页面缓存的站点,务必先清一次缓存再验证,否则你会以为改动没生效。
<?php
/**
* Template Name: 落地页-宽屏
* Template Post Type: page
*
* @package lumen-mimangfei-child
*/
get_header();
?>
<main id="primary" class="site-main landing-wide">
<?php
while ( have_posts() ) :
the_post();
the_content();
endwhile;
?>
</main>
<?php
get_footer();
<?php
// 子主题 functions.php:只追加一段发货说明,不复制整个模板
add_action( 'woocommerce_single_product_summary', 'mimangfei_ship_note', 25 );
function mimangfei_ship_note() {
if ( ! is_product() ) {
return;
}
echo '<p class="ship-note">下单后 48 小时内发货,支持 7 天无理由退换。</p>';
}
// 需要精确指定模板位置时,用过滤器接管查找结果
add_filter( 'woocommerce_locate_template', 'mimangfei_locate_template', 10, 3 );
function mimangfei_locate_template( $template, $template_name, $template_path ) {
if ( 'single-product.php' === $template_name ) {
$custom = get_stylesheet_directory() . '/woocommerce/single-product.php';
if ( file_exists( $custom ) ) {
return $custom;
}
}
return $template;
}
# 正确的覆写目录结构(子主题)
wp-content/themes/lumen-mimangfei-child/
├── style.css
├── functions.php
├── template-landing-wide.php
└── woocommerce/
├── archive-product.php
├── single-product.php
└── content-product.php
# 确认当前启用的主题与父子关系
wp theme list
常见踩坑
- 目录名写成
WooCommerce、woocommerce-templates或templates,覆写静默失效,排查半天找不到原因。 - 复制模板时删掉了
@version注释,后台持续提示版本过时,误以为覆盖没成功。 - 改完没清页面缓存,前台依旧输出旧模板。
- 父主题和子主题各放一份同名文件,实际只有子主题那份生效,在父主题里改等于白改。
- 案例:某站把
content-product.php里的价格输出删掉换成自己拼的 HTML,一次 WooCommerce 升级后促销价逻辑变化,活动页全部显示原价,挂了两天才发现。正确做法是保留woocommerce_template_loop_price的调用,用 CSS 或woocommerce_get_price_html过滤器改呈现。 - 只改了
single-product.php,但商品是可变商品,实际渲染走的是表单与变量分支,改动看不到。 - 一边改插件目录、一边改子主题,两份文件并存时自己都分不清哪个在生效。
总结
判断标准很简单:改动范围小于一个函数,用 action/filter;需要重排一整块结构,才复制模板到 子主题/woocommerce/。落地清单:① 确认子主题已启用;② 复制时保留 ABSPATH 检查与 @version 注释;③ 改完到 WooCommerce 状态页核对;④ 每次插件升级后复查一次模板页;⑤ 上线前清一次缓存。子主题的创建方式可参考 WordPress 子主题制作,模板查找顺序的细节可延伸阅读 WordPress 模板层级,涉及商品页结构调整的也可结合 WooCommerce 模板覆写 一起看。
