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: