你把 ads.txt 加进了仓库、部署完毕,浏览器里访问 https://your-domain.com/ads.txt 也能看到预期的 google.com, pub-XXX, DIRECT, ... 那一行。几个小时后,AdSense → 网站 依然把这个域名标成 “ads.txt 状态:未找到” 或 “收入面临风险”。你又重新部署。又等了一天。同样的警告。罪魁祸首基本都是重定向:你的托管平台把 http:// 301 到了 https://,或者剥掉了 www,或者规范化了路径,而 AdSense 的爬虫对 ads.txt 拒绝跟任何一种跳转。IAB 规范非常严格——ads.txt 必须在规范根 URL 上直接返回 HTTP 200,不允许任何 3xx 跳。
常见原因
按触发失败的频次排序。
1. 主域和 www 互相重定向
你提供服务的是 https://www.example.com,但 AdSense 抓的是 https://example.com/ads.txt,它 301 跳到 www。爬虫不跟 ads.txt 的重定向,记成”缺失”。
怎么判断:跑 curl -I https://example.com/ads.txt 和 curl -I https://www.example.com/ads.txt。其中一个对另一个返回 301/302,就是这条。
2. HTTP 到 HTTPS 的强制重定向命中 ads.txt 路径
你的站强制走 TLS,把 http://example.com/ads.txt 301 到 https://example.com/ads.txt。AdSense 可能先试 HTTP,然后拒绝跳转。
怎么判断:curl -I http://example.com/ads.txt 返回 301 Location: https://...。大多数现代主机默认这么做,通常没问题,但部分 AdSense 爬虫路径会因此失败。
3. 大小写或斜杠归一化导致的重定向
Netlify、Vercel、Cloudflare Pages 等主机有时会把 /ads.txt/ 301 到 /ads.txt,或者把大写路径规范化。如果 AdSense 恰好用了一个奇怪路径来探测,就吃到重定向。
怎么判断:curl -I https://example.com/Ads.txt(大写 A)和 curl -I https://example.com/ads.txt/(尾斜杠)。任何一个返回 301/302,平台就在做归一化。
4. CDN 层把”爬虫”重定向到验证码或拦截页
bot 防护规则(Cloudflare、AWS WAF、Akamai)会把爬虫送进验证码或过渡页。AdSense 爬虫触发规则,拿到一个 302 跳到 /captcha,文件就被报成缺失。
怎么判断:curl -I -A "Mediapartners-Google" https://example.com/ads.txt。如果响应里有 cf-mitigated、x-firewall-action,或者 302 到一个挑战页,就是 WAF 干的。
5. SPA 框架把未知路径全部拦截了
单页应用通常会配一个 catch-all,把任意未知路径都返回 index.html。如果你的构建产物里没有 ads.txt,SPA 返回 200,但内容是 HTML——AdSense 一解析,认不出有效记录,报”格式错误”。
怎么判断:curl https://example.com/ads.txt | head -3。看到 <!DOCTYPE html> 而不是 google.com, pub-XXX, ...,就是 SPA 吞掉了路径。
6. CDN 缓存了之前文件不存在时的 404
你的 CDN 在你加 ads.txt 之前缓存了一个 404。即使部署完成,边缘节点仍然几小时内一直返回这个过期的 404。
怎么判断:curl -I 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 -IL https://example.com/ads.txt的输出(-L会跟着重定向把每一跳都打印出来)。- 同样的命令对 www 变体跑一遍的结果。
curl -I -A "Mediapartners-Google" https://example.com/ads.txt(模拟 AdSense 的 UA)。- 托管平台名(Vercel、Netlify、Cloudflare Pages、S3、自建 Nginx 等)。
- 当前
ads.txt内容(前 5 行)。 - AdSense → 网站 → 你的域名 那条警告的准确文字。
一步步修复
从最便宜到最重的顺序。
第 1 步:确认规范 URL 直接返回 200
AdSense 爬虫探的就是你注册时填的 https://<your-domain>/ads.txt。先确认主域和 www 都返回 200:
curl -IL https://example.com/ads.txt
curl -IL https://www.example.com/ads.txt
任何一边在跟踪中出现 301 或 302,那一边就是有问题的。修法取决于哪边是规范域。
第 2 步:如果主域和 www 互相跳,两边都放 ads.txt
爬虫可能从任一边探测。最简单的修法是在 example.com/ads.txt 和 www.example.com/ads.txt 都放一份真实的 ads.txt,绕过重定向。
Nginx:
server {
server_name example.com;
# 正常情况下其他全部跳到 www……
location / {
return 301 https://www.example.com$request_uri;
}
# ……但 ads.txt 例外
location = /ads.txt {
alias /var/www/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 Workers 给 /ads.txt 加一条例外规则,直接返回文件。
第 3 步:关掉路径归一化重定向
如果你的主机会把大小写或尾斜杠 301,给 ads.txt 加例外。Netlify 的 _redirects:
# 除 ads.txt 外其他都加尾斜杠
/ads.txt /ads.txt 200
/* /:splat/ 301
200 状态码保证它是直接返回,而不是跳转。
第 4 步:在 WAF 里给 AdSense 爬虫放行
如果防 bot 规则在拦,放行 AdSense 的几个 UA:
Mediapartners-GoogleAdsBot-GoogleGooglebot(ads.txt 验证有时也用它)
Cloudflare → Security → WAF → Tools → Allowlist,加一条自定义规则:
(http.request.uri.path eq "/ads.txt") or (http.user_agent contains "Mediapartners-Google")
动作选 “Skip” → 勾上 “Bot Fight Mode”、“Managed Rules”、“Rate Limiting”。
第 5 步:把 ads.txt 放成 SPA 不会拦截的静态文件
Next.js:把 ads.txt 放进 public/:
public/
ads.txt
Next 会直接服务 public/ 里的文件,不会走 React 应用。线上确认:
curl https://example.com/ads.txt
# 预期: google.com, pub-XXX, DIRECT, f08c47fec0942fa0
Astro 也是放 public/。Gatsby 同理。纯 SPA(Vite、CRA)也必须放 public/。
服务端渲染框架要确保路由不匹配 /ads.txt:
// Express 示例
app.get("/ads.txt", (req, res) => {
res.type("text/plain");
res.sendFile(path.join(__dirname, "ads.txt"));
});
第 6 步:清掉 CDN 缓存里这条路径
修完之后,残留缓存还会让警告继续在:
# 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 重新抓取
上面这些都做完之后,AdSense 在 24-48 小时内会重新抓 ads.txt。别一直重新部署,给它一天时间。在 AdSense → 网站 → 你的域名 看状态。
验证
curl -IL https://<canonical-domain>/ads.txt全程只有一个200 OK,没有301或302。- 文件正文是纯文本,以
google.com, pub-...开头,不是 HTML。 - 48 小时内 AdSense → 网站 → 你的域名 状态变成 “ads.txt status: Authorized”。
curl -A "Mediapartners-Google" https://<your-domain>/ads.txt也返回 200 和同样的纯文本,证明爬虫 UA 没被拦。
长期防止
- 全站只用一个规范域名(主域或 www,二选一),AdSense 从一开始就按这个配。
- ads.txt 放仓库版本管理,别用主机控制台的网页编辑器改。
- 任何平台或 CDN 迁移之后,把上面那几条 curl 检查加进切换 checklist。
- 加一个合成监控(UptimeRobot、Pingdom)每天打
/ads.txt,非 200 或 content-type 错就报警。 - 在团队 runbook 里写清楚规范域名是哪个——很多重定向坑都是 DNS 改动时埋下的。
- 如果用了会改 HTTP 头的 CMP,确认它不动
/ads.txt的响应。
常见坑
- 用 CMS 自带文件编辑器改 ads.txt,文件开头被加了 UTF-8 BOM——这一个字节让 AdSense 解析整行失败。
- 给响应加了
Content-Type: text/html——AdSense 期望text/plain(或者任意文本类型;HTML 浏览器能看但会触发”文件类型不对”信号)。 - 把 ads.txt 放在
/static/ads.txt之类的子目录,再把/ads.txtrewrite 到那儿——rewrite 对爬虫不可见,但部分 rewrite 日志会被记成 redirect。 - 只在 AdSense 网站里登记了主域和 www 中的一个,而规范重定向却朝另一边走——把两边都登记上就行。
- 改完重定向忘了清 CDN 缓存,然后误以为”修复没生效”,其实只是缓存还没过期。
FAQ
Q:curl 显示是 200,AdSense 还说”未找到”,怎么办?
试 curl -A "Mediapartners-Google" -I <url>——如果返回不一样(302、403 等),说明 WAF 对爬虫做了区别对待。按第 4 步放行 UA。这个问题不解决,下游症状见 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 才会重检?
通常 24-72 小时。状态字段是异步更新的。这段时间别在面板里禁用/重启广告单元,那只会增加噪音,不会加快重检。
Q:出于 SEO 必须做重定向,真的只能两边都放 ads.txt 吗?
对于 ads.txt 来说就是这样。多放一份(主域一份、www 一份)成本接近 0,能解锁验证。SEO 规范化靠 <link rel="canonical"> 和 Search Console 设置来实现,不需要靠重定向 ads.txt 路径。