本文约 6,114 字,预计阅读 16 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:用户点过「×」,刷新一次又弹出一次
全站通知条(Notification Bar / Announcement Bar)看着是个小功能,真上线却经常翻车。常见投诉有两类:一是用户点了关闭,点进下一篇文章公告又回来了——因为他点的只是视觉隐藏,浏览器根本没记住状态;二是运营在后台改了文案,前台三天没变,一问才知道是页面缓存把旧公告缓存住了,而且缓存的粒度是全站共用的那一份。
这两类问题的根源不在插件选得对不对,而在于理解状态存在哪(localStorage / cookie / user meta)和内容什么时候被渲染(PHP 服务端输出 vs JS 异步拉取)。插件帮你做的事是把这两件事打包,但一旦和页面缓存组合,默认实现往往会失效。本文从机制讲起,给出三条可行路径和一套兼容全站缓存的方案。
原理分析:一块横幅背后的两个独立问题
| 问题 | 存放 | 典型实现 | 缓存兼容性 |
|---|---|---|---|
| 「已关闭」状态 | 浏览器端 cookie 或 localStorage |
wp_footer 注入 JS 读写 |
好(完全客户端) |
| 登录用户的关闭状态 | user_meta |
update_user_meta() |
差(会让页面变成用户私有) |
| 公告正文 | wp_options 或主题 mod |
服务端 echo |
差(被页面缓存固化) |
| 是否对当前页显示 | PHP 条件判断 | is_front_page() 等 |
中 |
关键结论有三条:
第一,关闭状态必须存在浏览器端。 用 localStorage 存一个版本号最稳妥:localStorage.setItem('announce_v', '2026-10-01')。每次运营改文案就在后台 bump 一次版本号,版本号变了则所有用户的关闭状态自动失效、公告重新展示。这解决了”改了文案但用户之前关闭过,永远看不到新活动”的死局。
第二,服务端渲染公告内容=给页面缓存挖坑。 只要公告 HTML 进入 <body>,Nginx FastCGI Cache、WP Rocket、Cloudflare APO 都会把它当作页面的一部分缓存。解决方案有两条:让公告 HTML 走 JS 异步拉取(牺牲一点首屏),或者干脆接受缓存规则——更新公告后自动执行一次清缓存。
第三,布局抖动(CLS)是隐形杀手。 很多主题把公告条用 JS 动态插到 <header> 之前,页面已经渲染完才出现,正文整体下移几十像素,Google 的 CLS 指标直接爆红。正确做法是预留占位高度,或者让公告条 position: fixed 不占文档流。
布局指标相关:WordPress 核心网页指标优化
保姆级实操
步骤 1:先选实现路径
三条路,按站点情况选:
- 纯静态 / 不需要频繁改:主题自带公告选项(Kadence、Astra Pro、OceanWP 都有),零插件,改起来最快。
- 需要定时上下线、需要做 A/B 测试:用专用通知条插件。免费方案里 WP Notification Bars、Holler Box(免费版可用) 是常见选项;Hello Bar 是 SaaS,需要外嵌外部脚本,国内加载偏慢,一般不建议。付费里 Holler Box Pro 支持关闭状态与转化统计。
- 追求零依赖 / 要和缓存共存:自己写,下面给完整代码。
步骤 2:注册后台配置项
把公告文案、链接、版本放进 Customizer,运营可以直接改而不用碰代码:
add_action( 'customize_register', function( $wp_customize ) {
$wp_customize->add_section( 'mf_announce', array(
'title' => '全站公告',
'priority' => 20,
) );
$wp_customize->add_setting( 'mf_announce_text', array(
'default' => '',
'sanitize_callback' => 'wp_kses_post',
'transport' => 'refresh',
) );
$wp_customize->add_setting( 'mf_announce_ver', array(
'default' => '1',
'sanitize_callback' => 'sanitize_text_field',
) );
$wp_customize->add_control( 'mf_announce_text', array(
'label' => '公告内容(支持 <a> 标签)',
'section' => 'mf_announce',
'type' => 'textarea',
) );
$wp_customize->add_control( 'mf_announce_ver', array(
'label' => '版本号(改文案时必须 +1,用于重置用户关闭状态)',
'section' => 'mf_announce',
'type' => 'text',
) );
} );
步骤 3:服务端输出(避免 CLS 的写法)
关键点是用一个固定高度的容器占位,内容由服务端输出,关闭由 JS 处理:
add_action( 'wp_body_open', function () {
$text = get_theme_mod( 'mf_announce_text', '' );
$ver = get_theme_mod( 'mf_announce_ver', '1' );
if ( ! $text ) {
return;
}
printf(
'<div id="mf-announce" class="mf-announce is-hidden" data-ver="%1$s" role="region" aria-label="站点公告">
<div class="mf-announce-inner">%2$s</div>
<button type="button" class="mf-announce-close" aria-label="关闭公告">×</button>
</div>',
esc_attr( $ver ),
wp_kses_post( $text )
);
} );
is-hidden 类默认隐藏,由 JS 判断是否有历史关闭记录再决定是否显示——这样即使全站缓存了 HTML,最终是否展示仍由浏览器端决定。
步骤 4:关闭逻辑 + 版本号(核心代码)
(function () {
var KEY = 'mf_announce_dismissed_ver';
var bar = document.getElementById('mf-announce');
if (!bar) { return; }
var ver = bar.getAttribute('data-ver');
var seen = localStorage.getItem(KEY);
if (seen === ver) {
// 已经关过当前版本,保持隐藏
bar.parentNode.removeChild(bar);
return;
}
bar.classList.remove('is-hidden');
bar.querySelector('.mf-announce-close').addEventListener('click', function () {
bar.classList.add('is-hidden');
try { localStorage.setItem(KEY, ver); } catch (e) { /* Safari 隐私模式会抛错 */ }
});
})();
注意那个 try/catch:Safari 无痕模式下 localStorage.setItem 会抛异常,不包一层会让整个 JS 崩掉。
步骤 5:CSS(不吃 CLS)
.mf-announce {
position: relative;
background: #1f2937;
color: #f9fafb;
font-size: 14px;
line-height: 1.6;
text-align: center;
}
.mf-announce.is-hidden { display: none; }
.mf-announce-inner { padding: 10px 56px 10px 16px; max-width: 1200px; margin: 0 auto; }
.mf-announce-inner a { color: #fbbf24; text-decoration: underline; }
.mf-announce-close {
position: absolute; top: 6px; right: 12px;
background: none; border: 0; color: #f9fafb;
font-size: 22px; line-height: 1; cursor: pointer;
}
@media (max-width: 600px) {
.mf-announce-inner { padding: 10px 44px 10px 12px; font-size: 13px; }
}
步骤 6:更新公告后同步清理缓存
服务端输出公告的场景,必须在保存配置时触发一次清缓存:
add_action( 'customize_save_after', function() {
// WP Rocket
if ( function_exists( 'rocket_clean_domain' ) ) { rocket_clean_domain(); }
// LiteSpeed Cache
if ( function_exists( 'do_action' ) ) { do_action( 'litespeed_purge_all' ); }
// Nginx FastCGI 或自建 redis:用 WP-CLI 在 deploy hook 里清,见下方 bash
} );
# Nginx FastCGI Cache 目录清理(路径按服务器实际配置调整)
rm -rf /var/run/nginx-cache/*
echo "FastCGI cache purged"
常见踩坑
- 关闭后换页又弹出来:典型原因是把状态存在了 PHP session 而不是浏览器,或者插件用的是
.hide()纯 CSS 隐藏,没写任何持久化。用上面的localStorage+ 版本号方案一次性解决。 - 公告被
wp_body_open之外的位置卡住:很多老主题没有实现wp_body_open()hook(WordPress 5.2 引入)。用wp_head输出会被<body>标签拦截解析错误,正确做法是先在header.php里补<?php wp_body_open(); ?>。 - 案例:某外贸客户站在 9 月 30 日上线了”国庆 8 折”的公告栏,设定 10 月 8 日下线。10 月 9 日客户投诉:优惠码还在被使用,因为公告虽然到期被移除了,CDN 边缘节点缓存的旧页面还在分发,且有一部分用户是在移动端 App 内置浏览器里看到的离线副本。复盘后改成:公告内容带明确到期时间戳,前端再次
Date.now()校验,过期则不渲染——服务端/缓存层的过期控制永远要有一层客户端兜底。 - CDN(Cloudflare)把整页 HTML 缓存:尤其开了 APO 的情况。必须在 Cloudflare 后台配 purge 规则,或者干脆让公告走异步接口。
user_meta存关闭状态导致缓存分裂:登录用户可以精确记忆,但只要日志缓存按 user key 分片,缓存命中率会暴跌,TTFB 反升。非必要不要用。- 无障碍与 SEO:公告条会被算进正文相关性。如果是长期挂在首页的通用文案(”本店支持七天无理由退货”),建议给容器加
role="region"且不要放进<h1>附近,避免稀释标题语义。 - CLS 超标:动态 JS 插入公告必须用固定高度占位或 fixed 定位,否则每次加载都会抖一下,Core Web Vitals 里 CLS 直接爆表。
总结
判断标准一句话:是否展示由浏览器决定,什么时候更新由版本号决定。 具体落地三条:①用「版本号 + localStorage」而不是单纯布尔标记管理关闭状态;②服务端输出的公告必须在配置变更钩子(如 customize_save_after)里触发一次全站清缓存;③前端再加一层时间戳兜底,防止任何一层缓存失效导致过期活动继续展示。插件适合快速上线和需要转化统计的场景,自己写方案适合缓存层完全可控、不希望额外依赖的站点。
缓存清理相关:WordPress 缓存插件配置与清理
