WordPress 首页没有 H1 标记怎么修:AeroCore 主题实测修复全过程

AI 摘要997笔记AI

AeroCore 首页缺少 h1 并非误报:home.php 只拼标题给 head、渲染布局组件,也不触发 wp_body_open,改模板还会被升级覆盖。最佳解法是用 mu-plugin 在输出层注入视觉隐藏但读屏可读的 h1,不改主题、不影响视觉,且已有 h1 时跳过。

此内容由AI根据文章内容自动生成。

站长工具提示「首页缺少 h1 标记」,很多站主第一反应是去主题里改模板。但如果你用的是 AeroCore 这类「组件化拼装」的主题,改模板既费劲又会在主题升级后被覆盖。这篇记录我在 997笔记上的完整排查过程和最终解法:不改主题,用一个 mu-plugin 在输出层补一条视觉隐藏的 h1。

一、先确认问题是真的

不要急着改。先把「首页到底有没有 h1」这件事确认清楚,别最后改了半天发现是工具误报。

最快的方法是直接抓线上 HTML 数一下:

curl -s https://你的域名.com/ | grep -o '<h1' | wc -l

我这边实测返回 0,确实是真缺失,不是工具误报。下面这行是排查用的,可以顺便记下来备用:

检查项 命令 实测结果
首页 h1 数量 curl -s 域名/ | grep -o '<h1' | wc -l 0(缺失)
文章页 h1 数量 同上,换成文章 URL 1(正常)
主题是否调 wp_body_open grep -rn 'wp_body_open' wp-content/themes/主题名 | wc -l 0(关键线索)
home.php 里有没有标题输出 grep -n 'the_title|<h1' home.php 0 处

注意:如果你页面开了 fastcgi_cache 或 CDN,curl 拿到的可能是缓存版本。给 URL 加个随机参数(如 ?_=$(date +%s))绕过缓存再测,否则容易把「缓存里没有」误判成「模板里没有」。

二、根因:AeroCore 的 home.php 结构

打开主题的首页模板 templates/default/home.php,全文核心逻辑就这么点:

<?php

namespace AeroCore;

$description = get_bloginfo('description');
$title = get_bloginfo('name');
if ($description !== '') {
    $title .= ' - ' . $description;
}

Template::renderHeader([
    'title' => $title
]);

$layout_config = get_option('aerocore_layout_config', '[]');
$layout_items = json_decode($layout_config, true);
// ... 循环 renderLayoutComponent($item) 输出轮播/文章列表/侧栏
Template::renderFooter();

看明白了:这个模板做三件事——拼一个标题字符串给 renderHeader(那是 <head> 里的 <title>)、读取后台配置的布局项、循环渲染布局组件。

它从头到尾没有输出任何 <h1>。标题字符串只进了 <head>,页面上一个标题标签都没有。这不是 bug,是这类主题的设计取向:首页在它眼里是「组件拼装页」,不是「文章页」,没有天然的标题元素。

还有个更隐蔽的坑,这类主题几乎都不调 wp_body_open()。我 grep 整个主题目录,结果是 0 次。这意味着坊间最常见的「挂 wp_body_open 钩子注入 h1」方案,对这个主题完全无效——钩子根本没人触发。这也是很多人试了网上教程还是没效果的原因。

三、为什么不该直接改 home.php

先说清楚我为什么不推荐直接编辑主题文件:

做法 当下 主题升级后
改主题 home.php 生效 被覆盖,h1 丢失,得重新改
建子主题覆盖 生效 不丢,但要维护整套模板
写 mu-plugin 生效 不丢,主题管不到它

mu-plugin 是 WordPress 官方机制:wp-content/mu-plugins/ 下的文件只会自动加载、无法在后台禁用、也不会被主题更新覆盖。对一个「补一个缺失标签」这种小改动,这是最合适的落点。

四、思路:不改 HTML 结构,在输出层补

核心思路是:不碰主题的任何文件,在页面即将输出时拦截 HTML,在 <body> 后插入一条 h1。

