上一篇讲了 add_filter 是什么,这一篇拿我自己主题里真实在跑的一行代码开刀,把它彻底拆开。这行代码是那个文件里唯一把四个参数用满的一行,搞懂它,add_filter 就算过关了。
就是这行:
add_filter('wp_title', 'lingfeng_wp_title', 10, 2);
一句话概括它的意思:WordPress 每次要输出网页标题之前,先把它交给我的 lingfeng_wp_title 改一遍——并且我要接 2 个参数。
新手最容易在这里搞混。wp_title 这个钩子改的不是文章标题,而是浏览器标签页上显示的那行字,也就是 HTML 里的 <title> 标签内容。官方文档的原话是:过滤 the text appearing in the HTML <title> tag,也就是常说的”title tag”或”meta title”。
换句话说:您在后台给文章起的标题一动不动,变的只是搜索引擎和浏览器标签看到的那一行。
add_filter( 'wp_title', 'lingfeng_wp_title', 10, 2 );
// ↑钩子名 ↑我的函数名 ↑优先级 ↑我要接几个参数
| 位置 | 值 | 含义 |
|---|---|---|
| 第 1 个 | wp_title |
挂在”生成网页标题”这个点上 |
| 第 2 个 | lingfeng_wp_title |
到点了让这个函数去干 |
| 第 3 个 | 10 |
优先级,默认值。写在这里其实是为了给第 4 个参数占位置 |
| 第 4 个 | 2 |
关键:告诉 WordPress”我这个函数要接 2 个变量” |
这是整行代码最值得讲的地方。WordPress 在这个点上,其实会递出 3 个值给所有登记过的函数,源码就这一句:
$title = apply_filters( 'wp_title', $title, $sep, $seplocation );
// ↑标题文字 ↑分隔符 ↑分隔符位置
但您写的是 2,所以您的函数只能接到前两个,第三个 $seplocation(值是 'right')被放弃了。这不是写错,函数里没用它,写 2 反而更省事——但规矩必须清楚:
add_filter('wp_title', 'f', 10, 1); // 只能写 function f($title)
add_filter('wp_title', 'f', 10, 2); // 可以写 function f($title, $sep) ← 我这里
add_filter('wp_title', 'f', 10, 3); // 可以写 function f($title, $sep, $seplocation)
如果第四个参数不写(默认 1),你函数里却写了第二个变量,那个变量就是 undefined。 这是新手最常踩的坑,也正是这行代码里 2 存在的唯一理由——为了让下面的函数签名能理直气壮地写成 ($title, $sep)。
这行代码不是一个孤零零的句子,它只是”登记”这个动作。真正跑起来是这样一条链:
header.php 里写着 <title><?php wp_title('', true, 'right'); ?></title>;$title;apply_filters('wp_title', ...),翻出所有在这个钩子上登记过的函数,逐个调用;lingfeng_wp_title($title, $sep),接过来改一改,return 回去;header.php,把改完的结果 echo 进 <title> 标签。注意第 4 步和第 5 步之间:您从来不主动调用自己的函数,是 WordPress 替您调的。这就是”钩子”这个词的来源——挂上去,等它被拉动。
function lingfeng_wp_title($title, $sep) {
global $paged, $page; // ① 开门:拿外面的分页号
if ( is_feed() ) { // ② RSS 输出时原样还回去
return $title;
}
$title = $title; // ③ 自己赋值给自己(废代码)
$site_description = get_bloginfo( 'description', 'display' );
if ( $site_description && ( is_home() || is_front_page() ) )
$title .= get_bloginfo( 'name' ); // ④ 首页追加站点名
if ( $paged >= 2 || $page >= 2 ) // ⑤ 第 2 页起,拼上"第 X 页"
$title = "$title $sep " . sprintf( '第%s页', max( $paged, $page ) );
$search = array('–', ' '); // ⑥ 去掉短横线实体和空格
$replace = array('-', '');
$title = str_replace($search, $replace, $title);
return $title; // ⑦ filter 的命根子
}
global 这行是把 $paged(列表分页号)和 $page(文章内部分页号)请进来。return——filter 的每条出口都要还值。str_replace 做两个替换:–(短横线的 HTML 实体)换成 -,空格换成空。写这篇文章的最大收获不是讲懂了参数,而是把代码摊开看时发现它有三处毛病。这三处都不影响网站运行,所以一直没暴露,但它们确实存在。
回看 header.php 里那句调用:
wp_title('', true, 'right');
// ↑ 第一个参数就是 $sep,我传了空字符串
wp_title() 的完整签名是 wp_title( $sep = '»', $display = true, $seplocation = '' ),第一个参数正是分隔符,默认是 »。我在这里传了 '',于是函数里的 $sep 拿到的是空字符串。
结果就是第 ⑤ 步 "$title $sep " 拼出来是”标题 + 两个空格 + 第2页”,紧接着第 ⑥ 步又把空格全删掉——最终显示成 某某文章第2页。
修法:把 header.php 里改成 wp_title('|', true, 'right'),或者在函数里写死分隔符,不依赖 $sep。
$search = array('–', ' ');
$replace = array('-', ''); // 空格 → 空,而且是全部空格
这正是我上一篇文章里讲的 str_replace“全部替换”——在这里它变成了副作用:文章标题里原本的空格也被一起干掉了。标题是 Hello World,输出就变成 HelloWorld。
中文标题不带空格,所以我的站从没出过问题;但只要有一篇英文标题的文章,就会露出马脚。这也说明一件事:一个函数在 A 处是解法,在 B 处可能就是 bug,得看它作用的字符串里到底有什么。
$title = $title; 是废代码自己赋值给自己,什么也没发生,直接删掉即可。估计是当初写着写着留下的痕迹。
wp_title() 是 WordPress 4.4 之前的标准做法。现在推荐的做法是在 functions.php 里声明一句:
add_theme_support( 'title-tag' );
然后删掉 header.php 里手写的 <title> 标签,让 WordPress 核心接管标题输出。
好处有两个:一是标题格式由核心统一维护,不用自己拼;二是 Yoast、Rank Math 这类 SEO 插件才能正常接管标题。主题手写 <title> 的话,插件和主题会各输出一套,互相打架。
我这个主题没声明 title-tag,目前跑得好好的,但属于遗留写法——哪天想装 SEO 插件,就得先动这一处。
回头看,add_filter('wp_title', 'lingfeng_wp_title', 10, 2) 这行代码里藏着四层信息:挂哪个点、谁去干、什么时候轮到我、我能拿几个参数。搞懂它之后再看主题里其他 add_filter,就都是同一个模子了。
但比参数更值钱的是第六条那三处问题——它们都不是靠”学语法”发现的,是靠把代码摊开、一行一行追问”这个变量此刻到底等于什么”挖出来的。 $sep 看着是个分隔符,实际是空字符串;str_replace 看着是清理格式,实际连标题里的空格一起吃了。代码不会报错,它就安安静静地错着,直到某天你翻到第 2 页、或者发了一篇英文标题的文章。
所以我的习惯改了:看到一段能跑的旧代码,别急着信,问一句”它真的在做我以为的那件事吗“。这次的答案是:三处里有一处半不是。
(这是 WordPress 踩坑系列的第五篇,上一篇是 add_filter 是 WordPress 独有的吗。)