AdSense 控制台横幅写着 “Earnings at risk — you need to fix some ads.txt file issues to avoid severe impact to your revenue”。你加了文件,但警告不消。或者 https://yourdomain.com/ads.txt 返回 404,但你确定推送了。
最快修复: 从 AdSense 复制那行精确文本(首页有提示就点 Fix now,否则进 Sites → 你的站 → Ads.txt snippet → Copy),放进框架的静态目录,让它在 https://yourdomain.com/ads.txt 以 HTTP 200 和 Content-Type: text/plain 返回,部署,再用一条 curl 确认。然后就是等——AdSense 按自己的节奏重爬,多数情况几天(低流量站可能要一个月,见 Google 官方文档)。所以「文件验证已正确、横幅却还挂着」通常只是爬取延迟,不是真问题。
ads.txt 规范本身很简单——根目录一个纯文本文件,列出授权 seller——但部署上有几个坑能套住大多数独立站。
你属于哪一类?
先跑这一条,结果会告诉你该看哪一节。
curl -sIL "https://yourdomain.com/ads.txt"
| 你看到 | 最可能的原因 | 看这里 |
|---|---|---|
404 / 403 | 路径错,或框架没把文件 ship 出去 | 原因 1、2 |
200 但 Content-Type: text/html | 平台 / CDN 把 .txt 改写成 HTML | 原因 3 |
200、text/plain,但内容不对 | publisher ID 错、BOM、智能引号 | 原因 4 |
200、内容对,但只有加 ?nocache=... 才对 | CDN 缓存陈旧 | 原因 5 |
301/302 跳到另一个域名 | 重定向离开了根域名 | 原因 6 |
| 全都对,横幅还在 | AdSense 还没重爬 | 原因 7 |
-L 会跟随重定向,让你看到最终响应,而不是第一跳。
常见原因
按命中率从高到低。
1. 文件路径错
ads.txt 必须能在 https://yourdomain.com/ads.txt 解析到——不是 /public/ads.txt、不是 /assets/ads.txt、不是 /static/ads.txt。Google 的抓取「will start at the root domain, and the root domain needs to return from, or redirect to, the ads.txt file」(Google 文档)。它只命中这一个 URL。
怎么判断:
curl -sI "https://yourdomain.com/ads.txt"
要看到 HTTP/2 200。其它(404、跳错地方的 301、403)都说明根目录解析不到。
2. 框架没把文件 ship 出去
文件源放在仓库的静态目录里,build 会把它复制到 web 根目录。放进 src/ 就永远不会被部署。
- Astro:
public/ads.txt - Next.js(pages 或 app router):
public/ads.txt - SvelteKit:
static/ads.txt - Nuxt 3:
public/ads.txt(Nuxt 2 旧版用static/) - Hugo / Jekyll:
static/ads.txt/ 仓库根 - Vercel 静态部署: 项目根或
public/
怎么判断: 部署后 curl https://yourdomain.com/ads.txt。404 就是 build 没带它——检查文件是否已提交、是否在静态目录里、有没有被 gitignore 掉。
3. Content-Type 错,或返回 HTML「软 404」
某些配置(nginx 配错、CDN 的兜底改写规则、或者把未知路径都返回 index.html 的 SPA)会把 .txt 用 Content-Type: text/html 返回,或者返回一个状态码是 200 的 HTML 404 页。AdSense 按 HTML 解析,找不到 seller 行,就当文件缺失。
怎么判断:
curl -sI "https://yourdomain.com/ads.txt" | grep -i content-type
应该是 Content-Type: text/plain(常见 text/plain; charset=utf-8)。如果是 text/html,说明平台在改写,或者 SPA 兜底吞掉了这个请求。
4. publisher ID 错或夹带不可见字符
这一行必须是:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
四个逗号分隔的字段:广告系统域名(google.com)、你的 publisher ID、关系类型(DIRECT)、Google 的认证机构 ID(f08c47fec0942fa0,所有 AdSense 发布商都一样)。常见破坏因素:尾部空格、复制粘贴带来的智能引号、文件开头的 BOM、写错的 pub- 号、漏了逗号。
怎么判断:
curl -s "https://yourdomain.com/ads.txt" | od -c | head -3
留意意外的字节——UTF-8 BOM 会在文件最开头显示为 357 273 277。有的话,把文件重新存为不带 BOM 的 UTF-8。
5. Cloudflare / CDN 缓存还在返回旧版
你改了文件,但缓存里的旧副本仍然胜出。AdSense 爬到的还是旧的(缺失)状态。
怎么判断:
curl -s "https://yourdomain.com/ads.txt?nocache=$(date +%s)"
查询串能强制 cache miss。如果它和缓存 URL 的内容不一样,就是 CDN 缓存陈旧——清掉它(下一节原因 5 的修复)。
6. 重定向离开了根域名,或只有一个协议在返回
Google 明确允许重定向,但有规则。单次重定向可以指向根域名之外(比如 example1.com/ads.txt → example2.com/ads.txt)。多次重定向只有在每一跳都留在原根域名内(含 www 或其它子域名)时才行。另外,爬虫会同时尝试 http:// 和 https://;如果只有一个协议返回文件,要做重定向让另一个协议也能拿到。
怎么判断:
curl -sIL "http://yourdomain.com/ads.txt" # 注意是 plain http
curl -sIL "https://yourdomain.com/ads.txt"
两条都应该最终落在一个返回你 ads.txt 的 200 上。跳两次到别的域名、或 http:// 走不通,都会验证失败。
7. AdSense 还没重爬(最常见的虚惊)
AdSense 按自己的节奏重查 ads.txt,不按需。按 Google 说法,改动需要几天反映,对广告请求很少的站可能要一个月。如果之前爬过的文件短暂返回 5xx 错误,Google 会保留上一次成功的条目最多 5 天,所以临时故障不会立刻重新触发警告。
怎么判断: curl 确认文件正确、又是最近才修的,那就是在等。别反复重发。
最短修复路径
第 1 步:从 AdSense 拿到精确文本
如果 AdSense 首页有提示,点 Fix now。否则进 Sites → 点你的站 → Ads.txt snippet → Copy。复制那一行,别手敲。
应该像:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
第 2 步:放到正确路径
| 框架 | 文件放哪 | URL |
|---|---|---|
| Astro | public/ads.txt | /ads.txt |
| Next.js(pages 或 app) | public/ads.txt | /ads.txt |
| SvelteKit | static/ads.txt | /ads.txt |
| Nuxt 3 | public/ads.txt | /ads.txt |
| Hugo | static/ads.txt | /ads.txt |
| 纯 HTML | webroot 的 ads.txt | /ads.txt |
部署,再验证。
第 3 步:用 curl 验证
curl -sIL "https://yourdomain.com/ads.txt"
# 期望:最终 HTTP/2 200, Content-Type: text/plain
curl -sL "https://yourdomain.com/ads.txt"
# 期望:google.com, pub-XXXXX, DIRECT, f08c47fec0942fa0
Content-Type 是 text/html 或其它,说明平台或 CDN 在改写。加显式规则:
Vercel — vercel.json:
{
"headers": [
{ "source": "/ads.txt", "headers": [{ "key": "Content-Type", "value": "text/plain" }] }
]
}
Netlify — _headers:
/ads.txt
Content-Type: text/plain
Cloudflare Pages — 项目根目录的 _headers:
/ads.txt
Content-Type: text/plain
第 4 步:确认 robots.txt 没拦住爬虫
Google、Mediapartners-Google 和 Google-Display-Ads-Bot 这几个爬虫都遵守 robots.txt。如果某条规则 disallow 了 /ads.txt 路径或其中某个 user agent,文件会被悄悄忽略。检查一下:
curl -s "https://yourdomain.com/robots.txt"
如果有 Disallow 规则可能命中 /ads.txt,显式加一条 allow:
User-agent: *
Allow: /ads.txt
第 5 步:清 CDN 缓存
Cloudflare → Caching → Configuration → Purge Cache → Custom Purge → 输入 https://yourdomain.com/ads.txt。Vercel 和 Netlify 通常每次部署自动清。
第 6 步:等 AdSense 重新核对
横幅不会瞬间消。只要 curl 返回的是正确的纯文本,AdSense 会在下一次抓取时识别——通常几天,低流量站可能要一个月。忍住别反复重发,那并不能加快抓取。
第 7 步:CI 加校验,别让它再悄悄回归
# .github/workflows/verify-ads-txt.yml 或 postdeploy 脚本
curl -sfL "https://yourdomain.com/ads.txt" | grep -q "pub-$ADSENSE_PUB_ID" || \
{ echo "ads.txt missing or wrong"; exit 1; }
每次部署后跑一次。文件一旦消失或被改写,部署的那一刻就能发现。
怎么确认修好了
以下全部成立才算修好:
curl -sIL https://yourdomain.com/ads.txt最终落在HTTP/2 200。Content-Type是text/plain。curl -sL https://yourdomain.com/ads.txt打印出你那行精确的pub-文本,没有 BOM 或多余字符。http://和https://都能拿到文件。
这些都通过后,AdSense 横幅纯粹是抓取延迟的残留,下一次抓取就会清掉。
容易误判的情况
把 ads.txt 放错目录(或被框架 404 掉)是静态站最常见的原因。紧随其后的是 robots.txt 里一条规则悄悄拦住了路径。用 curl 验证,别只盯着代码库——AdSense 爬的是部署出来的实际结果。
预防建议
ads.txt永远放在静态 / public 目录,每次部署自动带上。- CI 里加校验:
curl -sfL yourdomain.com/ads.txt | grep -q pub-$PUB_ID。 - 平台配置里强制
Content-Type: text/plain。 robots.txt里保持/ads.txt可访问。- 改 CDN 或 DNS 后,重新验证
ads.txt在两个协议下都正确返回。 - 接第二家广告网络(Mediavine、Ezoic、Raptive),把它们的行也加上——每个授权 seller 一行。
FAQ
- 修好后 “Earnings at risk” 横幅多久消失? 按 Google 说法,多数情况几天,广告请求很少的站可能要一个月。没有手动「立即重爬」的按钮。
- 只用 AdSense 也要 ads.txt 吗? 要。
ads.txt里没有你的 publisher ID,AdSense 会限制对你库存的需求竞价——这正是警告所指的问题。 - ads.txt 能多行吗? 能,每个授权 seller 一行。你合作的每家广告网络各占一行。
- 文件在根域名上,子域名却还报警告,为什么? 只有当子域名的授权 seller 或 publisher ID 与根域名不同时,才需要在子域名上单独放 ads.txt,并从根
ads.txt引用它;否则根域名的文件就覆盖了它。 - 用
301重定向到我的 ads.txt 行吗? 单次重定向可以离开根域名;多次重定向则要求每一跳都留在同一根域名内。跳到外部主机的多跳链会失败。 - AdSense 说「文件缺失」,但
curl能看到,怎么办? 检查robots.txt没拦住路径,确认http://和https://都能返回,然后等下一次抓取。文件正确而横幅还挂着,就是抓取延迟。