这里有个关键取舍——插入的 h1 是视觉隐藏的,页面上看不见。这不是偷懒,而是正确做法,原因有三:

  • 首页视觉不能变。首页是组件拼装的,硬塞一个可见的大标题会破坏布局;截图对比会看出明显差异。
  • 标题已通过 <title> 传达。访客和搜索引擎都已经从 <head> 的标题拿到站点名了,视觉上重复反而是噪音。
  • 读屏软件能读到。视觉隐藏 ≠ 对辅助技术隐藏。视障用户访问首页时会得到站点名,这正是 h1 的语义作用。

关键在于「视觉隐藏」的正确写法。这里有个很容易踩的坑:

千万别用 display:none 或 visibility:hidden。这两种方式元素会从无障碍树里彻底移除,读屏软件也读不到——那等于白加,h1 依然「不存在」。

必须用「视觉隐藏但保留在无障碍树」的方案,完整写法在下面的代码里。

五、完整代码

新建文件 wp-content/mu-plugins/n997-home-h1.php,内容如下:

<?php
/**
 * Plugin Name: 首页 h1 补全
 * Description: AeroCore 主题的 home.php 只输出布局组件,首页没有任何 h1,
 *              站长工具会报「缺少 h1 标记」。本插件用输出层改写,
 *              在首页 <body> 后注入一条屏幕阅读器专用的 h1。
 * Version: 1.1
 */

if (!defined('ABSPATH')) {
    exit;
}

final class N997_Home_H1
{
    /** 视觉隐藏但读屏软件可读的 h1 样式 */
    const CSS = '.n997-sr-h1{position:absolute;width:1px;height:1px;padding:0;margin:-1px;'
              . 'overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);'
              . 'white-space:nowrap;border:0;}';

    public static function init()
    {
        add_action('template_redirect', array(__CLASS__, 'startBuffer'), 0);
    }

    public static function startBuffer()
    {
        if (is_admin() || (defined('DOING_AJAX') && DOING_AJAX)
            || (defined('REST_REQUEST') && REST_REQUEST) || is_feed()) {
            return;
        }
        if (!is_front_page()) {
            return;
        }
        if (self::frontPageHasH1()) {
            return;
        }
        ob_start(array(__CLASS__, 'inject'));
    }

    /** 静态首页且正文非空时,主题会自己渲染 h1,不要重复加 */
    private static function frontPageHasH1()
    {
        if (is_page() && is_front_page()) {
            $p = get_queried_object();
            if ($p instanceof WP_Post && '' !== trim((string) $p->post_content)) {
                return true;
            }
        }
        return false;
    }

    public static function inject($html)
    {
        if ($html === '' || $html === null) {
            return $html;
        }
        // 双保险:输出层再查一次,已有 h1 就跳过
        if (preg_match('#<h1b#i', $html)) {
            return $html;
        }

        $name = trim((string) get_bloginfo('name'));
        if ($name === '') {
            return $html;
        }
        $desc = trim((string) get_bloginfo('description'));
        $text = ($desc !== '') ? $name . '|' . $desc : $name;
        $text = trim(wp_strip_all_tags(
            html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8')
        ));

        $h1 = '<h1 class="n997-sr-h1">' . esc_html($text) . "</h1>n";
        $style = '<style>' . self::CSS . "</style>n";

        // 样式必须在 h1 之前落,否则 h1 会占位显示 1px 外的元素
        if (stripos($html, '</head>') !== false) {
            $html = preg_replace('#</head>#i', $style . '</head>', $html, 1);
        } else {
            $html = $style . $html;
        }

        // 插到 <body...> 之后,只插第一个
        $n = 0;
        $html = preg_replace_callback('#(<bodyb[^>]*>)#i',
            function ($m) use ($h1, &$n) {
                if ($n > 0) {
                    return $m[1];
                }
                $n++;
                return $m[1] . "n" . $h1;
            }, $html, 1);

        return $html;
    }
}

N997_Home_H1::init();

上传后刷新首页,h1 数量就变成 1 了。

六、为什么用 template_redirect 而不是 wp_body_open

这点值得单独说,因为它是整个方案里最容易踩错的地方。

网上最常见的写法是:

add_action('wp_body_open', function () {
    echo '<h1>xxx</h1>';
});

这段代码在标准主题(如 Twenty 系列)上是有效的。但我 grep 过 AeroCore 的整个主题目录,wp_body_open 出现 0 次——钩子从没被触发过,代码挂上去等于没写。

