本文约 6,419 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:换了自制主题后,统计代码和 SEO 标签全没了
自己动手写了个精简主题,header.php 干干净净只留了一个 <link> 和 <meta charset>。上线一个月后看数据:Google Analytics 零数据、Search Console 提示”缺少 robots 与 canonical”、Yoast 设置的标题描述不生效、联系表单点提交没反应。
主题没错,错在删掉了两个看似多余的 <?php wp_head(); ?> 和 <?php wp_footer(); ?>。它们是整座插件生态的入口。
原理分析:两个钩子分别是”头部注入点”与”尾部注入点”
wp_head() 的实现就是一句 do_action( 'wp_head' ),它把所有挂在这个动作上的回调执行一遍:
// wp-includes/general-template.php(简化)
function wp_head() {
do_action( 'wp_head' );
}
function wp_footer() {
do_action( 'wp_footer' );
}
function wp_body_open() {
do_action( 'wp_body_open' ); // WP 5.2+
}
核心自己就往这两个钩子上挂了几十个回调,优先级大致如下:
| 优先级 | 回调 | 作用 | 钩子 |
|---|---|---|---|
| 1 | rsd_link、wlwmanifest_link |
远程发布接口声明 | wp_head |
| 5 | wp_generator |
输出 WordPress 版本号 | wp_head |
| 8 | wp_print_styles |
输出队列里所有 CSS | wp_head |
| 9 | wp_no_robots |
站点公开性设置 | wp_head |
| 10 | rel_canonical |
canonical 链接 | wp_head |
| 10 | wp_shortlink_wl |
shortlink | wp_head |
| 20 | wp_print_head_scripts |
输出 <head> 区脚本 |
wp_head |
| 10 | wp_print_footer_scripts |
输出页脚脚本 | wp_footer |
关键在优先级 8 和 20:你在 wp_enqueue_scripts 阶段加入队列的样式和脚本,最终是在 wp_head() 执行时才被真正打印成 <link> 和 <script>。没有这一行,队列里的东西永远出不来。
wp_footer() 同理,而且它是所有”页脚加载”脚本的唯一出口——wp_enqueue_script() 第五个参数传 true,脚本就等在这儿输出。
三个钩子的分工:
| 钩子 | 位置 | 典型用途 |
|---|---|---|
wp_head() |
</head> 之前 |
样式、head 脚本、meta 标签、OG 标签 |
wp_body_open() |
<body> 开头 |
GTM 的 <noscript> 兜底、跳转脚本、无障碍跳转链接 |
wp_footer() |
</body> 之前 |
页脚脚本、内联初始化代码、统计代码 |
保姆级实操
第 1 步:确认三个钩子都在,且位置正确
<?php /** header.php */ ?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="profile" href="https://gmpg.org/xfn/11">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="skip-link screen-reader-text" href="#primary"><?php esc_html_e( '跳到主要内容', 'mimangfei' ); ?></a>
<?php /** footer.php */ ?>
</main>
<footer id="colophon" class="site-footer">
<div class="site-info">
<span><?php printf( '© %s %s', esc_html( gmdate( 'Y' ) ), esc_html( get_bloginfo( 'name' ) ) ); ?></span>
</div>
</footer>
<?php wp_footer(); ?>
</body>
</html>
三条硬规则:wp_head() 必须在 </head> 之前;wp_body_open() 紧跟 <body>;wp_footer() 必须在 </body> 之前。放错位置会导致脚本在 DOM 未就绪时执行,或样式闪烁(FOUC)。
第 2 步:验证注入是否生效
# 抓首页源码,检查关键输出
curl -s https://example.com/ | grep -E "wp-emoji|<link rel='stylesheet'|canonical|generator"
更直接的办法是在主题里临时探测当前挂在钩子上的回调:
add_action( 'wp_footer', function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
global $wp_filter;
foreach ( array( 'wp_head', 'wp_body_open', 'wp_footer' ) as $hook ) {
$count = isset( $wp_filter[ $hook ] ) ? count( $wp_filter[ $hook ]->callbacks ) : 0;
printf( '<!-- %s 上挂了 %d 个回调 -->', esc_html( $hook ), (int) $count );
}
}, 999 );
第 3 步:清理不需要的默认输出
wp_head() 会带出一些你可能不想要的东西,用 remove_action 精准摘除,而不是删掉整个钩子:
add_action( 'init', function () {
remove_action( 'wp_head', 'wp_generator' ); // 隐藏版本号
remove_action( 'wp_head', 'rsd_link' ); // RSD 声明
remove_action( 'wp_head', 'wlwmanifest_link' ); // Windows Live Writer
remove_action( 'wp_head', 'wp_shortlink_wl', 10 );
remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10 );
remove_action( 'wp_head', 'rest_output_link_wp_head', 10 ); // REST 声明
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); // oEmbed 声明
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
} );
注意 remove_action 的第三个参数(优先级)必须与核心 add_action 时一致,否则摘不掉。rest_output_link_wp_head 摘掉后,部分依赖 REST 自动发现的应用会受影响,按需取舍。
第 4 步:往钩子里正确注入自己的代码
// 自定义 meta 与 JSON-LD,直接 echo,注意转义
add_action( 'wp_head', function () {
if ( ! is_singular( 'post' ) ) {
return;
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Article',
'headline' => get_the_title(),
'datePublished' => get_the_date( 'c' ),
);
echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
}, 15 );
// 需要 jQuery 的内联初始化代码,挂在页脚并确保脚本已注册
add_action( 'wp_footer', function () {
if ( ! wp_script_is( 'mmf-main', 'enqueued' ) ) {
return;
}
wp_add_inline_script( 'mmf-main', 'MMF.init();', 'after' );
}, 25 ); // 必须晚于 wp_print_footer_scripts(优先级 20)
第 5 步:wp_body_open 的典型用法
// Google Tag Manager 的 noscript 兜底必须紧跟 <body>
add_action( 'wp_body_open', function () {
$id = get_theme_mod( 'mmf_gtm_id' );
if ( ! $id ) {
return;
}
printf(
'<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=%s" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>',
esc_attr( $id )
);
} );
如果主题不支持 wp_body_open()(老主题),这段代码会完全不执行——这也是”为什么我的 GTM 兜底代码没生效”的常见答案。
常见踩坑
- 把
wp_head()放到了<body>里。 部分浏览器会把<link>自动移到 head,但脚本执行时机变得不可预测。 wp_footer()被放进了某个<div>内部之后又漏了</body>。 用 Theme Check 插件扫一遍,它会明确提示缺钩子。- 用
remove_action( 'wp_head', 'x' )但优先级写错。 核心add_action( 'wp_head', 'print_emoji_detection_script', 7 )是优先级 7,写remove_action( 'wp_head', 'print_emoji_detection_script' )默认按 10 去找,摘不掉。 - 在
wp_head里输出但没有转义。 直接echo get_the_title()存在 XSS 风险,应走esc_html(),详见输出转义规范 esc_html esc_attr esc_url。 - 删掉
wp_head()只为了”少几行垃圾代码”。 所有插件的样式与脚本、SEO 标签、canonical、oEmbed 全部失效,且没有任何报错提示。 - 案例: 某站为了追求”纯净 HTML”,把
header.php精简到只剩字符集与视口标签。上线后统计代码零数据、Yoast 的标题设置不生效、缓存插件无法插入合并后的 CSS、安全插件的登录保护脚本也没加载。排查时反复检查插件设置,最后在 Theme Check 的报告里看到REQUIRED: Could not find wp_head。补回一行代码,所有问题同时消失。 - 缓存插件把
wp_footer输出的内容缓存后过期不更新。 动态内容(如 nonce)不要挂在wp_footer输出,应走wp_add_inline_script()或 REST 接口取,wp_add_inline_script()的完整写法见wp_enqueue_script 正确加载 CSS 与 JS。
总结
wp_head()、wp_body_open()、wp_footer() 三个钩子是主题与插件生态之间唯一的契约,缺一个就是”插件集体失灵”。自研主题的自检清单第一条就是确认它们在位且位置正确;需要精简输出时用 remove_action 精准摘除(注意优先级),绝不动钩子本身。记住它们各自的输出时机——样式在 wp_head 优先级 8,脚本在优先级 20,页脚脚本在 wp_footer 优先级 20,你自己的内联代码要挂在更晚的优先级上。
