搜索全站
主题 17 分钟阅读

wp_head 与 wp_footer 为什么不能删:插件资源注入点

删掉 wp_head 或 wp_footer 会让 SEO 标签、统计代码、弹窗脚本集体失效。讲清两个钩子的注入机制、必须调用的场景、wp_body_open 的补充作用与自检方法。

U
UFO_PF 2020-08-19 · 阅读 17 分钟
wp 主题
阅读提示
本文约 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( '&copy; %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 兜底代码没生效”的常见答案。

常见踩坑

  1. 把 wp_head() 放到了 <body> 里。 部分浏览器会把 <link> 自动移到 head,但脚本执行时机变得不可预测。
  2. wp_footer() 被放进了某个 <div> 内部之后又漏了 </body>。 用 Theme Check 插件扫一遍,它会明确提示缺钩子。
  3. 用 remove_action( 'wp_head', 'x' ) 但优先级写错。 核心 add_action( 'wp_head', 'print_emoji_detection_script', 7 ) 是优先级 7,写 remove_action( 'wp_head', 'print_emoji_detection_script' ) 默认按 10 去找,摘不掉。
  4. 在 wp_head 里输出但没有转义。 直接 echo get_the_title() 存在 XSS 风险,应走 esc_html(),详见输出转义规范 esc_html esc_attr esc_url。
  5. 删掉 wp_head() 只为了”少几行垃圾代码”。 所有插件的样式与脚本、SEO 标签、canonical、oEmbed 全部失效,且没有任何报错提示。
  6. 案例: 某站为了追求”纯净 HTML”,把 header.php 精简到只剩字符集与视口标签。上线后统计代码零数据、Yoast 的标题设置不生效、缓存插件无法插入合并后的 CSS、安全插件的登录保护脚本也没加载。排查时反复检查插件设置,最后在 Theme Check 的报告里看到 REQUIRED: Could not find wp_head。补回一行代码,所有问题同时消失。
  7. 缓存插件把 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,你自己的内联代码要挂在更晚的优先级上。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。