seo-automation
Blog

IndexNow GitHub Actions: Submit Changed URLs on Deploy

November 9, 2026

An IndexNow GitHub Actions workflow should run after your host reports a successful production deploy. It should send only the URLs that changed, in one POST to an IndexNow endpoint, and fail the job when the response is not 200 or 202. Google does not take part in IndexNow, so you also need to resubmit your sitemap through Search Console.

Most tutorials run the submission on git push and use git diff to find the pages. That works for a plain static site. It breaks on Next.js, Astro or a headless CMS, where one content change can update many pages, or where nothing in the repo changes at all. This guide gives you a setup that holds up in those cases. It compares the sitemap before and after each deploy, handles several domains and makes errors visible.

Start for $1 and get your SEO plan in a few minutes.

What IndexNow does, and which engines use it

IndexNow is a protocol for pinging search engines when a URL is added or updated. You send the URL to one endpoint, https://<searchengine>/indexnow, and the submission is shared with the other participating engines.

The official list of participants is Bing, Yandex, Seznam, Naver, Yep, the Internet Archive and Amazonbot. Google is not on it. IndexNow helps with Bing and the engines that use Bing's index, but it does nothing for Google. For Google, you resubmit your sitemap, which we cover below.

If you want this whole loop to run without you, it fits into a wider SEO automation pipeline: content gets published, then the engines get notified.

Set up the IndexNow key

The key proves you own the host. According to the IndexNow documentation:

  • Length: 8 to 128 characters.
  • Allowed characters: lowercase and uppercase letters, digits and dashes.
  • Location: a UTF-8 text file named <your-key>.txt at the site root, whose only content is the key.
  • Other locations: possible with the keyLocation parameter, but the key then only covers URLs under that path.

Where to put the file:

Stack Folder that becomes the root
Next.js public/
Astro public/
Hugo static/
Headless CMS + any frontend the frontend's static folder

Store the key in a GitHub secret named INDEXNOW_KEY. The key becomes public once the file is live, since anyone can open /<key>.txt. The secret keeps the key out of your code and makes rotation easier. It does not hide the key. If you would rather not commit the file, have your host's build step write it from an environment variable, for example echo "$INDEXNOW_KEY" > public/$INDEXNOW_KEY.txt.

Trigger after the deploy, not on push

A workflow cannot tell on its own when your hosting provider has finished deploying, as this tutorial points out. If you submit on push, crawlers can arrive before the new page exists. They will then see a 404 or the old version.

There are three ways to fix this, from most to least reliable:

  1. deployment_status event. Hosts that report deployments to GitHub, like Vercel, fire this event. Filter on state == 'success' and on your production environment name. (GitHub docs)
  2. workflow_run event. Use this if you deploy with your own workflow, for example GitHub Pages. It runs after the deploy workflow completes. (GitHub docs)
  3. Poll the live site. Use this if your host reports nothing back. Loop curl on the sitemap until it contains the new URL or lastmod, then submit.

How to submit only changed URLs to IndexNow

git diff-tree --no-commit-id --name-only -r HEAD maps changed files to URLs (example). That is enough when one Markdown file equals one page. It misses tag pages, paginated lists, pages rebuilt from a CMS, and content edited outside Git.

Comparing sitemaps avoids those gaps. After each deploy, the script reads the live sitemap and compares each URL's lastmod with the previous run. It then sends only the new or changed URLs. The workflow keeps the previous snapshot in the Actions cache.

name: IndexNow after deploy
on: deployment_status
jobs:
  indexnow:
    if: github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'Production'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: prev.json
          key: sitemap-${{ github.run_id }}
          restore-keys: sitemap-
      - run: python3 scripts/indexnow.py
        env:
          SITEMAP_URL: ${{ vars.SITEMAP_URL }}
          INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}
      - uses: actions/cache/save@v4
        with:
          path: prev.json
          key: sitemap-${{ github.run_id }}

Check the environment name your host uses (Production on Vercel). Set SITEMAP_URL as a repository variable. If your framework writes a sitemap index, as Astro's integration does, point the variable at the child sitemap that lists the pages.

import json, os, sys, urllib.request, urllib.error, xml.etree.ElementTree as ET
from urllib.parse import urlparse
url, key = os.environ["SITEMAP_URL"], os.environ["INDEXNOW_KEY"]
ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
root = ET.fromstring(urllib.request.urlopen(url).read())
now = {u.findtext("s:loc", None, ns): u.findtext("s:lastmod", "", ns) for u in root.findall("s:url", ns)}
old = json.load(open("prev.json")) if os.path.exists("prev.json") else {}
changed = [u for u, m in now.items() if old.get(u) != m]
if old and changed:
    body = json.dumps({"host": urlparse(url).netloc, "key": key, "urlList": changed[:10000]}).encode()
    req = urllib.request.Request("https://www.bing.com/indexnow", body, {"Content-Type": "application/json; charset=utf-8"})
    try:
        code = urllib.request.urlopen(req).status
    except urllib.error.HTTPError as e:
        code = e.code
    print(f"{len(changed)} URLs sent, HTTP {code}")
    if code not in (200, 202):
        sys.exit(1)
json.dump(now, open("prev.json", "w"))

On the first run, the script only saves a snapshot. Each later run sends only what changed. If a submission fails, the job stops before the snapshot is saved, so the same URLs go out again on the next deploy. The [:10000] cap follows the limit of 10,000 URLs per POST. Above that, send several requests.

Limits of the sitemap diff

