The AdSense dashboard banner says “Earnings at risk — your ads.txt file is missing.” You added the file. The warning didn’t go away. Or https://yourdomain.com/ads.txt returns 404 even though you swear you pushed it. The ads.txt spec is simple — a plain text file at the root of the domain listing authorized sellers — but four deploy traps catch most indie sites.
This walks through where ads.txt actually needs to live, how to verify it, and the AdSense reconciliation lag that makes the banner stick around even after a correct fix.
Common causes
Ordered by hit rate, highest first.
1. File is at the wrong path
ads.txt must be at https://yourdomain.com/ads.txt — not /public/ads.txt, not /assets/ads.txt, not /static/ads.txt. The crawler hits exactly one URL.
How to spot it:
curl -sI "https://yourdomain.com/ads.txt"
Look for HTTP/2 200. Anything else (404, 301, 403) means it’s not at the right path.
2. Framework didn’t ship the file with the deploy
Astro: ads.txt goes in public/. Next.js: also public/. Vercel: in public/ for Next, root for static. SvelteKit: static/. If you put it in src/, it’s not deployed.
How to spot it: After deploy, curl https://yourdomain.com/ads.txt. If 404, your build didn’t ship it.
3. Content-Type wrong or content is gzipped HTML
Some platforms (poorly configured nginx, certain CDN rules) serve .txt files with Content-Type: text/html or wrap them in a 404 HTML page that returns 200. AdSense sees HTML, not the seller line, and treats it as missing.
How to spot it:
curl -sI "https://yourdomain.com/ads.txt" | grep -i content-type
Should be Content-Type: text/plain (or text/plain; charset=utf-8). If text/html, the platform is rewriting.
4. Wrong publisher ID or formatting
The line must be exactly:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
Common errors: trailing whitespace, smart quotes from copy-paste, BOM character at start of file, wrong publisher number, missing comma.
How to spot it:
curl -s "https://yourdomain.com/ads.txt" | od -c | head -3
Look for unexpected characters (BOM is 357 273 277 at start).
5. Cloudflare or CDN cache shows old version
You fixed it but a cached version is still being served. AdSense crawls the cached version and sees the old (missing) state.
How to spot it: Add ?nocache=$(date +%s) to bypass cache: curl "https://yourdomain.com/ads.txt?nocache=$(date +%s)". If contents differ, CDN cache is stale.
6. AdSense hasn’t re-crawled yet
The banner persists for 24-48 hours after fix because AdSense rechecks on its own schedule.
How to spot it: If your ads.txt is verified-correct via curl and you fixed < 48h ago, you’re just waiting.
Shortest path to fix
Step 1: Get the exact text from AdSense
AdSense → Sites → your-site → “Get snippet.” Copy the line — don’t retype it.
Should look like:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
Step 2: Put it at the right path
| Framework | File goes in | URL it serves at |
|---|---|---|
| Astro | public/ads.txt | /ads.txt |
| Next.js (pages or app) | public/ads.txt | /ads.txt |
| SvelteKit | static/ads.txt | /ads.txt |
| Nuxt | static/ads.txt | /ads.txt |
| Hugo | static/ads.txt | /ads.txt |
| Plain HTML | ads.txt in webroot | /ads.txt |
Deploy. Verify.
Step 3: Verify with curl
curl -sI "https://yourdomain.com/ads.txt"
# Expect: HTTP/2 200, Content-Type: text/plain
curl -s "https://yourdomain.com/ads.txt"
# Expect: google.com, pub-XXXXX, DIRECT, f08c47fec0942fa0
If Content-Type is text/html or anything else, your platform / CDN is rewriting. Add an explicit rule:
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 file at root:
/ads.txt
Content-Type: text/plain
Step 4: Flush CDN cache
Cloudflare → Caching → Purge → Custom Purge → enter https://yourdomain.com/ads.txt.
Vercel: usually self-flushes on deploy.
Step 5: Wait 24-48h for AdSense recheck
The banner doesn’t disappear instantly. As long as curl https://yourdomain.com/ads.txt returns the correct content over plain text, AdSense will pick it up within 48 hours.
Step 6: Add a CI check
# .github/workflows/verify-ads-txt.yml or a postdeploy script
curl -sf "https://yourdomain.com/ads.txt" | grep -q "pub-$ADSENSE_PUB_ID" || \
{ echo "ads.txt missing or wrong"; exit 1; }
Run this after every deploy. It catches regressions instantly.
When this is not on you
AdSense rechecks ads.txt periodically, not on demand. The dashboard banner may take 24-48 hours to clear after a verified fix. Don’t keep redeploying — just wait.
Easy to misdiagnose as
Placing ads.txt in the wrong directory (or letting the framework 404 it) is the single most common cause on static sites. Verify with curl, not by looking at your codebase.
Prevention
- Always put
ads.txtin the static / public folder so it ships with every deploy automatically. - Validate in CI:
curl -sf yourdomain.com/ads.txt | grep -q pub-$PUB_ID. - Force
Content-Type: text/plainvia platform config. - When changing CDN settings, re-verify
ads.txtis still served correctly. - If you ever add a second ad network (Mediavine, Ezoic), update
ads.txtto include their lines too.
FAQ
- Do I need ads.txt if I only use AdSense? Yes — Google requires it for full AdSense earnings.
- Can ads.txt have multiple lines? Yes, one line per authorized seller. Multiple ad networks each get their own line.