你把 ads.txt 加好、部署完毕,浏览器里访问 https://your-domain.com/ads.txt 也能看到预期的 google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0 那一行。几个小时后,AdSense 仍然显示 “Earnings at risk”,ads.txt 状态:Not found。你重新部署、等了一天,警告纹丝不动。
最快的修法: 跑 curl -ILk https://example.com/ads.txt(主域),确认整条跟踪最终落在一个 HTTP/2 200 上,content-type: text/plain,正文以 google.com, pub-... 开头。如果你看到两条或以上 3xx、一条先离开本域再二次跳回的重定向、HTML 正文,或者 404,那就是断点所在。把跳转压成最多一跳,且终点是真正的 200 纯文本文件。
多数教程在这里搞错了一点:Google 的爬虫是会跟 ads.txt 重定向的。按 Google 官方《Ensure your ads.txt files can be crawled》文档(截至 2026 年 6 月仍有效),抓取从根域开始,根域必须返回文件或重定向到文件,而且只跟一次离开原始根域的重定向。第二次跳转——哪怕是跳回同一个域——抓取就会停止。爬虫同时会试 HTTP 和 HTTPS。所以真正的故障是跳转链、永远落不到 200 的跳转、返回 HTML 而非纯文本、robots.txt 挡了路径,或者一个过期的 404 缓存——而不是”重定向”本身。
你看到的是哪种状态?
AdSense 会报三种状态之一。动手改配置前,先对上号。
| AdSense 里的状态 | 含义 | 该看哪里 |
|---|---|---|
Authorized | 文件已找到、已解析,你的 pub-XXXX ID 匹配 | 无需处理 |
Unauthorized | 文件找到了也读得出,但你的发布商 ID 不在里面 | 改文件内容(pub- 行写错或缺失),与重定向无关 |
Not found | 爬虫取不到有效文件(404、跳转链、HTML 正文、robots 拦截、超时) | 本文 |
如果你是 Unauthorized,问题就不在重定向——从 AdSense 复制那一行,把正确的 pub- ID 粘进文件即可。本文剩下的内容是针对 Not found 的。
常见原因
按触发 Not found 的频次排序。
1. 跳转链(两跳或以上)
一跳没事,两跳就不行。常见模式:http://example.com/ads.txt → https://example.com/ads.txt → https://www.example.com/ads.txt。这是两跳,如果第一跳已经离开了根域、或者触发了第二次重定向,抓取就停。
怎么判断:curl -ILk http://example.com/ads.txt,数 location: 行。在 200 之前出现不止一次重定向,就是断点。
2. 主域和 www 拆开,但重定向方向反了
Google 的规则:www.example.com/ads.txt 只有在 example.com/ads.txt 重定向到它 时才会被抓。如果你从 www 提供服务,但主域返回 404(而不是重定向到 www),爬虫就永远到不了文件。
怎么判断:curl -Ik https://example.com/ads.txt。如果主域返回 404,而 https://www.example.com/ads.txt 返回 200,就加一条主域→www 的重定向(一跳),让抓取能顺着链进到文件。
3. robots.txt 挡了 ads.txt 路径
按 Google 文档,如果 robots.txt 禁止了 ads.txt 所在的 URL 路径,爬虫就会忽略 ads.txt。一条宽泛的 Disallow: / 或一条会命中 /ads.txt 的规则,会在文件浏览器里返回 200 的情况下悄悄挡掉验证。
怎么判断:curl https://example.com/robots.txt,看有没有任何 Disallow: 命中 /ads.txt。加一条明确放行即可:
User-agent: *
Allow: /ads.txt
4. SPA 框架对该路径返回 HTML(软 404)
单页应用通常配一个 catch-all,把任意未知路径返回 index.html。如果你的构建产物里没有 ads.txt,该路径返回 200,但内容是 HTML。爬虫读正文、找不到有效记录,就报文件未找到 / 格式错误。
怎么判断:curl https://example.com/ads.txt | head -3。看到 <!DOCTYPE html> 而不是 google.com, pub-...,就是 SPA 吞掉了路径。
5. CDN / WAF 把”爬虫” UA 重定向走
bot 防护规则(Cloudflare、AWS WAF、Akamai)会把爬虫送进验证码或过渡页。AdSense 爬虫触发规则,拿到一个 302 跳到 /challenge,文件就读成缺失。
怎么判断:curl -Ik -A "Mediapartners-Google" https://example.com/ads.txt。响应里出现 cf-mitigated 头、x-firewall-action、403,或者 302 到挑战页,就是 WAF 干的。
6. CDN 缓存了文件存在之前的 404
你的 CDN 在你部署 ads.txt 之前缓存了一个 404,然后几小时内一直从边缘节点返回它。
怎么判断:curl -Ik https://example.com/ads.txt 显示 cf-cache-status: HIT 加 404,而直连源站返回 200。
开始前先确认
- 确认你的规范域名——主域(
example.com)还是 www(www.example.com)。把这个准确的主机登记进 AdSense。 - 知道 DNS 服务商、托管平台、CDN 链路分别是什么。
- 准备好发布商 ID:
pub-XXXXXXXXXXXXXXXX(AdSense → 账号 → 账号信息 里看)。
需要收集的信息
curl -ILk https://example.com/ads.txt——-L会跟着重定向把每一跳都打印出来,-k避免 http→https 那一跳的证书噪音。- 同样的命令对 www 变体跑一遍。
curl -Ik -A "Mediapartners-Google" https://example.com/ads.txt——模拟 AdSense 内容爬虫的 UA。curl https://example.com/robots.txt——确认/ads.txt没被 disallow。- 托管平台名(Vercel、Netlify、Cloudflare Pages、S3、自建 Nginx 等)。
- 当前
ads.txt内容(前 5 行)。 - AdSense → 网站 → 你的域名 那条警告的准确文字。
一步步修复
从最便宜到最重的顺序。
第 1 步:把整条跳转链跟踪出来
curl -ILk https://example.com/ads.txt
curl -ILk https://www.example.com/ads.txt
数 location: 行。通过条件是:最多一次重定向,且最终响应是 HTTP/2 200、正文为纯文本。两跳或以上、或者终点不是 200,就是故障。
第 2 步:把链压成一跳干净跳转,且终点是 200
如果你做主域→www 重定向,那一跳是允许的——只要确保主域不会同时走 http→https→www(那就成两跳了)。把 TLS 终结和主机名归一化都放在边缘完成,让一条 301 直接落到 www 上的文件。
干净、对爬虫友好的配置是:选定一个规范主机,另一个用恰好一条 301 跳到它,并保证文件在终点返回 200。在 Nginx 上,把文件直接放在规范主机上,让非规范主机做一条单跳重定向过去:
# 规范主机直接提供真实文件
server {
server_name www.example.com;
location = /ads.txt { alias /var/www/ads.txt; default_type text/plain; }
}
# 主域只做一条到规范主机的重定向
server {
server_name example.com;
location / { return 301 https://www.example.com$request_uri; }
}
如果你没法保证只有一跳(例如 CDN 在你的跳转之前先插了一条它自己的 http→https 跳转),最稳的变通办法是在两个主机上都直接返回 200 的 ads.txt,这样这条路径就不需要任何重定向。Vercel 在 vercel.json 里:
{
"redirects": [
{ "source": "/(.*)", "destination": "https://www.example.com/$1", "permanent": true, "missing": [{ "type": "header", "key": "x-skip-redirect" }] }
],
"rewrites": [
{ "source": "/ads.txt", "destination": "/ads.txt" }
]
}
Cloudflare 上加一条 Configuration Rule(或 Worker),把 /ads.txt 路径从所有重定向里豁免出来,直接返回文件。
第 3 步:关掉 ads.txt 路径上的归一化重定向
如果你的主机会把大小写或尾斜杠 301,给 ads.txt 加例外。Netlify 的 _redirects:
# 在任何 catch-all 之前,直接返回 ads.txt(状态 200,不跳转)
/ads.txt /ads.txt 200
/* /:splat/ 301
200 状态码保证它是直接返回,而不是跳转。顺序很重要——ads.txt 那行必须在最前面。
第 4 步:在 robots.txt 和 WAF 里放行该路径
确认 robots.txt 放行 /ads.txt:
User-agent: *
Allow: /ads.txt
如果防 bot 规则在拦,放行 AdSense 的几个爬虫 UA(截至 2026 年 6 月):
Mediapartners-Google——AdSense 内容爬虫。Google-Display-Ads-Bot——你把站点加进 AdSense 时用来验证站点的爬虫。AdsBot-Google——Google Ads 广告质量爬虫,抓 ads.txt 时也会见到。
Cloudflare → Security → WAF → 自定义规则,加一条 Skip 规则:
(http.request.uri.path eq "/ads.txt") or (http.user_agent contains "Mediapartners-Google")
动作选 Skip,并勾上 “All managed rules”、“Bot Fight Mode”、“Rate Limiting”,让它们都不再重新拦这条路径。
第 5 步:把 ads.txt 放成 SPA 不会拦截的静态文件
Next.js、Astro、Gatsby:把 ads.txt 放进 public/:
public/
ads.txt
这些框架会直接服务 public/ 里的文件,不走应用本体。线上确认:
curl https://example.com/ads.txt
# 预期: google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
纯 SPA(Vite、CRA)也必须放 public/。服务端渲染框架则要确保路由在任何 catch-all 之前匹配 /ads.txt:
// Express 示例
app.get("/ads.txt", (req, res) => {
res.type("text/plain");
res.sendFile(path.join(__dirname, "ads.txt"));
});
第 6 步:清掉 CDN 缓存里这条路径
即便修好了,一个过期的 404 缓存仍会让警告挂着:
# Cloudflare
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://example.com/ads.txt","https://www.example.com/ads.txt"]}'
Vercel / Netlify 重新部署会自动清。S3 + CloudFront 要在 CloudFront 控制台里 invalidate /ads.txt。
第 7 步:等重新抓取
文件可达之后,AdSense 会重新抓取并清掉警告。Google 文档说改动”可能要几天”,对几乎不发广告请求的低流量站点最长可达一个月。别一直重新部署——至少给它几天。在 AdSense → 网站 → 你的域名 看状态。
怎么确认修好了
curl -ILk https://<your-canonical-domain>/ads.txt最多一个3xx,最终落在单个200 OK。- 最终响应的
content-type是text/plain(不是text/html)。 - 正文是纯文本,以
google.com, pub-...开头,不是<!DOCTYPE html>。 curl -Ik -A "Mediapartners-Google" https://<your-domain>/ads.txt返回同样的200和纯文本——证明爬虫 UA 没被拦或被区别重定向。curl https://<your-domain>/robots.txt没有任何Disallow:命中/ads.txt。- AdSense → 网站 → 你的域名 在几天内翻成
Authorized。
长期防止
- 全站只用一个规范域名(主域或 www,二选一),从一开始就把这个准确主机登记进 AdSense。
- 重定向保持一跳。如果 CDN 自己加了一条 http→https 跳转,就在你自己的主机重定向之前先终结 TLS,避免叠加跳数。
- ads.txt 放版本管理,别用主机控制台的网页编辑器改(那些编辑器可能加 BOM)。
- 任何平台 / CDN / DNS 迁移之后,把上面那几条 curl 检查加进切换 checklist。
- 加一个合成监控(UptimeRobot、Pingdom)每天打
/ads.txt,非 200 或 content-type 不是text/plain就报警。 - 如果用了会改 HTTP 头的 CMP 或边缘中间件,确认它不动
/ads.txt。
常见坑
- 文件开头有 UTF-8 BOM(某些 CMS 编辑器会加)——这个字节让 AdSense 把第一行读成格式错误。
- 响应是
Content-Type: text/html——AdSense 期望text/plain。HTML 浏览器能正常渲染,但会触发”文件类型不对”的信号。 - 把 ads.txt 放在
/static/ads.txt之类的子目录,再把/ads.txtrewrite 过去——部分主机会把这个内部 rewrite 记成 redirect,多算一跳。 - 只在 AdSense 里登记了主域 / www 中的一个,而你那条单跳重定向却朝另一边走——登记实际返回 200 的那个主机,或者主域重定向到达的那个。
- 误以为”修复没生效”,其实只是 CDN 缓存还没过期——修完一定要清缓存。
FAQ
Q:浏览器能正常打开 ads.txt——为什么 AdSense 还说 Not found?
浏览器会乖乖跟多跳重定向、也会渲染 HTML;爬虫不会。跑 curl -ILk,检查有没有两跳或以上、终点非 200、或者 HTML 正文。这些在浏览器里都过得了,却过不了爬虫。
Q:Google 真的对 ads.txt 拒绝一切重定向吗?
不是。Google 会跟一次离开根域的重定向(主域→www 这条跳转本来就是 www-规范站点该有的样子)。出问题的是链——第二次跳转,哪怕跳回同一个域,抓取就停。见 Google 抓取文档。
Q:AdSense 说的是 Unauthorized,不是 Not found——同样的修法吗?
不一样。Unauthorized 意味着文件读到了,但你的 pub-XXXX ID 不在里面。从 AdSense → 网站 复制那一行,把发布商 ID 粘进文件即可,这种情况和重定向无关。
Q:curl 显示是单个 200,AdSense 还说 Not found,怎么办?
试 curl -Ik -A "Mediapartners-Google" <url>。如果返回不一样(302、403),说明 WAF 对爬虫做了区别对待——按第 4 步放行 UA。也检查 robots.txt 有没有对 /ads.txt 的 Disallow。下游症状见 AdSense 广告不显示。
Q:每个子域都要 ads.txt 吗?
只有你在 AdSense 网站里登记过的那个主机需要。你登记的是 www.example.com,文件就必须能在 www.example.com/ads.txt 取到。blog.example.com 之类的子域只有在它自己也注册了 AdSense 才需要独立的 ads.txt。
Q:可以用 sellers.json 替代吗?
不行。sellers.json 是 SSP / 交易所发布的,不是发布商发布的。AdSense 的发布商验证需要 ads.txt。两者可以共存,但 ads.txt 是必需的那个。“文件根本不在”的更简单情形见 ads.txt 未找到。
Q:修完多久 AdSense 才会重检?
Google 说几天;低流量站点最长可到一个月。状态字段是异步更新的。这段时间别在面板里禁用 / 重启广告单元,那只会增加噪音,不会加快重检。