排查这类问题的通用技巧:当某个钩子「怎么都不生效」时,先 grep -rn '钩子名' 主题目录 确认主题到底调没调它,而不是反复怀疑自己的代码。钩子不存在,代码写得再对也是空跑。

所以方案改成 template_redirect 钩子 + ob_start 输出缓冲回调。这个位置在模板加载前触发,一定能拦住。

几个细节值得说明:

  • 优先级写 0:确保尽可能早地启动缓冲,早于其他可能输出内容的逻辑。
  • 排除后台/AJAX/REST/Feed:这四种场景用 ob_start 会把 JSON、RSS 响应也缓冲掉,可能导致接口异常。
  • 只对首页生效:is_front_page() 判断,文章页、留言页一概不管。
  • 双重防重复:静态首页如果本身有正文(主题会渲染 h1),和方法里的兜底正则都能挡住,绝不会出现两个 h1。

七、验证:一定要做视觉回归

改完别急着收工,必须验证两件事:h1 有了,且页面没被改坏。

如果本机没装 PHP 环境,别在本地折腾语法检查,直接把文件传到服务器上跑:

# 语法检查
php -l wp-content/mu-plugins/n997-home-h1.php
页面 修复前 h1 修复后 h1 结论
首页 / 0 1 已修复
留言板 /message/ 1 1 未误伤
友情链接 /friend-links/ 1 1 未误伤
站点地图 /sitemap/ 1 1 未误伤
文章页 1 1 未误伤

「未误伤」这列才是重点。如果你的改动让文章页冒出 2 个 h1,那就是引入了新问题。

视觉验证要靠截图对比。命令行 curl 只能验证标签数量,验证不了「有没有多出一个奇怪的小黑块」。本地没装 Selenium 的话,用无头 Chrome 截图最省事:

chrome --headless=new --dump-dom "https://你的域名.com/" > page.html
grep -c '<h1' page.html
grep -o 'n997-sr-h1' page.html

我这边对比了修改前后的首页截图,像素级无任何可见变化,这才是合格的标准——SEO 标签的补充不应该让用户看到任何东西。

八、几个延伸问题

1. h1 内容该写什么?

我用的是「站点名|站点副标题」,比如「997笔记|大番茄 · 软路由 OpenWrt、Linux 命令、网络运维折腾笔记」。这样 h1 同时承载了品牌和页面主题,比单纯写站点名更利于理解页面内容。

注意:如果你给文章页配了自定义 SEO 标题,首页的 h1 建议别跟着改,保持「站点名 + 副标题」这个稳定的语义。

2. 会影响 SEO 吗?

正面影响。h1 是页面主题最核心的语义信号,缺失会导致搜索引擎对页面主题的判断依据变弱。补上之后,站长工具的「缺少 h1」告警消除,也补齐了无障碍访问的语义结构。

3. 主题以后更新了会怎样?

mu-plugin 不受影响。但有一种情况要注意:如果主题作者将来把 h1 补上了,你的插件会检测到并自动跳过(方法里的 preg_match('#<h1b#i', $html) 兜底),不会变成两个 h1。这就是那层双重防重复设计的意义。

4. 为什么不用子主题?

子主题也能解决,但代价是要复制一套模板文件。而这里的改动只是「加一个隐藏标签」,用 mu-plugin 几行代码搞定,维护成本低得多。改主题要改的量,用插件能小就小,就别用子主题。

九、一句话总结

组件化主题的首页没有 h1 是设计使然,不是 bug。修复的正确姿势是:先 grep 确认钩子是否存在 → 用 mu-plugin 承载改动 → template_redirect + ob_start 在输出层注入 → 视觉隐藏而非 display:none → 截图做视觉回归 → 验证其他页面没被误伤。

整套改动不到 90 行,不动主题任何文件,主题升级也不会失效。整站每个页面各留一个 h1,是最干净的 SEO 结构。

上一篇 DoH/DoT 实测:13 家公共加密 DNS 延迟对比,和一个几乎人人填错的地址
下一篇 搜索结果图标采集不到怎么办:favicon 完整排查与修复攻略