四个缺口,按代价排序
GEO(生成式引擎优化)不是 SEO 换个名字。SEO 争的是结果页里的一个位置,GEO 争的是答案里的一句话。搜索引擎会点进你的页面;生成式引擎不会 —— 它读完、摘走一个结论、附上引用,然后结束。
所以问题从「我怎么排上去」变成「这一页有什么能被摘走」。这个转向让审计变得有用:它不问写得好不好,它问机器能不能判断这页在说什么、谁在说、说话的人是不是一个真实存在的实体。
四个维度,权重比分数重要:可引用性 35%、实体品牌 25%、技术可达 20%、结构化数据 20%。可引用性涨 10 分,比其他任何一项涨 10 分都值钱。第一份报告长这样:
| 维度 | 权重 | 博客 | 仓库 | 它衡量什么 |
|---|---|---|---|---|
| 技术可达性 | 20% | 73 | 81 | 爬虫抓不抓得到、解析得了吗 |
| 内容可引用性 | 35% | 71 | 66 | 有没有一句值得被引用的话 |
| 结构化数据 | 20% | 12 | 28 | 机器知不知道这是什么 |
| 实体与品牌 | 25% | 40 | 35 | 别人说不说得出你存在 |
| 总分 | 52 | 54 | 两边都是 C |
有两个数字直接指向真问题。Schema 12 和 28 不是「写得不好」,是「机器在猜」。实体品牌 40 在当时就已经是加权后最大的洞。其余都是噪音。
英文那一半不存在
这个站双语已经好几个月了。中文写在 HTML 里,英文放在 data-en / data-i18n 属性里,访客点 EN 时一小段脚本把两者换过来。设计本身是合理的:体积减半、单一真源、关掉 JS 中文读者照样能看。
但从爬虫的视角,这是一个纯中文站。GPTBot、ClaudeBot、PerplexityBot 都不执行 JS,它们拿到的 HTML 里只有中文。validate DBC against ARXML 这种英文查询在页面上没有任何东西可以匹配 —— 英文躺在爬虫永远不读的属性里。
Google 和 Bing 会渲染 JS。所以这个问题在传统搜索里完全看不出来:你的英文页排得好好的,你就以为这套方案没问题。只有生成式引擎会漏。要不是审计专门用非 JS 的 UA 抓了一次,我根本发现不了。
修法是把英文在构建期落成静态文件。脚本读每页、把英文从属性里取出来、生成镜像:index.en.html、blog/index.en.html、每篇文章一份。两边用 hreflang 互指,各自有自己的 canonical,sitemap 两个都列。