The method is only as good as your lastmod values. Check three things before you rely on it:

  • lastmod must change only when the page changes. A Next.js sitemap.ts that sets lastModified: new Date() on every entry gives every URL a new date at each build. The script would then send the whole sitemap on every deploy. Use the real update date of each page, from your content front matter or your CMS.
  • Pages without lastmod are never detected as updated. The script only catches them when they first appear in the sitemap.
  • The cache can expire. GitHub removes cache entries that have not been accessed in over 7 days. If you go a week without deploying, the next run finds no snapshot, saves a new one and sends nothing. Pages changed during that gap will wait for the next change or for crawlers.

Rather not maintain this pipeline? Deploy events, sitemap diffs, cache keys and error handling add up to a system you have to keep working. SEO Automation writes and publishes 50 articles a month, resubmits the sitemap to Google Search Console and notifies IndexNow after each publish. Start for $1 and get your SEO plan in a few minutes.

Read the response codes and fail cleanly

These are the codes documented by IndexNow and what to do about each one:

Code Meaning What to do
200 Submitted Nothing
202 Received, key validation pending Normal on the first submissions. Check that the key file is reachable
400 Invalid format Check the JSON body and the Content-Type
403 Key not valid The key file is missing at the root, or its content differs from the secret
422 URLs don't match the host or key The sitemap lists another domain (www vs apex, a preview URL), or keyLocation covers a different path
429 Too many requests You are submitting too often. Send changes once per deploy and never the full sitemap on a schedule

A failed step in the Actions log is far easier to spot than a silent curl that returns 403 on every deploy for months.

Several sites in one workflow

If you are an agency or run separate docs and marketing sites, use a matrix. Each site needs its own cache key and its own restore-keys prefix. If restore-keys stays sitemap-, a job can restore the latest snapshot of another domain. None of its URLs would match, so the whole sitemap would go out as "changed".

    strategy:
      matrix:
        sitemap: [https://www.example.com/sitemap.xml, https://docs.example.com/sitemap.xml]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: prev.json
          key: sitemap-${{ strategy.job-index }}-${{ github.run_id }}
          restore-keys: sitemap-${{ strategy.job-index }}-
      - run: python3 scripts/indexnow.py
        env:
          SITEMAP_URL: ${{ matrix.sitemap }}
          INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}
      - uses: actions/cache/save@v4
        with:
          path: prev.json
          key: sitemap-${{ strategy.job-index }}-${{ github.run_id }}

strategy.job-index follows the order of the matrix list. If you reorder or remove a site, the indexes shift and each job will read the wrong snapshot once. To avoid that, add a short stable id per site to the matrix and use it in both keys instead.

Each host needs a key file at its own root. You can use the same key on every host, or one key per host stored in separate secrets. When sites live in separate repos, put the script in a shared repo and call it as a reusable workflow, so a fix lands everywhere at once.

IndexNow GitHub Action or a curl script?

Approach Good for Limits
Ready-made action, such as index-now-submit-sitemap-urls-action (v2.0.2, MIT) Small sites, quick setup Submits whole sitemaps, not only changed pages
curl + git diff One Markdown file per page Misses pages that change without a file change
Sitemap diff script (above) Next.js, Astro, Hugo, headless CMS, several domains You maintain about 20 lines of Python and need reliable lastmod values

Cover Google and check what got indexed

For Google, submit the sitemap with the Search Console API sitemaps.submit method in the same job, using a service account that has access to the property. Google retired its sitemap ping endpoint in 2023 and relies on accurate lastmod values, which the sitemap diff also depends on. Our guide to the Search Console API key setup covers the credentials.

A 200 response only means the engine received the URLs. It does not mean the pages are indexed. Check indexing a few days later:

  • Google: the URL Inspection API, run on the URLs from the last submission.
  • Bing: Bing Webmaster Tools.

If you build on Next.js, also check that your sitemap and canonical tags are clean before you automate anything. Our list of Next.js App Router SEO mistakes covers the usual ones.

What to automate and what to keep human

You can safely automate:

  • the trigger after the deploy
  • the sitemap diff
  • the submission and the response check
  • the sitemap resubmission to Google
  • the indexing check

A person should still review:

  • why a 403 or 422 appears (usually a domain or redirect problem)
  • pages that stay unindexed, which often have thin or duplicate content
  • whether a page should be published at all

Submitting a URL does not make it rank, and pinging weak pages more often won't help them.

How SEO Automation handles this

After each article goes live on WordPress, Webflow, Shopify, GitHub (Next.js, Astro, Hugo) or by webhook, the Publisher agent resubmits the sitemap to Google Search Console and notifies IndexNow. You don't need to maintain a workflow file. Articles that drift to page 2 are rewritten.

Start for $1 and get your SEO plan in a few minutes.

Frequently asked questions

Does Google support IndexNow?

No. The official list of participating engines does not include Google. To notify Google, submit your sitemap with the Search Console API and keep accurate lastmod dates.

Should I submit the whole sitemap or only changed pages?

Submit only changed pages. Sending the full sitemap on every deploy adds noise and makes 429 responses more likely. A sitemap diff limits each submission to new or updated URLs, as long as lastmod only changes when a page really changes.

Where does the IndexNow key file go?

At the root of the host, named <key>.txt and containing only the key. In Next.js and Astro that means public/, and in Hugo static/. A key file in a subfolder only validates URLs under that folder.

Why does IndexNow return 422?

The submitted URLs don't match the host or the key location. The usual causes are a sitemap that lists www URLs while the request uses the apex domain, preview deployment URLs, or a keyLocation that covers a different path.