Skip to content
Approvalens

Reading room · 5 min read

ads.txt on Cloudflare Pages and Workers: Setup and the SPA Trap

How to serve ads.txt from Cloudflare Pages or Workers static assets, why a missing file can return your home page with status 200, and how to test it.

By the Approvalens team

Fixes these report findings

  • ads.txt file
  • ads.txt content type
  • Google line in ads.txt
  • ads.txt redirect
  • Google crawlers can open the site

On Cloudflare Pages and on Workers with static assets, ads.txt is just a file in the folder you deploy. Put ads.txt in the build output (for most frameworks, the public/ or static/ folder that gets copied there), deploy, and it's served at /ads.txt. The Cloudflare-specific part is what happens when the file isn't found. In single-page-application mode Cloudflare answers with your index.html and status 200, so AdSense's crawler reads an HTML page where it expected a list of sellers.

Decision flow for a request to /ads.txt on Cloudflare: if a Pages Function or a Worker with run_worker_first matches, code answers; otherwise a matching static file is served as text/plain; if there is no file, SPA mode returns index.html with 200 and 404-page mode returns 404
How Cloudflare decides what /ads.txt returns. Only the green path gives Google a real ads.txt.

The line you need

google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0

Use your own 16-digit ID with pub- (not ca-pub-). Add lines from any other ad networks below it. Formatting rules are in the ads.txt setup guide.

Cloudflare Pages

  1. Put ads.txt in the folder your framework copies into the build output: public/ for Vite, Next.js, Astro and Create React App, static/ for Hugo and SvelteKit. For a plain HTML site, put it next to index.html.
  2. Deploy, then open https://yourproject.pages.dev/ads.txt to confirm the file is in the build.
  3. Check the same path on your custom domain.

Why a missing file returns your home page

Cloudflare's docs: "If your project does not include a top-level 404.html file, Pages assumes that you are deploying a single-page application", and "Pages' default single-page application behavior matches all incoming paths to the root (/)" (serving Pages).

So when ads.txt didn't make it into the build, /ads.txt doesn't return 404. It returns your home page with status 200. Google treats a "real page returned for a URL that doesn't actually exist" as a soft 404 (crawl rules), and a human glancing at the browser may think the file exists. Two defences:

  • add a 404.html to the build output, so missing paths return a real 404;
  • after every deploy, look at the content type and the body, not only the status code (see the checks below).

Pages Functions

A matched Function takes priority. "If no Function is matched, it will fall back to a static asset if there is one" (Functions routing). A catch-all such as functions/[[path]].js therefore also answers /ads.txt. Exclude it in _routes.json, which lives in the build directory:

{
  "version": 1,
  "include": ["/*"],
  "exclude": ["/ads.txt", "/robots.txt"]
}

"Exclude always take priority over include", so the static file is served.

Content type with _headers

Pages sends a Content-Type for every static file. If the type for /ads.txt is wrong, override it in a _headers file in the build output. Headers defined there "override what Cloudflare ordinarily sends" (headers):

/ads.txt
  Content-Type: text/plain; charset=utf-8

Don't use _redirects for www or apex problems. Cloudflare's own table marks "Domain-level redirects" as not supported there (redirects).

Workers with static assets

With Workers, the assets folder is set in the Wrangler config ("assets": { "directory": "./public/" }). Two settings decide what /ads.txt returns (static assets, binding options):

Setting What happens on /ads.txt
run_worker_first false (default) and the file exists Cloudflare serves the file. The MIME type comes from the file extension
not_found_handling = "single-page-application" and the file is missing "a 200 OK response with index.html". This is the trap
not_found_handling = "404-page" and the file is missing The nearest 404.html with status 404
run_worker_first = true Your Worker runs on every request. It must hand /ads.txt to the assets binding or answer it itself

run_worker_first also accepts a list of path patterns, including ! exclusions. That lets you run the Worker on /api/* only and leave files alone.

A Worker without static assets

If your Worker has no assets folder, answer the path in code:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === "/ads.txt") {
      return new Response("google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0\n", {
        headers: { "Content-Type": "text/plain; charset=utf-8" },
      });
    }
    // …the rest of your app
  },
};

Custom domains and the root domain

Google starts at the root domain: "An ads.txt file on www.domain.com/ads.txt will only be crawled if domain.com/ads.txt redirects to it" (crawl rules). On Pages:

  • add the domain under Workers & Pages → your project → Custom domains → Set up a domain. For an apex domain like example.com, "you will need to add your site as a Cloudflare zone" (custom domains);
  • if you serve the site on www, redirect the apex to it (or the reverse) with a Redirect Rule or Bulk Redirect. Fixing www vs non-www in Cloudflare walks through both.

Also submit the custom domain to AdSense, not the pages.dev or workers.dev address.

Cloudflare in front of another host

If Cloudflare only proxies a site hosted elsewhere (WordPress, cPanel), ads.txt lives on that server and the steps above don't apply. Cloudflare can still get in the way: security rules or bot challenges that block Google's crawlers. The Cloudflare and AdSense guide covers those settings.

Check it yourself

curl -sI https://example.com/ads.txt | grep -iE "^HTTP|content-type"
curl -s  https://example.com/ads.txt | head -3
curl -sI http://example.com/ads.txt | grep -iE "^HTTP|location"

Good result: 200, text/plain, and your Google line as the first non-comment line. If you see text/html or <!doctype html>, you're looking at the SPA fallback, not your file. The ads.txt checker does the same tests and flags HTML bodies and wrong IDs. To see whether Cloudflare treats Google's crawlers differently from browsers, use the Googlebot access checker.

Then run a free scan of the custom domain. It checks ads.txt on HTTP and HTTPS, with and without www, along with the rest of what AdSense reviews.

Spotted something out of date or wrong? Tell us and we'll correct it.

Read this guide in Turkish →

Check it on your own site. Free, no sign-up.

Free tools for this

Free scan

Check your own site

Free scan: readiness score and every issue, usually in a few minutes.

Free scan · score and every problem found · no sign-up

All guides →