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.

IndexNow submission and key-file verification handshake On publish the site posts changed URLs to an IndexNow endpoint, the engine fetches the hosted key file to verify host ownership, and once verified the URLs are queued for fast re-crawl across participating engines. Publish event URL changed POST /indexnow host, key, urlList Key file at root {key}.txt Engines fast re-crawl engine verifies the key file before acting on the submission
Publish posts the changed URLs; the engine fetches the hosted key file to confirm ownership, then queues the URLs for re-crawl.

Step-by-step fix

  1. Generate a key and host it at the root. The key is an opaque string; the file is named {key}.txt and 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
  2. 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
    }
  3. 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;
    }
  4. 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);
    }
  5. Only submit indexable, canonical URLs. Submitting a noindex URL 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.

Debouncing many change events into one deduplicated submission Repeated change events within a thirty-second window collect in a Set that drops duplicate URLs, and a single flush posts the deduplicated batch to the IndexNow endpoint. A 30-second window coalesces changes into one deduplicated batch pending Set — 30s window /a /b /a /c /b pink = duplicate URL dropped by the Set flush() POST /indexnow urlList: /a /b /c
A busy CMS fires many small changes; the queue coalesces them into a single deduplicated batch instead of one request per keystroke.

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.txt must return the bare key as text/plain, not the app shell — the SPA router intercepting this path is the most common failure.

  • Check the submission status code. A 200 or 202 means accepted; 403 means the key file did not verify, and 422 means 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.

What each IndexNow response status means A 200 or 202 means the submission is accepted, a 403 means the hosted key file did not verify, and a 422 means a submitted URL does not belong to the host. Reading the submission response before assuming success Response status code 200/202 accepted — 202 pending key check 403 key file did not verify at the root 422 a URL does not belong to the host
Only a 200 or 202 confirms acceptance; a 403 points at the key file and a 422 at a URL that is not on the submitting host.

Common Pitfalls

  • Router swallowing the key file. If the catch-all route serves the app shell for /{key}.txt, verification fails with 403. 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.
Common IndexNow pitfalls and the correct practice for each Serving the app shell for the key file, shipping the key in client code, treating IndexNow as a sitemap replacement, submitting on every keystroke, and over-relying on Google's Indexing API each have a specific fix. Each pitfall, and what to do instead Pitfall Do this instead Router serves shell for the key file serve the key file at host or edge Key shipped in the client bundle submit from a server handler only Used as a sitemap replacement keep the XML sitemap for discovery Submitting on every keystroke debounce changes into batches Expecting the Indexing API to cover all it is scoped to jobs and events only
Most IndexNow failures trace to the key file or over-submission; each pitfall has a direct corrective practice.

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.

← Back to Sitemaps & Indexing Control for SPAs