Submitting SPA URLs with the IndexNow API
A sitemap tells engines a URL exists, but not that it changed five seconds ago — and for a client-rendered site publishing time-sensitive content, waiting for the next scheduled crawl is too slow. IndexNow closes that gap, and this guide wires it into a SPA’s publish flow as part of sitemaps and indexing control for SPAs.
Problem Statement
Discovery through a sitemap is passive: the crawler decides when to re-read it. When an article publishes, a price changes, or a listing is removed, that lag can span hours or days, which is unacceptable for content whose value is time-bound. IndexNow inverts the model — the site notifies participating engines the instant a URL changes, and because the notification is a plain HTTPS request it behaves identically for a client-rendered app and a static one. The only SPA-specific wrinkle is that the ownership-verification key file must be served as a real text file at the host root, not intercepted by the client router and returned as the app shell.
The flow is a short handshake: host a key file once, then post changed URLs whenever they publish. Engines fetch the key file to verify the host owns the submission before acting on it.
Step-by-step fix
-
Generate a key and host it at the root. The key is an opaque string; the file is named
{key}.txtand contains exactly that string. Serve it as a static asset so the client router never handles the path.Path: https://example.com/a1b2c3d4e5f64789a0b1c2d3e4f50617.txt Content: a1b2c3d4e5f64789a0b1c2d3e4f50617 -
Submit a single URL with a GET on publish. For one URL, a GET to the endpoint with query parameters is enough. Do this server-side — from the publish handler or a webhook — so the key never ships in the client bundle.
// submit-one.ts — runs server-side on a publish event const HOST = 'example.com'; const KEY = process.env.INDEXNOW_KEY!; // kept out of the client bundle export async function submitUrl(url: string): Promise<number> { const endpoint = new URL('https://api.indexnow.org/indexnow'); endpoint.searchParams.set('url', url); endpoint.searchParams.set('key', KEY); endpoint.searchParams.set('keyLocation', `https://${HOST}/${KEY}.txt`); const res = await fetch(endpoint, { method: 'GET' }); return res.status; // 200 accepted, 202 accepted pending verification } -
Batch changed URLs with a POST. When several URLs change together, send them in one JSON request. Keep each batch to a single host and within the per-request limit of 10,000 URLs.
// submit-batch.ts const HOST = 'example.com'; const KEY = process.env.INDEXNOW_KEY!; export async function submitBatch(urls: string[]): Promise<number> { const res = await fetch('https://api.indexnow.org/indexnow', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ host: HOST, key: KEY, keyLocation: `https://${HOST}/${KEY}.txt`, urlList: urls.slice(0, 10000), }), }); return res.status; } -
Debounce submissions behind a queue. A busy CMS can fire many small changes; coalesce them into periodic batches so you submit deduplicated URLs instead of hammering the endpoint per keystroke.
// submission-queue.ts import { submitBatch } from './submit-batch'; const pending = new Set<string>(); let timer: ReturnType<typeof setTimeout> | null = null; export function enqueue(url: string): void { pending.add(url); if (timer) return; timer = setTimeout(flush, 30_000); // coalesce a 30s window } async function flush(): Promise<void> { timer = null; const urls = [...pending]; pending.clear(); if (urls.length) await submitBatch(urls); } -
Only submit indexable, canonical URLs. Submitting a
noindexURL or a parameterized variant wastes the channel and muddies signals. Filter against the same policy that drives your robots directives and canonical rules before enqueuing.
Validation
Confirm the key file is reachable and submissions are accepted:
-
Fetch the key file as an engine would.
curl -s https://example.com/a1b2c3d4e5f64789a0b1c2d3e4f50617.txtmust return the bare key astext/plain, not the app shell — the SPA router intercepting this path is the most common failure. -
Check the submission status code. A
200or202means accepted;403means the key file did not verify, and422means a URL did not belong to the host.// submit.test.ts import { submitUrl } from './submit-one'; test('a published URL is accepted by IndexNow', async () => { const status = await submitUrl('https://example.com/blog/launch-day'); console.assert([200, 202].includes(status), `unexpected status ${status}`); }); -
Cross-check re-crawl timing in Search Console’s URL Inspection: a submitted URL should show a more recent crawl than an equivalent unsubmitted one.
-
Log submissions so you can confirm each publish fired exactly one notification and see the deduplicated batch contents.
Common Pitfalls
- Router swallowing the key file. If the catch-all route serves the app shell for
/{key}.txt, verification fails with403. Serve the key file ahead of the shell at the host or edge. - Shipping the key in the client bundle. The key belongs on the server; a key exposed in client JavaScript can be abused to submit URLs on your behalf. Submit from a server handler or webhook.
- Treating IndexNow as a sitemap replacement. It notifies about changes; it is not a complete inventory. Keep generating the XML sitemap for full discovery.
- Submitting on every keystroke. Un-debounced submission floods the endpoint with near-duplicate requests; coalesce into batches.
- Expecting Google’s Indexing API to cover everything. Its official scope is limited to job posting and broadcast event pages and it needs OAuth service-account auth, so it is not a general fast-index route the way IndexNow is.
Frequently Asked Questions
How does IndexNow verify that I own the URLs I submit?
You host a text file named after your key at the site root, containing the key as its only content. When you submit URLs, engines fetch that key file over the same host and confirm it matches the key in your request, proving control of the host before they act on the submission.
Does IndexNow replace an XML sitemap for a SPA?
No. IndexNow is a notification channel for URLs that just changed, giving fast re-crawl of individual pages, while the sitemap remains the standing inventory of every indexable URL. Use both: the sitemap for complete discovery and IndexNow to close the lag on time-sensitive updates.
How is Google’s Indexing API different from IndexNow?
Google’s Indexing API is officially scoped to job posting and broadcast event pages and requires OAuth service-account authentication, so it is not a general fast-index channel. IndexNow is an open protocol accepted by several engines for any URL, verified by a hosted key file rather than OAuth.
Related
- Sitemaps & Indexing Control for SPAs — where notification fits alongside discovery and permission.
- Generating Dynamic XML Sitemaps for SPAs — the standing inventory IndexNow complements.
- Controlling Indexing with Robots Meta in SPAs — submit only URLs you actually permit to be indexed.
← Back to Sitemaps & Indexing Control for SPAs