<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Crawlers · 仁才德</title>
    <link>https://jared.lynskey.co.nz/zh-cn/tags/crawlers/</link>
    <description>关于软件工程与研发团队领导力的短文——绩效评估、招聘与团队扩张、CI/CD、DevOps 监控、智能体 AI。写于首尔。</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <copyright>© 2026 仁才德</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/zh-cn/tags/crawlers/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>怎样被 ChatGPT 引用：一个小博客做了什么，日志又怎么说</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/</guid>
      <pubDate>Wed, 16 Sep 2026 00:00:00 +1200</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>SEO</category>
      <category>Crawlers</category>
      <category>LLM</category>
      <category>Hugo</category>
      <category>Cloudflare</category>
      <category>content strategy</category>
      <category>Writing</category>
      <description>想被 ChatGPT 等 AI 助手引用（GEO），我在这个博客上做了什么：该放行哪些 bot、不用浏览器就能读的 HTML、带作者的结构化数据，以及没人读的 llms.txt。</description>
      <content:encoded><![CDATA[<p>想被 ChatGPT 等 AI 助手引用，第一步是放行它们回答问题时来抓页面的检索 bot。这些 bot 和训练爬虫是两回事，拦住一种对另一种毫无影响。然后要给出不跑 JavaScript 也能读的 HTML，并带上写明作者的结构化数据。至于 llms.txt，从我的日志看根本没人来取，所以没用；最要紧的是写作时让每一节第一句就亮出结论，让任何一段都能单独被引用。</p>
<p>这个博客的搜索流量已经平了两三年，我也不指望它再动了。所以我转而去看访客都是从哪来的，AI 助手在不在其中。在，勉强算在。截至 2026 年 9 月 16 日的十二个月里，访问统计记到这里有 5,372 个会话。其中 27 个来自 AI 助手：12 个来自 chatgpt.com，9 个来自 gemini.google.com，6 个来自 Perplexity，来自 claude.ai 的是零。半个百分点。不过从 ChatGPT 来的访客是好的那种。他们平均停留五分钟，直接访问是 27 秒，Google 来的是三分钟，因为他们已经读过答案，想看的是细节。</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="十二个月按来源统计的会话数，AI 助手高亮显示"
    width="2400"
    height="1164"
    src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_590f408ed875db1f.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_590f408ed875db1f.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_d8da7a3f1d1c9190.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_b3dda7d23abf271d.webp"></span><figcaption>十二个月按来源统计的会话数。蓝色是 AI 助手：5,372 次会话里占 27 次。</figcaption></figure>
</p>
<p>所以我花了些时间琢磨，一个小博客怎么才能被选中当这些回答的来源。这篇写的是我在这个站上做了什么，以及日志对此怎么说。有数字的地方我给数字，靠猜的地方我说明是猜。初稿里有三句话，一查之下发现是错的，我把改正留在了文里。</p>

<h2 class="relative group">两种 bot，只有一种会引用你
    <div id="两种-bot只有一种会引用你" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%a4%e7%a7%8d-bot%e5%8f%aa%e6%9c%89%e4%b8%80%e7%a7%8d%e4%bc%9a%e5%bc%95%e7%94%a8%e4%bd%a0" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>第一件要弄清楚的事：&ldquo;AI bot&rdquo; 其实是两种不同的活，用的是不同的 user agent，拦住一种对另一种毫无影响。</p>
<p>训练爬虫成批地抓页面，用来训练下一个模型。OpenAI 的叫 GPTBot，Anthropic 的叫 ClaudeBot，Google 的由 Google-Extended 这个 token 控制，Apple 的是 Applebot-Extended，Common Crawl 的 CCBot 则喂着大多数开放数据集。它们拿走的东西最后变成权重，不会有任何链接指回你。拦掉这些是有道理的，我不觉得这想法傻。</p>
<p>检索 bot 是另一种。有人问 ChatGPT、Claude 或 Perplexity 一个问题时，助手会跑一次搜索，当场抓几个页面，读一遍，然后写出带引用的回答。这些抓取来自 OAI-SearchBot 和 ChatGPT-User、Claude-SearchBot 和 Claude-User、PerplexityBot 和 Perplexity-User。Google 的 AI Overviews 用的就是普通的 Googlebot，所以根本没有单独的开关。会把访客送回来的是这些 bot。如果目标是被引用，你要优化的就是它们，而且值得知道的是，拦不拦 GPTBot，ChatGPT 的搜索结果都不会因此变好或变坏。</p>
<p>这个站的 robots.txt 是 Hugo 的默认，也就是全部放行：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">User-agent: *
</span></span><span class="line"><span class="cl">Allow: /
</span></span><span class="line"><span class="cl">Sitemap: https://jared.lynskey.co.nz/sitemap.xml</span></span></code></pre></div></div>
<p>我想过把它拆开，放检索 bot 进来，把训练爬虫挡在外面，最后觉得对一个存在意义就是被人读的博客来说不值得。真要那么做的话，写出来就是下面这样。把它放出来是因为和<a href="https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-09-cloudflare-crawlers-cache/" >Cloudflare 给 Googlebot 发了一个空页面</a>里的教训一样：列表里写的得是 bot 的名字，不是厂商。没有哪一行能写成&quot;OpenAI&quot;。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">User-agent: GPTBot
</span></span><span class="line"><span class="cl">User-agent: ClaudeBot
</span></span><span class="line"><span class="cl">User-agent: Google-Extended
</span></span><span class="line"><span class="cl">User-agent: Applebot-Extended
</span></span><span class="line"><span class="cl">User-agent: CCBot
</span></span><span class="line"><span class="cl">Disallow: /
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">User-agent: *
</span></span><span class="line"><span class="cl">Allow: /</span></span></code></pre></div></div>
<p>站在 Cloudflare 后面的话，有一件事要查一下。从 2025 年年中起，那里新建的 zone 默认拦 AI 爬虫，bot 设置里还有一个一键开关，能对已有的 zone 做同样的事。它两种都拦。这个博客上周刚搬到 Cloudflare Pages，所以这件事在我的清单上。如果你因为&quot;AI bot&quot;听起来像该挡的东西就把它打开了，那你也把引用一起关掉了。Cloudflare 后来开始在它托管的 robots.txt 里写一行 <code>Content-Signal</code>，带 <code>search</code>、<code>ai-input</code> 和 <code>ai-train</code> 三个独立的标志。没人有义务遵守它，但这套词是对的：和上面一样的划分，只是表达成一种偏好，而不是一堵墙。</p>

<h2 class="relative group">真正来敲门的是谁
    <div id="真正来敲门的是谁" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%9c%9f%e6%ad%a3%e6%9d%a5%e6%95%b2%e9%97%a8%e7%9a%84%e6%98%af%e8%b0%81" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>搬到 Cloudflare 有个副作用：我头一次能按 user agent 看到每一个请求。下面是 2026 年 9 月 13 日到 16 日这四天进来的东西，假冒的已经剔掉了，那部分后面再说。</p>
<table>
	<thead>
			<tr>
					<th>Bot</th>
					<th>请求数</th>
					<th>做了什么</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>ClaudeBot（训练）</td>
					<td>1,777</td>
					<td>两天里抓了 1,380 个不同页面，五种语言全在内，另外读了 37 次 robots.txt</td>
			</tr>
			<tr>
					<td>meta-externalagent（训练）</td>
					<td>1,748</td>
					<td>一样，晚一天</td>
			</tr>
			<tr>
					<td>OAI-SearchBot（ChatGPT 的搜索索引）</td>
					<td>59</td>
					<td>五种语言的页面都有，robots.txt 读了十六次</td>
			</tr>
			<tr>
					<td>ChatGPT-User（回答时抓取）</td>
					<td>20</td>
					<td>冷启动那篇的日语版、自建更新服务器那篇、首页</td>
			</tr>
			<tr>
					<td>Claude-User（回答时抓取）</td>
					<td>8</td>
					<td>更新服务器那篇、联系页、首页</td>
			</tr>
			<tr>
					<td>PerplexityBot</td>
					<td>2</td>
					<td>iOS 调试那篇，英语版和越南语版</td>
			</tr>
			<tr>
					<td>Claude-SearchBot、Perplexity-User</td>
					<td>0</td>
					<td></td>
			</tr>
			<tr>
					<td>Googlebot / Bingbot / Applebot / CCBot</td>
					<td>168 / 71 / 45 / 28</td>
					<td></td>
			</tr>
	</tbody>
</table>
<p>有两点很显眼。训练爬虫把整个站读了两遍，用的时间只够检索 bot 读几十个页面。而检索 bot 抓的不是随机页面：ChatGPT-User 和 Claude-User 都奔着更新服务器那篇去了，那是站上最具体的一篇，日语的冷启动那篇被抓的也正是日语版。</p>
<p>再说假冒的。自称 GPTBot 的 20 个请求里，有 18 个来自 OpenAI 公布的地址段之外，要的是 <code>/.env</code>、<code>/.git/HEAD</code> 和 <code>/firebase-adminsdk.json</code>。36 个 &ldquo;ChatGPT-User&rdquo; 请求里有 15 个是同一路货色。&ldquo;PerplexityBot&rdquo; 的流量大多想要 <code>/id_ed25519</code>。user agent 就是一个谁都能发的字符串，漏洞扫描器已经发现 AI bot 的名字会被放行。OpenAI、Anthropic 和 Perplexity 都公布了自己的 IP 段。如果你打算对某个 bot 特殊对待，不管是放还是拦，查地址，别查名字。上面的 20、59 和 8，是从正确地址来的那些请求。</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="声称是 OpenAI user agent 的请求，按是否来自 OpenAI 公布的 IP 段拆分"
    width="2400"
    height="722"
    src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_e7035266dc7ce0b1.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_e7035266dc7ce0b1.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_638e8c6286f5c91f.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_11d44262308d7c6c.webp"></span><figcaption>同样的 user agent 字符串，按来源地址拆开。多数自称 &lsquo;GPTBot&rsquo; 的流量根本不是 OpenAI 发来的。</figcaption></figure>
</p>

<h2 class="relative group">页面不用浏览器也得能读
    <div id="页面不用浏览器也得能读" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%a1%b5%e9%9d%a2%e4%b8%8d%e7%94%a8%e6%b5%8f%e8%a7%88%e5%99%a8%e4%b9%9f%e5%be%97%e8%83%bd%e8%af%bb" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>检索 bot 在回答的那一刻抓取，有超时，而且大多数不跑 JavaScript。一个在 bundle 加载完之前只是一个空 div 的页面，就是它们读不到的页面。Hugo 输出的是完整的 HTML，所以这个博客白得了这一点，这也是我从没动过心思把它搬到什么更聪明的东西上的主要原因。我做的市场应用是反面例子，一个 React Native 的 Web 应用，为了让爬虫拿到一份真正的文档，先做了一条动态渲染分支，然后又被一个缓存 bug 悄悄抵消了。上面那篇 Cloudflare 的文章讲的就是这事。</p>
<p>速度也是同样的道理，而这里日志纠正了我。我原本以为，静态站放在 Cloudflare 后面，就是从最近的边缘节点出去的。不是。这个博客放在 Cloudflare Pages 上，HTML 是不经缓存发出去的，从首尔发请求，响应从洛杉矶回来，大约要半秒。对一个超时有好几秒的 bot 来说这仍然够用，但它不是我原本会张口就说的几十毫秒，而且我知道这一点只是因为我测了。一个正从六个页面里拼答案的助手，会把那个要花三秒的丢掉，而且不会告诉你它丢了。</p>

<h2 class="relative group">写得让一段话能单独被摘出来
    <div id="写得让一段话能单独被摘出来" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%86%99%e5%be%97%e8%ae%a9%e4%b8%80%e6%ae%b5%e8%af%9d%e8%83%bd%e5%8d%95%e7%8b%ac%e8%a2%ab%e6%91%98%e5%87%ba%e6%9d%a5" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这些系统用页面的方式和读者不一样。它们把页面切成块，给每块按问题打分，引用得分高的那些。一篇用八百字铺垫到结论的文章，每一块的分都低。一篇每一节第一句就亮出观点、后面再论证的文章，会在匹配的那一节得分。</p>
<p>这件事对我在这里写东西的影响，比任何技术上的事都大。front matter 里的 description 现在是对文章内容的概括，不是勾你点进来的钩子，因为 description 往往是助手唯一展示的部分。小节标题说的是这一节的结论。数字连同单位写进句子，而不是放在图表里。日期写全，因为&quot;上周&quot;对一年后读这页的模型来说什么都不是。还有，一篇文章只讲一件事。</p>
<p>另一半是要有值得引用的东西，而这里的数据是掺杂的。日志里，回答时的 bot 抓的都是具体的那些页面：冷启动、更新服务器、iOS 调试。Naver 的爬虫叫 Yeti，Daum 的叫 Daumoa，我用的托管平台上机器重启是一个 30 秒的 504 窗口，Cloudflare 在线做的 Brotli 在我的 bundle 上测出来比 gzip 还差。这些事实没几个地方写过，所以需要其中某一条的问题就会落到这里。可是从助手那边点进来的人，大多落在 2023 年和 2024 年那些讲管理的文章上，每篇一两个会话。具体的文章被机器读，泛泛的文章拿到点击。这我给不出一个利落的解释，宁可照实报出来，也不想编一个。杠杆要是有的话，仍然是把你这周弄明白的、当初搜也搜不到的那件具体的事写下来，因为机器明摆着读的就是这些。</p>

<h2 class="relative group">说清楚是谁写的，而且每次都说一样的
    <div id="说清楚是谁写的而且每次都说一样的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%af%b4%e6%b8%85%e6%a5%9a%e6%98%af%e8%b0%81%e5%86%99%e7%9a%84%e8%80%8c%e4%b8%94%e6%af%8f%e6%ac%a1%e9%83%bd%e8%af%b4%e4%b8%80%e6%a0%b7%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>一个助手在决定要不要引用一个页面时，做的事和一个认真的读者差不多：这是谁写的，什么时候写的，和我找到的其他东西对得上吗？这个站上这部分的机制大多是主题的功劳。每篇文章都带 Article 结构化数据，有作者、发布日期和修改日期，作者是一个 Person，链接到同一组公开主页。这在模板里只是几行，却是&quot;一个页面&quot;和&quot;一个有来历的、署了名的人写的页面&quot;之间的区别。</p>
<p>不是主题功劳的那部分是一致性。关于页、llms.txt、各个公开主页和每篇文章顶上的那一行，描述的都是同一个人，同一段工作经历。有东西变了，就全都要改，不然忘掉的那个就成了错的那个。听起来挺较真，但一个页面说我是工程经理、另一个说我是自由职业，正是那种会让模型含糊其辞、或者干脆换个人引用的矛盾。</p>
<p>修改日期也得老实。内容变了它才变，否则我不碰它。为了显得新鲜而去改它，是那种管用一次、然后整个域名都被打折扣的事。</p>

<h2 class="relative group">llms.txt 是什么，以及没人读它这件事
    <div id="llmstxt-是什么以及没人读它这件事" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#llmstxt-%e6%98%af%e4%bb%80%e4%b9%88%e4%bb%a5%e5%8f%8a%e6%b2%a1%e4%ba%ba%e8%af%bb%e5%ae%83%e8%bf%99%e4%bb%b6%e4%ba%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>llms.txt 是 2024 年 9 月提出的一个方案：在站点根目录放一个 Markdown 文件，给语言模型一段站点概述和一份精选的链接列表。我今年 2 月加了一份。开头一段讲我是谁，然后按管理、基础设施、移动和社区分节，每篇文章一行：标题、URL，加一句话说这篇文章的结论。读起来像一份写给赶时间的人的目录。</p>
<p>要说明的是，没人读它。在我手上这四天的日志里，请求过 <code>/llms.txt</code> 的只有我自己。Common Crawl 在 2026 年 8 月那一轮抓取里收了这个站的 76 个页面，从来没收过这个文件。没有哪家主要的助手说过它会读它。所以这里有什么被引用都不是因为它，我也不会以此为由推荐给任何人。留着它有两个原因。它只花了一个小时，而且给每篇文章写一句&quot;这篇到底得出了什么结论&quot;，比文件本身更值，因为它让我看到了哪些文章什么结论都没有。它也是站上唯一一个告诉机器这个站是干什么的页面，我宁愿有它而用不上。之后它已经跑偏了，因为我是手写的，会忘，这也是应该改成用 Hugo 的输出格式从 front matter 生成它的理由。这事在清单上。</p>

<h2 class="relative group">Feed、sitemap 和五种语言
    <div id="feedsitemap-和五种语言" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#feedsitemap-%e5%92%8c%e4%ba%94%e7%a7%8d%e8%af%ad%e8%a8%80" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>无聊的管道照样算数。sitemap 是爬虫在新文章上线当天找到它的方式。RSS 是它得知一篇文章改过的方式。两个都是 Hugo 的默认，我从没需要操心。ClaudeBot 两天里读了 36 次 sitemap，它就是这样那么快找到 1,380 个页面的。</p>
<p>语言是我没料到会有影响的部分。这里的所有内容都有英语、日语、韩语、简体中文和越南语版本，带 alternate 链接，让爬虫知道它们是同一篇文章。爬虫把它们当成五个站：ClaudeBot 的抓取几乎是在五种语言之间平分的，Common Crawl 8 月收走的 73 个文章页面里，只有四个是英语的。助手用被提问的语言回答，也偏向那种语言的来源，所以一个关于冷启动的韩语问题，可以直接从韩语页面回答，而不是现场把英语页面翻一遍，而 ChatGPT 实际抓走的正是日语那一页。条件是译文读起来得像用那种语言写的。今年早些时候我请母语者帮着检查做的那一遍修订，本来是为了人类读者，但也正是它让越南语页面成了一个来源，而不是一个稀奇玩意儿。</p>

<h2 class="relative group">我能测量什么
    <div id="我能测量什么" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%91%e8%83%bd%e6%b5%8b%e9%87%8f%e4%bb%80%e4%b9%88" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>比我以为的多，只要别再靠猜。Cloudflare 的免费套餐给的是按 user agent 和路径分的请求日志，一次查询一天，上面那张 bot 表就是从这来的；referrer 要付费套餐才有。访问统计工具里有 referrer，通过它的报表 API 把数据拉出来，而不是眯着眼看仪表盘，开头那些数字就是这么来的。两边合起来能看出个大致轮廓。四天里，检索 bot 从 OpenAI 和 Anthropic 的地址抓了大约九十个页面。上一个月，有两个人从 ChatGPT 过来。读得多，偶尔引，很少点。据我所知，还是没有办法看到那些用了某个页面却没被点击的回答。最有用的检查依然是手动的那种：拿你文章回答的那个问题去问助手，看它引的是谁的页面。</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="十三个月里每月来自 AI 助手的会话数，看不出趋势"
    width="2400"
    height="968"
    src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_cd5dc99be8b0cfc3.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_cd5dc99be8b0cfc3.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_7d401a84f177509c.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_5c308d89ad110ef7.webp"></span><figcaption>AI 助手带来的访问按月分布。没有什么趋势可言。</figcaption></figure>
</p>
<p>如果这一切里只能留一样，我留写作那条规则。robots.txt、结构化数据和 llms.txt 是一个下午的活，做完就完了。让每一节先说观点，写具体的事而不是泛泛的事，是一种习惯，而正是这个习惯决定了一个页面到底值不值得引用。第二样是：先测了再说。被机器信任，结果看起来和被人信任很像：说你是谁，说你知道什么，而且每次都用同一种说法。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>React Native Web 的 SEO：Cloudflare 给 Googlebot 发了一个空页面</title>
      <link>https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-09-cloudflare-crawlers-cache/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/zh-cn/posts/2026/2026-09-09-cloudflare-crawlers-cache/</guid>
      <pubDate>Wed, 09 Sep 2026 04:00:00 +1200</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Cloudflare</category>
      <category>SEO</category>
      <category>Django</category>
      <category>Caching</category>
      <category>Crawlers</category>
      <category>Naver</category>
      <category>React Native</category>
      <description>我怎么用动态渲染让 React Native Web 单页应用被 Google 和 Naver 爬虫看见，以及那条让 Googlebot 拿到空 SPA 外壳的 Cloudflare 缓存规则。</description>
      <content:encoded><![CDATA[<p>用 React 或 React Native Web 做的单页应用，Naver 的爬虫（Yeti）和 Bing 都不会执行它的 JavaScript，Google 也只是有时会。解决办法是动态渲染：按 User-Agent 识别爬虫，给它返回服务端渲染的 HTML，内容和应用本来要画出来的一样。在 Cloudflare 上要避开的坑，是把这个 URL 对所有人一起缓存。Cloudflare 对 HTML 不看 <code>Vary</code>，谁的响应先到，后面所有访客拿到的就是谁的，Googlebot 也可能因此拿到空外壳。只缓存爬虫请求，收窄规则之后记得清一次缓存。</p>
<p>我做的这个集市应用是用 React Native Web 写的单页应用。在 JavaScript 跑起来之前，每一个商品页都只是一个 script 标签加一个空 div。这对人没问题，对搜索却是致命的：Google 有时能执行那段 JS，Bing 不会，而对一个商品几乎全是韩语的站点最要紧的 Naver 爬虫，肯定不会。</p>
<p>这篇讲的是我怎么让这些页面被看见，以及之后那条以我没预料到的方式把成果抵消掉的缓存规则。</p>

<h2 class="relative group">动态渲染，不是伪装
    <div id="动态渲染不是伪装" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8a%a8%e6%80%81%e6%b8%b2%e6%9f%93%e4%b8%8d%e6%98%af%e4%bc%aa%e8%a3%85" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>办法很老，而且 Google 认可：识别爬虫，给它返回一份服务端渲染的 HTML，内容和 SPA 本该画出来的一样。不是不同的内容，是同样的内容，只是更早。</p>
<p>于是像 <code>/listings/view?id=...</code> 这样的商品 URL 分叉了。浏览器拿到 SPA 外壳。爬虫拿到一份真正的文档：一个标题，商品的关键信息，每张照片一张带 alt 的图，一段正文摘录，以及通往几个相关商品的链接，好让这页不至于成为孤儿。</p>
<p>最后那点比我预想的重要得多。在这些内链存在之前，大约一千个商品页没有任何东西指向它们，Search Console 里能看到抓取一路沉到每天 7 到 11 次请求。找不到你页面的爬虫，不会在乎那些页面有多好。</p>

<h2 class="relative group">陷阱：厂商的名字不是它家爬虫的名字
    <div id="陷阱厂商的名字不是它家爬虫的名字" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%99%b7%e9%98%b1%e5%8e%82%e5%95%86%e7%9a%84%e5%90%8d%e5%ad%97%e4%b8%8d%e6%98%af%e5%ae%83%e5%ae%b6%e7%88%ac%e8%99%ab%e7%9a%84%e5%90%8d%e5%ad%97" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>爬虫识别是 user-agent 匹配，问题就出在这里。</p>
<p>韩国平台用的爬虫名字和公司名一点都不像。Naver 的爬虫叫 <strong>Yeti</strong>，Daum 的叫 <strong>Daumoa</strong>，Kakao 的链接抓取器叫 <strong>kakaotalk-scrap</strong>。三个都不是厂商名。</p>
<p>与此同时，同样这几家还发布带内嵌浏览器的 App，那些浏览器发送的是真实浏览器 UA，后面缀上 App 的名字。KakaoTalk 的内置浏览器说自己是 <code>KAKAOTALK/10.5.1</code>，Kakao 的抓取器说自己是 <code>kakaotalk-scrap/1.0</code>。</p>
<p>用 <code>kakao</code> 去匹配，两个都会中。我当初正是这么干的，结果每一个打开链接的 KakaoTalk、Naver App、Daum App 用户，拿到的都是爬虫页：没有 JavaScript，没有导航，没有登录。一份挑不出毛病的文档，对人却毫无用处。</p>
<p>修复靠的是判断顺序，这一点值得明确写成规则：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 内嵌浏览器会在普通浏览器 UA 后面附上应用名。</span>
</span></span><span class="line"><span class="cl"><span class="n">INAPP_BROWSER_RE</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;kakaotalk/|naver\(inapp|daumapps&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 认机器人名，不认厂商名：Naver 用 Yeti，Daum 用 Daumoa，</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Kakao 的链接预览抓取器用 kakaotalk-scrap。</span>
</span></span><span class="line"><span class="cl"><span class="n">CRAWLER_RE</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;googlebot|bingbot|yeti/|daumoa|kakaotalk-scrap&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">is_crawler</span><span class="p">(</span><span class="n">user_agent</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 内嵌浏览器永远不是爬虫，所以这个判断放在最前面。</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">user_agent</span> <span class="ow">or</span> <span class="n">INAPP_BROWSER_RE</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">user_agent</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="n">CRAWLER_RE</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">user_agent</span><span class="p">))</span></span></span></code></pre></div></div>
<p>内嵌浏览器先判断，而且它赢。并且每一个爬虫匹配词都必须指向一个<strong>机器人</strong>，而不是一个厂商。</p>

<h2 class="relative group">然后我在前面放了 Cloudflare
    <div id="然后我在前面放了-cloudflare" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%84%b6%e5%90%8e%e6%88%91%e5%9c%a8%e5%89%8d%e9%9d%a2%e6%94%be%e4%ba%86-cloudflare" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>源站是悉尼的一台共享 CPU 机器，能撑住每秒二三十个请求。Googlebot 要走大约 995 个 URL，而它们全都打到了悉尼，因为每一个 URL 的 <code>cf-cache-status</code> 都是 <code>DYNAMIC</code>：<strong>除非有规则明说，Cloudflare 不缓存 HTML。</strong> 整个站点大约 16% 的内容被缓存，而真正要紧的那部分一个也不在里面。</p>
<p>那就给爬虫页加一条缓存规则。很简单。除了一点：一个商品 URL 并不只有一个响应体，它有三个。</p>
<ul>
<li>要 HTML 的爬虫拿到服务端渲染页。</li>
<li>人拿到 SPA 外壳。</li>
<li>发送 <code>Accept: text/markdown</code> 的 agent 拿到 Markdown，从数据库拼出来的，因为把一个空的 HTML 外壳转换一遍只会得到一份空文档。</li>
</ul>
<p>源站会发 <code>Vary: Accept</code>，而对 user-agent 做 <code>Vary</code> 本来也没什么指望。然后就是我不知道的这件事：</p>
<blockquote><p><strong>Cloudflare 在 HTML 上忽略 <code>Vary</code>。</strong> 三个响应体共用一个缓存键。</p>
</blockquote><p>哪个先到，哪个就占住这个 URL，对该边缘节点之后的所有访客生效，持续源站 <code>s-maxage</code> 允许的一小时，再加上一天的 <code>stale-while-revalidate</code>。</p>
<p>我在生产环境两个方向都亲眼见到了：</p>
<ul>
<li>一个 Naver App 用户用爬虫页预热了某个商品 URL，随后普通的 iPhone Safari 就在一次 HIT 上拿到了那个页面。一个人盯着一份没有 JavaScript 的文档。</li>
<li>一个人用 SPA 外壳预热了某个 URL，Googlebot 拿到了空外壳。整套 SEO 工作要防的正是这个失败，如今它更快了，还是从边缘发出来的。</li>
</ul>

<h2 class="relative group">靠收窄来修复的规则
    <div id="靠收窄来修复的规则" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%9d%a0%e6%94%b6%e7%aa%84%e6%9d%a5%e4%bf%ae%e5%a4%8d%e7%9a%84%e8%a7%84%e5%88%99" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>你没法让 Cloudflare 在 HTML 上尊重 <code>Vary</code>。你能做的，是确保三个响应体里只有<strong>一个</strong>有资格进缓存：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">(starts_with(http.request.uri.path, &#34;/listings/&#34;)
</span></span><span class="line"><span class="cl"> and not any(http.request.headers[&#34;accept&#34;][*] contains &#34;text/markdown&#34;)
</span></span><span class="line"><span class="cl"> and (lower(http.user_agent) contains &#34;googlebot&#34; or ...)
</span></span><span class="line"><span class="cl"> and not (lower(http.user_agent) contains &#34;kakaotalk/&#34; or ...))</span></span></code></pre></div></div>
<p>只匹配爬虫 UA，并排除 Markdown 请求。现在人的请求不匹配任何规则，于是读到 <code>DYNAMIC</code> 并像以前一样回源。只有爬虫请求读写缓存，而能进到缓存里的响应体只有爬虫那一个。</p>
<p>边缘 TTL 用 <code>respect_origin</code>，于是有效期仍归渲染这个页面的代码所有，而不是被钉死在控制台里。</p>
<p>一个容易漏掉的运维细节：<strong>收窄规则并不会驱逐已经在缓存里的东西。</strong> 得手动清一次缓存，否则那些刚刚已不再生成的污染条目还会继续被发出去。</p>

<h2 class="relative group">两份清单，以及它们的错误为何不对称
    <div id="两份清单以及它们的错误为何不对称" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%a4%e4%bb%bd%e6%b8%85%e5%8d%95%e4%bb%a5%e5%8f%8a%e5%ae%83%e4%bb%ac%e7%9a%84%e9%94%99%e8%af%af%e4%b8%ba%e4%bd%95%e4%b8%8d%e5%af%b9%e7%a7%b0" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>这条规则需要一份爬虫 UA 清单，而 Django 里已经有一份。同一份知识在两个系统里存两份通常是坏味道，但这里有意思的是它们漂移时会发生什么，因为并不对称：</p>
<ul>
<li><strong>Cloudflare 里的机器人清单必须始终是 Django 那份的子集。</strong> 一个 Cloudflare 认为是机器人而 Django 不认的匹配词，会把 SPA 外壳缓存到爬虫专用键下，然后发给真正的爬虫。</li>
<li><strong>内嵌浏览器的排除清单必须是 Django 那份的超集。</strong> 排除得太多，代价只是少几次缓存命中。</li>
</ul>
<p>任何一边朝安全方向错，你只是少缓存一点。第一条朝另一个方向错，你就又在给 Googlebot 发空页面了。顺带一提，这种不对称也正是这些清单用 <code>contains</code> 而不是正则的原因：<code>matches</code> 需要 Business 套餐，而对一份允许在已知安全方向上粗糙的清单，<code>contains</code> 够用了。</p>

<h2 class="relative group">一条让构建失败、而不是让页面失败的不变式
    <div id="一条让构建失败而不是让页面失败的不变式" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%80%e6%9d%a1%e8%ae%a9%e6%9e%84%e5%bb%ba%e5%a4%b1%e8%b4%a5%e8%80%8c%e4%b8%8d%e6%98%af%e8%ae%a9%e9%a1%b5%e9%9d%a2%e5%a4%b1%e8%b4%a5%e7%9a%84%e4%b8%8d%e5%8f%98%e5%bc%8f" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>爬虫页里嵌着有效期七天的预签名图片 URL。页面本身在边缘缓存一小时，外加一天的 stale-while-revalidate。</p>
<p>这两个数字是相关的，一旦缓存寿命超过图片寿命，边缘就会开始发出每张照片都 403 的页面。不会有任何告警，因为 HTML 本身没问题。</p>
<p>所以定义这些数值的代码，在关系被破坏时会拒绝导入：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">CRAWLER_IMAGE_TTL</span> <span class="o">=</span> <span class="mi">7</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">3600</span>  <span class="c1"># 预签名图片 URL</span>
</span></span><span class="line"><span class="cl"><span class="n">EDGE_S_MAXAGE</span> <span class="o">=</span> <span class="mi">3600</span>
</span></span><span class="line"><span class="cl"><span class="n">EDGE_STALE_WHILE_REVALIDATE</span> <span class="o">=</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">3600</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 边缘缓存可能比图片 URL 活得更久时，在 import 时就失败。</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">EDGE_S_MAXAGE</span> <span class="o">+</span> <span class="n">EDGE_STALE_WHILE_REVALIDATE</span> <span class="o">&gt;=</span> <span class="n">CRAWLER_IMAGE_TTL</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">&#34;edge cache lifetime must stay below the image URL lifetime&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>两个分处不同文件的常量之间的约束，在代码评审里看不见，在进程启动时却一目了然。把它放在进程启动的地方。</p>

<h2 class="relative group">Cloudflare 做的两件、我不得不撤销的事
    <div id="cloudflare-做的两件我不得不撤销的事" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#cloudflare-%e5%81%9a%e7%9a%84%e4%b8%a4%e4%bb%b6%e6%88%91%e4%b8%8d%e5%be%97%e4%b8%8d%e6%92%a4%e9%94%80%e7%9a%84%e4%ba%8b" aria-label="锚点">#</a>
    </span>
    
</h2>
<p><strong><code>Vary: Origin</code> 把打包产物挡在了缓存之外。</strong> <code>django-cors-headers</code> 会在它经手的每个响应上打 <code>Vary: Origin</code>，而 Cloudflare 拒绝缓存任何在 <code>Accept-Encoding</code> 之外还有 vary 的响应。这就悄悄把关键路径上最大的那个 1.6 MB JavaScript 包整个排除在边缘之外。修法是把一个中间件放在列表最前面，让它在出站方向最后执行，重写静态资源路径上的 <code>Vary</code>。</p>
<p><strong>边缘压缩比源站压缩更差。</strong> 让 Cloudflare 现场做 Brotli 得到 2,041,742 字节，而源站自己的 gzip 是 1,658,115 字节。为了不用动脑，代价是大了 23%。即时压缩优化的是 CPU，不是压缩率；源站预压缩的资源会赢。</p>

<h2 class="relative group">不等抓取就告诉搜索引擎
    <div id="不等抓取就告诉搜索引擎" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%8d%e7%ad%89%e6%8a%93%e5%8f%96%e5%b0%b1%e5%91%8a%e8%af%89%e6%90%9c%e7%b4%a2%e5%bc%95%e6%93%8e" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>两个协议，加在一起覆盖面也令人失望。</p>
<p><strong>IndexNow</strong> 接受一次 ping，覆盖 Bing、Yandex 和 Seznam。Google 和 Naver 不参与。每次保存商品时，我在后台带着很短的超时发一次 ping，已售出和已关闭的条目直接跳过。网络失败只记一条警告，绝不让触发它的那次保存失败——这是一个尽力而为的旁路通道唯一可以接受的行为。</p>
<p><strong>Google 的 Indexing API</strong> 只接受两种结构化数据类型，其中之一正是我有的 <code>JobPosting</code>。在招的职位发 <code>URL_UPDATED</code>，关闭的发 <code>URL_DELETED</code>。那条删除通知之所以诚实，是因为关闭的条目同时输出了 <code>noindex</code>，代码里也这么写着——好让日后拿掉 noindex 的人知道自己还顺手破坏了什么。</p>
<p>这里有两处让我耗了不少时间。配额是每天 200 个 URL，而批量导入一次商品就会触发几百次，所以导入进来的条目会被跳过，否则一次导入就把当天预算全花光。另外端点是带冒号的 <code>urlNotifications:publish</code>。用斜杠的话，Google 返回一个普通的 HTML 404，看起来和 API 没启用一模一样。</p>

<h2 class="relative group">结构化数据教给我的
    <div id="结构化数据教给我的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%bb%93%e6%9e%84%e5%8c%96%e6%95%b0%e6%8d%ae%e6%95%99%e7%bb%99%e6%88%91%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<p>三条规则，每一条都是被 Search Console 数落出来的：</p>
<ol>
<li><strong>解析不了的就别输出。</strong> 薪资是自由文本。我以前把原始字符串当作 <code>baseSalary.value</code> 输出，那是无效的，还让每一条职位都被标记缺少单位。现在解析不了的薪资干脆不出现。</li>
<li><strong>别为了满足结构化数据的要求而编造字段。</strong> 招聘广告很少带街道地址。人会很想给 <code>PostalAddress</code> 填个看着像样的东西。招聘广告上编造的地址比不完整的地址更糟，所以只放城市和国家。</li>
<li><strong>有效期不是可选项。</strong> Google 会丢掉没有 <code>validThrough</code> 的 <code>JobPosting</code>，所以要推导一个，并加上下限，免得一条旧条目声称自己昨天就过期了。</li>
</ol>
<p>还有关于站点地图的第四条：<strong>别给所有条目都声称 <code>changefreq: daily</code>。</strong> 要求 Googlebot 每天重新抓取一千个条目，会被读成一台过载的主机，而它的回应是降低整站的抓取速率。从这东西实际上多久变一次来推导。</p>
<p>站点地图还有一道质量下限：标题加正文合计四十个字符。Search Console 曾把大约一千条报成&quot;已抓取 – 尚未编入索引&quot;，其中某个二手页面的全部独有内容，是一个单词的标题加一个电话号码。四十这个数字是故意定低的，因为 484 条里有 471 条是韩语，而韩语单位字符承载的信息更多；按英语调的阈值会砍掉这个市场的一半。内容单薄的条目仍然通过索引页保持可抓取，一旦有人编辑就会自动重新进入站点地图。</p>

<h2 class="relative group">我会从中带走的
    <div id="我会从中带走的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%91%e4%bc%9a%e4%bb%8e%e4%b8%ad%e5%b8%a6%e8%b5%b0%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h2>
<ul>
<li><strong>在一个 URL 上返回不同的响应体，首先是一个缓存键问题。</strong> 弄清楚你的 CDN 用什么做键，并且在你于那个确切的内容类型上验证之前，就假定它忽略 <code>Vary</code>。</li>
<li><strong>当两个系统必须就一份清单达成一致时，先弄清哪个方向的错误是安全的</strong>，然后把它写在两份副本旁边。&ldquo;它们可以漂移，而且只允许朝这个方向漂&rdquo;，胜过假装它们不会漂。</li>
<li><strong>按机器人名字匹配机器人，永远别按厂商名字。</strong> 尤其在英语世界之外，那个厂商同时还做着你用户手里的那个浏览器。</li>
<li><strong>把相距遥远的常量之间的不变式，放在会大声失败的地方。</strong> import 时机是免费的，而且没人能跳过。</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-09-09-cloudflare-crawlers-cache/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