脚本里有两个决定值得抄。第一,它按解析器给出的偏移切原文,不重新序列化 DOM —— CSS、脚本、空白、属性顺序全部逐字节不变,diff 里只有改掉的文字,人能审。第二,它把镜像页上的语言切换器删掉,换成一句 plain 中文 链接。不删的话,localStorage 记着访客的偏好,静态英文页首帧就被翻回中文 —— 整个工程白做。
还有一个细节:镜像页的 FAQ schema 必须是英文的问答,不能只改个 inLanguage。英文页里挂中文问答比不挂更糟 —— schema 和正文对不上,摘取的时候一看就穿了。
Schema 12 分,因为一个字都没写
12 分不是写得差,是根本没有。整站一行 JSON-LD 都没有。机器本可以拿到的一切信号 —— 这是个真人、这些页面属于他、这是篇文章不是列表页 —— 全部只能从散文里猜。
关键不是「加上 schema」,是让它成为一个图。六块互不相干的标记 —— 这里一个 Person、那里一个 WebSite、每篇文章一个 TechArticle —— 告诉机器有六个陌生人。而一个 @graph、里面每个人物节点都指向同一个 @id,告诉机器:这是一个实体,有好几面。
所以首页只定义一次人物,锚在 https://me.tech-harry.top/#person:姓名、职务、雇主、sameAs 指向 GitHub / CSDN / PyPI 项目。其他页面全部引用这个 @id,不再重复姓名字符串。博客索引是 Blog,blogPost 指向各篇文章;每篇文章是 TechArticle,作者和发布者都是那个人物节点。
仓库那边问题更硬:GitHub 不给仓库输出任何 JSON-LD。这是平台限制,不是你写错了 —— 你没法往 github.com 上加标记。想让一个仓库作为「软件实体」被机器读懂,唯一的路是自己托管一个页面。于是有了 GitHub Pages 上的小文档站,index.html 里声明 SoftwareSourceCode,外加带 softwareVersion 和 offers.price: 0 的 SoftwareApplication。
offers.price: 0 不是花招。它回答了一个 AI 从散文里答不出来的问题:这东西免费吗。一个在第九段才提到 MIT 的工具页,会被免费工具推荐过滤掉;一个用机器可读形式声明 price: 0 的页面,会被纳入。
然后是 FAQPage。两篇文章本来都是「问 — 答」结构,但一个都没标,于是 Perplexity 和 AI Overviews 没有任何东西可以摘。每篇四个问题,就按人真会搜的形态写:为什么 ARXML 和 DBC 会对不上、AUTOSAR 位位置怎么对应 DBC 起始位、skill 和 prompt 有什么区别。这一项是整个审计里性价比最高的 —— 12 涨到 90。
有一条约束值得写下来:页面上可见的问答数量,必须和 schema 里的条数一致。我加了一条断言,两边都解析、数目不等就失败,然后故意改坏一处确认它真的会红。Schema 描述页面没有的内容,那不叫优化,那是一个机器抓得住的谎。
写了 s-maxage,一点用没有
这个站跑在 Cloudflare Worker 上,第一版对所有响应返回 cache-control: no-store。每个请求都重新生成页面,TTFB 2.1 到 3.0 秒。静态站这个速度不合理,而且速度是有代价的:爬虫有预算,慢的站它来得更少。
显然的修法是发 max-age=60, s-maxage=300 让边缘缓存。我改了。什么也没发生 —— 响应里始终不出现 cf-cache-status,TTFB 一动不动。原因是:对 Worker 的响应来说,s-maxage 对边缘只是建议值,不是指令。Worker 自己就是源站,没有一个独立的源站缓存供边缘去问。
真正管用的是 Cache API:显式把构建好的响应 put 进 caches.default,键自己设计。我用资源内容哈希做键,它有个好性质:新部署会改变哈希,所以旧页面不是「不太可能命中」,而是根本命中不了。
结果:TTFB 从 2.1–3.0 秒降到 0.58–0.78 秒。这是唯一一处纯技术改动真的推动了评分维度的地方 —— 因为爬取预算本身就是技术可达性的输入。
不是读代码,也不是 curl —— 那两样都会显示 header、看起来都对。我要看的是:第二次请求时 cf-cache-status 真的出现了,以及资源变化时键真的变了。没有观察到命中的缓存改动,等于没有缓存改动。
修好一个真 bug,GEO 一分没涨
第三轮给两篇文章加了作者卡和 FAQ 块。HTML 上线了、部署校验通过、线上页面里新段落都在。然后在浏览器里一看:作者卡整块塌了 —— 没有头像、没有圆角、宽度错乱。
原因是一处没人看过的缓存交叉:blog.css 用 max-age=86400 缓存,而所有页面用裸相对路径引用它。新的 CSS 类发出去了,但老访客最长一天内用的还是旧样式表。HTML 是新的,CSS 是旧的,两者对不上。
<!-- before: one URL, forever -->
<link rel="stylesheet" href="blog.css">
<!-- after: the URL changes when the file does -->
<link rel="stylesheet" href="blog.css?v=17a9da41d6">
修法是构建时把样式表自己的内容哈希拼上去,然后重算页面 ETag。一天缓存可以留着,但旧样式变得不可能拿到。这是标准做法,我没做,因为这个站小到一直没踩上。
接下来是这篇 GEO 文章里最该说的一句:这个修复让评分涨了 0 分。它不是技术可达性的输入,不是可引用性,也不是结构化数据。审计根本不管它。但它仍然是那一轮最有价值的改动 —— 因为它决定了一页是渲染正常还是坏的。
这个 bug 只在「改了 CSS 且访客有缓存」时存在。curl 没有缓存,源码 HTML 两种情况都正确。唯一的发现方式是在浏览器里读新元素的计算样式。这条教训很硬:改完 CSS 必须看渲染结果,不能只看 HTML 里有没有那个 class。
剩下 17 分,改代码没用
三轮之后博客 83、仓库 74。剩下的 17 分在哪 —— 这张图是整个过程中最有用的,因为它说的是停手:

