Skip to content
Approvalens

Reading room · 5 min read

How to Add ads.txt in Next.js (App Router and Pages Router)

Add ads.txt to a Next.js site: a static file in /public or a Route Handler, a proxy matcher that leaves it alone, and how to check it on the live domain.

By the Approvalens team

Fixes these report findings

  • ads.txt file
  • ads.txt content type
  • Google line in ads.txt
  • ads.txt format
  • ads.txt redirect

In Next.js, ads.txt is a plain file: save it as public/ads.txt, deploy, and it's served at https://yourdomain.com/ads.txt. This works the same in the App Router and the Pages Router. Most problems come from what sits between the file and Google: a proxy (formerly middleware) that rewrites the path, a www setup that leaves the root domain without the file, or a hosting rule that answers unknown paths with HTML.

1. Get your exact line

AdSense gives you one line with your own publisher ID (ads.txt guidance):

google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0

Use pub- plus 16 digits. The ca- prefix from your ad code doesn't belong in ads.txt. If you run other ad networks, their lines go in the same file. Formatting rules and errors are in the general ads.txt setup guide.

2. Put the file in /public

my-app/
├── app/            (or pages/)
├── public/
│   └── ads.txt     ← served at /ads.txt
└── next.config.ts

Next.js serves everything in public from the root: "Files inside public can then be referenced by your code starting from the base URL (/)" (public folder). There's no special ads.txt file convention in app/. The metadata conventions cover icons, the manifest, Open Graph images, robots.txt and sitemap.xml, nothing else (metadata files).

Save the file as UTF-8 plain text, in a code editor rather than a word processor, with a newline at the end.

Three ways a request for /ads.txt can end in a Next.js app: served from public/ads.txt with status 200, generated by app/ads.txt/route.ts, or caught by a proxy without a matcher and redirected to /en/ads.txt which returns 404
Two working routes to /ads.txt, and the proxy trap that breaks both.

Alternative: a Route Handler

If the publisher ID should come from an environment variable, or you deploy the same code to several domains, generate the file. Route Handlers can return non-HTML responses, and the docs use a dotted folder name for exactly this (app/rss.xml/route.ts) (Route Handlers):

// app/ads.txt/route.ts
export const dynamic = "force-static";

export function GET() {
  const line = `google.com, ${process.env.ADSENSE_PUB_ID}, DIRECT, f08c47fec0942fa0`;
  return new Response(line + "\n", {
    headers: { "Content-Type": "text/plain; charset=utf-8" },
  });
}

force-static builds the response once. It's also required if you use output: 'export', where Route Handlers must be marked static (static exports). Check that ADSENSE_PUB_ID is set in the production environment. Otherwise the file says google.com, undefined, DIRECT…, which is worse than no file. Keep either public/ads.txt or the Route Handler, never both.

We tested both versions on Next.js 16.3 in development. Each returned status 200 with a text/plain content type.

3. Keep proxy.ts away from the file

Next.js 16 renamed middleware.ts to proxy.ts. The docs warn: "Without a matcher, Proxy runs on every request, including static files … and assets in the public/ folder" (proxy.js). Typical consequences for ads.txt:

  • an i18n proxy that adds a locale prefix sends /ads.txt to /en/ads.txt, which doesn't exist;
  • an auth proxy sends Google's crawler to /login;
  • a bot filter returns 403 to anything that isn't a browser.

Exclude files from the matcher. The official example leaves out favicon.ico, sitemap.xml and robots.txt by name. Skipping every path with a dot covers ads.txt too:

// proxy.ts
export const config = {
  matcher: ["/((?!api|_next|.*\\..*).*)"],
};

Also check next.config rewrites. Files in public are served after beforeFiles rewrites but before regular ones (rewrites), so a catch-all beforeFiles rewrite to an external CMS can capture /ads.txt.

4. Make the root domain answer

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). The crawler also tries both HTTP and HTTPS.

  • On Vercel, Netlify and similar hosts, add both example.com and www.example.com to the project and set one to redirect to the other in the host's domain settings. Then confirm the redirect keeps the path: example.com/ads.txt should land on www.example.com/ads.txt, not on the home page.
  • In next.config, you can redirect by host. permanent: true sends a 308. If you prefer a classic 301, use statusCode instead (redirects):
// next.config.ts
async redirects() {
  return [{
    source: "/:path*",
    has: [{ type: "host", value: "www.example.com" }],
    destination: "https://example.com/:path*",
    statusCode: 301,
  }];
}

Redirects in next.config "are checked before the filesystem which includes pages and /public files", so this also applies to /ads.txt. They don't work with output: 'export', where the redirect has to be set up at the host. The www redirect guide for Cloudflare shows the DNS side.

5. Static export and other hosts

With output: 'export', public/ads.txt is copied into out/. What happens next depends on the host. Cloudflare Pages without a 404.html treats the project as a single-page app and serves the home page for missing paths, so a missing ads.txt looks like a 200 HTML page. The details are in ads.txt on Cloudflare Pages and Workers.

Check it on the live domain

curl -sI https://example.com/ads.txt
curl -sI http://example.com/ads.txt
curl -s  https://example.com/ads.txt
Terminal output of curl -i against a Next.js dev server: HTTP/1.1 200 OK, Content-Type text/plain; charset=UTF-8, Content-Length 59, followed by the single Google line
What a working response looks like. This is real output from our Next.js 16.3 test with the placeholder ID.

You want status 200 (directly or after one redirect), text/plain, and your Google line in the body, with no <html> anywhere. The ads.txt checker runs these checks and compares the ID with your AdSense code. If your robots.txt has a rule like Disallow: /ads, it also blocks /ads.txt. Test it with the robots.txt tester.

What you see Likely cause in Next.js Fix
404 on /ads.txt File outside public, or under public/static/ Move it to public/ads.txt
307/308 to /en/ads.txt i18n proxy without a file exclusion Add the matcher above
302 to /login Auth proxy Exclude files and public pages from the matcher
200 with HTML Catch-all rewrite or host SPA fallback Let the file be served before rewrites; add a 404.html on static hosts
undefined in the line Route Handler without the env var in production Set ADSENSE_PUB_ID or switch to a static file
Works on www, 404 on the root domain Root domain not connected or not redirecting Add the root domain and redirect it

AdSense needs time to notice changes. Google says it "may take a few days", and up to a month for sites with few ad requests (ads.txt guidance).

Before you apply, run the free site scan on the production domain. It fetches ads.txt over HTTP and HTTPS with and without www, so you see exactly what Google's crawler gets. The Next.js AdSense guide covers the rest of the setup: rendering, script placement and consent.

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 →