IndexNow for Next.js

Where the key file goes, when to actually submit, and the framework-specific reasons a correct setup still fails.

Quick answer

Put your key file at public/{key}.txt — Next serves public/ from the site root, which is where the protocol requires it. Submit from a post-deploy hook, not from the build, and guard on VERCEL_ENV === 'production'. If the file 404s, check middleware.ts before anything else.

The key file

One file, no configuration. Generate a key, then create the file containing only that key:

public/
└── a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.txt   # contains exactly that string

Everything in public/ is served from the root, so that arrives at https://yourdomain.com/a1b2…d6.txt. That is the whole setup.

Why not a route handler with an env var?

Because the key's location scopes what it authorises. A key served at /api/indexnow-key only validates URLs starting with /api/indexnow-key/ — which is none of your pages. If you want a route handler, it has to answer at the root path. A static file sidesteps the problem entirely, and the key is not a secret, so there is nothing to gain from hiding it in an environment variable.

Submit after deploy, never during build

This is the mistake that makes an otherwise perfect setup useless. Running a submission script in next build or a postbuild npm script announces URLs while the build output is still in a container that has never served traffic. Engines fetch within seconds and get whatever the old deployment serves — or a 404 for a genuinely new page.

Submit once the deployment is live and promoted, from a deploy hook or a CI job keyed on deployment success:

// scripts/submit-indexnow.mjs — run AFTER the deployment is live
if (process.env.VERCEL_ENV !== 'production') {
  console.log('Not production. Skipping IndexNow submission.');
  process.exit(0);
}

const res = await fetch('https://api.indexnow.org/indexnow', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    host: 'yourdomain.com',
    key: process.env.INDEXNOW_KEY,
    keyLocation: `https://yourdomain.com/${process.env.INDEXNOW_KEY}.txt`,
    urlList: ['https://yourdomain.com/blog/new-post'],
  }),
});

// 202 means "received", not "accepted". The key is validated later,
// and a failure is discarded silently. Verify the key file separately.
console.log(res.status);

The environment guard matters more than it looks. Without it, every pull-request preview announces your production URLs, from a *.vercel.app hostname whose relationship to your key file is at best accidental.

Submitting when content actually changes

For a site whose content comes from a CMS rather than the repo, deploys are the wrong trigger — the content changes without one. If you are already calling revalidatePath() or revalidateTag() from a CMS webhook, that handler is the correct place to submit as well. It is by definition the moment you know a specific URL changed.

// app/api/cms-webhook/route.ts
export async function POST(request: Request) {
  const { slug } = await request.json();

  revalidatePath(`/blog/${slug}`);
  await submitToIndexNow([`https://yourdomain.com/blog/${slug}`]);

  return Response.json({ revalidated: true });
}

Submit only what changed. Resubmitting your whole sitemap on every webhook is the fastest way to have a host deprioritised.

Why it fails on Next.js specifically

The file is in the right place, the code is right, and the key still does not validate. In rough order of how often each turns out to be the cause:

Middleware is intercepting the key file

middleware.ts runs before static files are served. A broad matcher catches /{key}.txt along with everything else, and any rewrite, redirect or auth check then applies to it.

Internationalisation middleware is the usual offender: it redirects paths without a locale prefix, so your key file becomes /en/{key}.txt and the root path either 404s or redirects. Some engines do not follow redirects on the key file at all.

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\.txt).*)'],
};

Vercel Deployment Protection returns 401

If protection is enabled, every request without a valid session gets a 401 — including search engines fetching your key file. It is on by default for preview deployments and is sometimes switched on for production during a soft launch and forgotten. The key file looks fine in your browser because you are authenticated.

You submitted the wrong hostname

A Vercel project answers on its custom domain, on project.vercel.app, and on a per-deployment URL. These are different hosts to the protocol. Submit the host your canonical tags and sitemap use, and never mix hosts in a single submission — that returns 422 rather than failing quietly.

A catch-all rewrite is swallowing it

A rewrites() entry in next.config.js with a source like /:path* — common when proxying a blog or docs subpath — can route your key file somewhere else entirely. The response is then a perfectly valid HTML page that simply is not your key.

You tried to write the file at runtime

public/ is a build-time snapshot, and serverless filesystems are read-only. Writing the key file from application code at startup works locally and does nothing in production.

A static export dropped your route handler

With output: 'export' there is no server, so route handlers are not included in the build. If you served the key from one, it vanishes — without an error, because the build succeeded. Files in public/ are copied through normally.

Confirm it before you trust it

The shared endpoint, Bing and Yandex return 202 Accepted for a submission whose key was never published, validate it later, and discard it silently on failure. A 202 in your deploy logs is not evidence of anything. Fetch the key file the way an engine does:

Frequently asked questions

Where does the IndexNow key file go in a Next.js project?

In the public/ directory, as public/{key}.txt. Next serves everything in public/ from the site root, so that file lands at https://yourdomain.com/{key}.txt, which is exactly where the protocol expects it. Do not put it in app/, pages/, or src/ — those are compiled, not served as static files.

Can I serve the key from a route handler instead, so it comes from an env var?

You can, but put it at the root path if you do. The specification scopes a key by its own location: a key served at /api/indexnow-key only authorises URLs beginning with /api/indexnow-key/, which is almost certainly none of your pages. A route handler at the root works; anything nested quietly restricts what you are allowed to submit. A static file in public/ avoids the whole question.

Should I submit URLs during next build?

No, and this is the most common mistake. The build runs before the deployment is live, so search engines fetching the URLs you just announced will get the previous version of the site or a 404. Submit from a post-deploy hook instead — on Vercel that is a Deploy Hook or a GitHub Action triggered on deployment success, not a build step.

How do I submit only from production and not from every preview deploy?

Guard on the environment. On Vercel, process.env.VERCEL_ENV is "production", "preview" or "development" — check for "production" before submitting anything. Without that guard, every pull request preview announces your production URLs, and does it from a hostname whose key file may not even match.

Why does my key file 404 when it is definitely in public/?

Check middleware.ts first. Middleware runs before static files are served, and a matcher that does not exclude your key file can rewrite or redirect it — internationalisation middleware is the usual culprit, since it redirects paths without a locale prefix. Add the key file to the matcher exclusions and it will resolve immediately.

Does IndexNow work with a static export?

Yes for the key file — output: "export" copies public/ into the exported output, so the file ships. But route handlers do not exist in a static export, so if you served the key from app/.../route.ts it silently disappears from the build with no error. That is a good reason to prefer the static file even if an env var feels tidier.