实体品牌缺 38 分。按 25% 权重算,它拖掉 9.5 分,超过可引用性、结构化数据、技术可达三项之和(6.05)。而这一维度衡量的东西是:仓库有几个 star、有没有 Wikidata 条目、有没有 LinkedIn 档案、有没有第三方提到过它。没有一项是我代码的属性。
这个仓库是审计前一天建的。0 star、0 fork、0 issue。这不是「信号没做好」,这是对一个一天大的项目的如实读数。你骗不过它,也不该想骗它:这个指标存在的意义,就是让批量生成的内容不能凭空排上去。
我朝这个方向做的事都很慢:把包发上 PyPI,让项目除了一个 git URL 之外还有个外部身份;给三个 awesome-list 提了 PR,让别人维护的清单指向它;加了 CITATION.cff 并且写清适用范围边界 —— 可被判断的边界比笼统的吹嘘更值得被引用。这几件事的共同点是:结果不由我控制,维护者完全可以不 merge。
改代码把博客从 52 推到 83。最后 17 分是穿着指标外衣的分发问题。如果你在给一个没人听说过的项目做这件事,请预期:技术部分是快的,被认识是慢的 —— 而当分数不再动的时候,别再往 CSS 里投入一周。
下次我会按这个顺序做
-
像爬虫那样抓一次。不用浏览器、不用普通 UA 的 curl —— 用不执行 JS 的方式抓,只读返回的 HTML 里有什么。这次审计里一半的问题,从其他任何视角都看不见。
-
区分两种「一样」的失败。爬虫看不见你的内容、和看见了但不在意,表现都是没流量。前者是构建问题,一个下午能修;后者是内容问题,要几个月。
-
按「权重 × 缺口」排序,不按爽感排序。Schema 最好玩,值 20%;实体品牌最不好玩,占总分 25%。先做爽的没问题;以为爽的那个更重要就有问题。
-
在论断所在的介质里验证它。缓存的说法要看到真实的命中;渲染的说法要进浏览器;schema 的说法要用真正吃它的解析器验,不是 JSON linter —— 而且一个语法合法的文件,照样可以描述页面里没有的内容。
-
开始之前就写下哪些分不归你。否则你会在 83 分的时候反复打开编辑器,纳闷数字为什么不动。
还有一处对我自己的更正,应该写在这儿:我第三轮报告里第一版写的是 82 分。按权重算,94×0.2 + 89×0.35 + 90×0.2 + 62×0.25 = 83.45。我加权求和算错了 —— 和「在仓库里跑测试永远是绿的」是同一类错误。没有从分项重算过的总分,就是猜的。
数据与复现
| 项目 | 改前 | 改后 |
|---|---|---|
| 博客总分 | 52 (C) | 83 (B) |
| — 技术可达性 | 73 | 94 |
| — 内容可引用性 | 71 | 89 |
| — 结构化数据 | 12 | 90 |
| — 实体与品牌 | 40 | 62 |
| 仓库总分 | 54 (C) | 74 (B) |
| 首字节时间 | 2.1–3.0s | 0.58–0.78s |
| 英文静态页 | 0 | 4 |
| JSON-LD 节点 | 0 | 6 类 / 12 页 |
| FAQ 问答 | 0 | 12(中英各 6 组) |
两个站都开源,生成器都在仓库里,所以这些数字可以被核对而不只是被相信:英文镜像是脚本生成的,上面两张图也是脚本按审计数字画的,而部署在任何一条断言失败时就会拒绝发布 —— 包括「可见问答数 == schema 条数」和「样式表引用带内容哈希」这两条。
常见问题
GEO 和 SEO 有什么区别?
SEO 争的是结果页里的一个位置,GEO 争的是答案里的一句话。搜索引擎会点进你的页面,生成式引擎不会 —— 它先读、再摘、再引用。所以 GEO 的核心不是排名,是可被摘取:每段有独立完整的结论、每个事实有可核对的数字、每个实体有稳定的标识。
为什么我的英文内容 AI 搜不到?
如果英文只存在 JS 的数据属性里(data-en、i18n 字典),爬虫看不到。GPTBot、ClaudeBot、PerplexityBot 不执行 JS,于是英文查询在页面上匹配不到任何东西。Google 和 Bing 会渲染 JS,所以这个问题在传统搜索里完全看不出来。修法是构建期把英文落成静态页,不要依赖运行时切换。
Cloudflare Workers 上写 s-maxage 为什么没用?
因为对 Worker 的响应来说,s-maxage 对边缘只是建议而非指令,而 Worker 自己就是源站,没有独立的源站缓存供边缘去问。结果就是什么都没发生:不出现 cf-cache-status,TTFB 不动。必须显式调用 Cache API 把响应 put 进 caches.default,并且自己设计键。用资源内容哈希做键,旧页面就命中不了,而不只是「不太可能命中」。
为什么 schema 修好了分还是不动?
因为加权后最大的缺口是实体与品牌,权重 25%:那里缺 38 分,拖掉 9.5 分,超过其余三项之和。它衡量的是第三方说不说得出你存在 —— star、Wikidata 条目、LinkedIn 档案、外部引用。没有一项是你代码的属性,所以再怎么改代码也推不动它。