The AdSense dashboard banner says “Earnings at risk — you need to fix some ads.txt file issues to avoid severe impact to your revenue.” You added the file, but the warning won’t clear. Or https://yourdomain.com/ads.txt returns 404 even though you swear you pushed it.
Fastest fix: copy the exact line from AdSense (homepage alert → Fix now, or Sites → your site → Ads.txt snippet → Copy), drop it in your framework’s static folder so it serves at https://yourdomain.com/ads.txt with HTTP 200 and Content-Type: text/plain, deploy, and confirm with one curl. Then wait — AdSense re-crawls on its own schedule, a few days in most cases (up to a month on low-traffic sites, per Google’s docs), so a verified-correct file with a stuck banner is usually just the crawl lag, not a real problem.
The ads.txt spec itself is trivial — a plain text file at the root of the domain listing authorized sellers — but a handful of deploy traps catch most indie sites.
Which bucket are you in?
Run this first; the result tells you which section to read.
curl -sIL "https://yourdomain.com/ads.txt"
| What you see | Most likely cause | Go to |
|---|---|---|
404 / 403 | Wrong path, or framework didn’t ship the file | Causes 1, 2 |
200 but Content-Type: text/html | Platform/CDN rewriting .txt to HTML | Cause 3 |
200, text/plain, but content looks wrong | Wrong publisher ID, BOM, smart quotes | Cause 4 |
200, content correct, but only on ?nocache=... | Stale CDN cache | Cause 5 |
301/302 chain to a different host | Redirect leaves the root domain | Cause 6 |
| Everything correct, banner still up | AdSense hasn’t re-crawled yet | Cause 7 |
The -L flag follows redirects so you see the final response, not the first hop.
Common causes
Ordered by hit rate, highest first.
1. File is at the wrong path
ads.txt must resolve at https://yourdomain.com/ads.txt — not /public/ads.txt, not /assets/ads.txt, not /static/ads.txt. Google’s crawl “will start at the root domain, and the root domain needs to return from, or redirect to, the ads.txt file” (Google docs). It hits exactly one URL.
How to spot it:
curl -sI "https://yourdomain.com/ads.txt"
Look for HTTP/2 200. Anything else (404, 301 to the wrong place, 403) means it’s not resolving at the root.
2. Framework didn’t ship the file with the deploy
The file source lives in your repo’s static folder, which the build copies to the web root. Put it in src/ and it never ships.
- Astro:
public/ads.txt - Next.js (pages or app router):
public/ads.txt - SvelteKit:
static/ads.txt - Nuxt:
public/ads.txt(Nuxt 3) — older Nuxt 2 usedstatic/ - Hugo / Jekyll:
static/ads.txt/ repo root - Vercel static deploy: project root or
public/
How to spot it: after deploy, curl https://yourdomain.com/ads.txt. A 404 means the build didn’t ship it — check that the file is committed and inside the static folder, not gitignored.
3. Content-Type wrong, or you get an HTML “soft 404”
Some setups (misconfigured nginx, a catch-all CDN rewrite rule, or a SPA that returns index.html for unknown paths) serve .txt with Content-Type: text/html, or return a 200-status HTML 404 page. AdSense parses HTML, finds no seller line, and treats the file as missing.
How to spot it:
curl -sI "https://yourdomain.com/ads.txt" | grep -i content-type
It should report Content-Type: text/plain (often text/plain; charset=utf-8). If it says text/html, your platform is rewriting or your SPA fallback is swallowing the request.
4. Wrong publisher ID or invisible characters
The line must be exactly:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
Four comma-separated fields: the ad system domain (google.com), your publisher ID, the relationship (DIRECT), and Google’s certification-authority ID (f08c47fec0942fa0, the same for every AdSense publisher). Common breakers: trailing whitespace, smart quotes from a copy-paste, a BOM at the start of the file, the wrong pub- number, or a missing comma.
How to spot it:
curl -s "https://yourdomain.com/ads.txt" | od -c | head -3
Watch for unexpected bytes — a UTF-8 BOM shows up as 357 273 277 at the very start of the file. If it’s there, re-save the file as UTF-8 without BOM.
5. Cloudflare or CDN cache is serving the old version
You fixed the file, but a cached copy still wins. AdSense crawls the cached version and sees the old (missing) state.
How to spot it:
curl -s "https://yourdomain.com/ads.txt?nocache=$(date +%s)"
The query string forces a cache miss. If the content differs from the cached URL, your CDN cache is stale — purge it (Cause 5 fix in the next section).
6. Redirect leaves the root domain, or only one protocol serves
Google explicitly allows redirects, but with rules. A single redirect can point off the root domain (e.g. example1.com/ads.txt → example2.com/ads.txt). Multiple redirects are fine only if every hop stays within the original root domain (including www or other subdomains). Also, the crawler tries both http:// and https://; if only one protocol serves the file, set up a redirect so the other one reaches it too.
How to spot it:
curl -sIL "http://yourdomain.com/ads.txt" # note: plain http
curl -sIL "https://yourdomain.com/ads.txt"
Both should end at a 200 returning your ads.txt. A redirect that bounces twice to another domain, or an http:// that dead-ends, will fail verification.
7. AdSense hasn’t re-crawled yet (most common false alarm)
AdSense re-checks ads.txt on its own schedule, not on demand. Per Google, changes take a few days to reflect, and up to a month on sites that make few ad requests. If a previously crawled file briefly returns a 5xx error, Google keeps the last-good entries for up to 5 days, so transient outages don’t immediately re-trigger the warning.
How to spot it: if curl confirms the file is correct and you fixed it recently, you’re just waiting. Don’t keep redeploying.
Shortest path to fix
Step 1: Get the exact text from AdSense
If there’s an alert on your AdSense homepage, click Fix now. Otherwise go to Sites → click your site → Ads.txt snippet → Copy. Copy the line; don’t retype it.
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 3 | public/ads.txt | /ads.txt |
| Hugo | static/ads.txt | /ads.txt |
| Plain HTML | ads.txt in webroot | /ads.txt |
Deploy, then verify.
Step 3: Verify with curl
curl -sIL "https://yourdomain.com/ads.txt"
# Expect: final HTTP/2 200, Content-Type: text/plain
curl -sL "https://yourdomain.com/ads.txt"
# Expect: google.com, pub-XXXXX, DIRECT, f08c47fec0942fa0
If Content-Type is text/html or anything else, your platform or 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 the project root:
/ads.txt
Content-Type: text/plain
Step 4: Make sure robots.txt isn’t blocking the crawler
The Google, Mediapartners-Google, and Google-Display-Ads-Bot crawlers honor robots.txt. If a rule disallows the /ads.txt path or one of those user agents, the file is silently ignored. Check it:
curl -s "https://yourdomain.com/robots.txt"
If a Disallow rule could catch /ads.txt, add an explicit allow:
User-agent: *
Allow: /ads.txt
Step 5: Flush CDN cache
Cloudflare → Caching → Configuration → Purge Cache → Custom Purge → enter https://yourdomain.com/ads.txt. Vercel and Netlify usually self-purge on each deploy.
Step 6: Wait for the AdSense recheck
The banner doesn’t disappear instantly. As long as curl returns the correct content over plain text, AdSense will pick it up on its next crawl — a few days typically, up to a month on low-traffic sites. Resist the urge to redeploy repeatedly; it doesn’t speed up the crawl.
Step 7: Add a CI check so it never silently regresses
# .github/workflows/verify-ads-txt.yml or a postdeploy script
curl -sfL "https://yourdomain.com/ads.txt" | grep -q "pub-$ADSENSE_PUB_ID" || \
{ echo "ads.txt missing or wrong"; exit 1; }
Run it after every deploy. It catches a vanished or rewritten file the moment it ships.
How to confirm it’s fixed
You’ve fixed it when all of these are true:
curl -sIL https://yourdomain.com/ads.txtends atHTTP/2 200.- The
Content-Typeistext/plain. curl -sL https://yourdomain.com/ads.txtprints your exactpub-line with no BOM or stray characters.- Both
http://andhttps://reach the file.
Once those pass, the AdSense banner is purely a crawl-lag artifact. It will clear on the next crawl.
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. A close second is a robots.txt rule quietly blocking the path. Verify with curl, not by eyeballing your codebase — the deployed reality is what AdSense crawls.
Prevention
- Always keep
ads.txtin the static / public folder so it ships with every deploy automatically. - Validate in CI:
curl -sfL yourdomain.com/ads.txt | grep -q pub-$PUB_ID. - Force
Content-Type: text/plainvia platform config. - Keep
/ads.txtallowed inrobots.txt. - After any CDN or DNS change, re-verify
ads.txtstill serves correctly on both protocols. - If you add a second ad network (Mediavine, Ezoic, Raptive), append their lines — one authorized seller per line.
FAQ
- How long until the “Earnings at risk” banner clears after I fix it? Per Google, a few days in most cases, and up to a month for sites that make few ad requests. There’s no manual “re-crawl now” button.
- Do I need ads.txt if I only use AdSense? Yes. Without your publisher ID in
ads.txt, AdSense restricts demand for your inventory, which is what the warning is about. - Can ads.txt have multiple lines? Yes — one line per authorized seller. Each ad network you work with gets its own line.
- My file is on the root domain but a subdomain still warns. Why? Crawl ads.txt on a subdomain only if its authorized seller or publisher ID differs from the root. Reference the subdomain file from the root
ads.txt; otherwise the root file covers it. - Does a
301redirect to my ads.txt work? A single redirect off your root domain is allowed, or multiple redirects as long as every hop stays within the same root domain. A multi-hop chain to a foreign host will fail. - AdSense says “file missing” but
curlshows it. What now? Checkrobots.txtisn’t blocking the path, confirm bothhttp://andhttps://serve it, then wait for the next crawl. A correct file plus a stuck banner is the crawl lag.