GitHub Action¶
Use indexnow as a step in any GitHub Actions workflow. The action downloads the matching release binary for the runner's OS/arch, verifies its sha256 against checksums.txt from the same release, and runs indexnow submit with the inputs you pass.
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
sitemap: https://example.com/sitemap.xml
Pin to a major (@v0) for floating-but-stable, a tag (@v0.5.0) for exactness, or a commit SHA for supply-chain paranoia.
Supported runners¶
runs-on |
Status |
|---|---|
ubuntu-latest, ubuntu-* (x64, arm64) |
supported |
macos-latest, macos-* (x64, arm64) |
supported |
windows-* |
unsupported — preflight fails fast with a pointer to ubuntu/macos |
Inputs¶
| Name | Required | Default | Notes |
|---|---|---|---|
key |
yes | — | IndexNow key (8..128, [A-Za-z0-9-]). Passed via env, not flags. |
urls |
one of | — | Newline-separated URLs. |
file |
one of | — | Path to a file with one URL per line. |
sitemap |
one of | — | URL or local path to sitemap.xml (.gz and <sitemapindex> followed). |
urls-from |
one of | — | Bash snippet; its stdout is treated as the URL list. See Custom URL sources. |
sitemap-since |
no | — | RFC3339; drop entries with older <lastmod>. |
sitemap-timeout |
no | CLI default (30s) |
Per-request HTTP timeout for sitemap fetches. |
host |
no | inferred from first URL | Site host (e.g. example.com). |
key-location |
no | derived from host + key |
Absolute URL to the hosted key file. |
endpoint |
no | api |
Alias or full URL; comma-separated for fan-out. |
user-agent |
no | indexnow/<version> |
Sent as User-Agent. |
config |
no | — | Path (relative to $GITHUB_WORKSPACE) to an indexnow yaml config — host, key, endpoint, etc. defaults. |
fail-on |
no | any |
any\|4xx\|5xx\|never — which response classes set exit ≠ 0. |
quiet |
no | false |
Suppress the CLI's stdout in the step log. |
verbose |
no | false |
Emit slog lifecycle/retry events to stderr. |
dry-run |
no | false |
Print what would be sent; do not call the endpoint. |
max-retries |
no | CLI default | Retries on 429/5xx/transport. |
base-backoff |
no | CLI default | Initial backoff (Go duration, e.g. 200ms). |
max-backoff |
no | CLI default | Cap on backoff. |
version |
no | action's tag | indexnow release to install. "latest" resolves at run time. |
github-token |
no | ${{ github.token }} |
Only used to call the GitHub Releases API. |
Exactly one of urls / file / sitemap / urls-from must be set — preflight fails otherwise.
Outputs¶
| Name | Notes |
|---|---|
exit-code |
Exit code of indexnow submit. |
submitted-count |
Sum of urlCount across all batches. 0 in dry-run. |
failed-count |
Batches that finished non-2xx or with an error. 0 in dry-run. |
report |
One-line summary; also written to $GITHUB_STEP_SUMMARY. |
Recipes¶
On every push to content/¶
name: indexnow
on:
push:
branches: [main]
paths: ["content/**"]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
sitemap: https://example.com/sitemap.xml
sitemap-since: ${{ github.event.before }}
endpoint: bing,yandex
sitemap-since: ${{ github.event.before }} is a string; the action passes it through to --sitemap-since, which parses RFC3339. For pushes that's the commit timestamp of the previous head — entries with older <lastmod> are skipped, the rest go out.
Hourly schedule¶
on:
schedule:
- cron: "0 * * * *"
jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
sitemap: https://example.com/sitemap.xml
sitemap-since: ${{ format('{0}-{1}-{2}T{3}:00:00Z', …) }}
If you don't want to fiddle with date arithmetic in YAML, drop sitemap-since — every hourly run re-submits the whole sitemap. IndexNow is idempotent; the cost is the HTTP call.
Custom URL sources via urls-from¶
When neither urls, file, nor sitemap fits — produce the URL list yourself. urls-from is a bash snippet whose stdout becomes the URL list (one URL per line, #-prefixed lines are comments, blank lines ignored). Runs in $GITHUB_WORKSPACE, so git, locally-checked-out files, and other tools are immediately available.
Empty output is success — the step exits 0 with submitted-count=0 and submit is not invoked. A non-zero exit from your snippet fails the step (stderr is preserved in the step log).
Recipe: only changed URLs from git diff¶
The original motivation. The path-to-URL mapping is project-specific (Hugo permalinks, Eleventy permalink: front-matter, custom routers), so it lives in your snippet — not in a config schema indexnow has to maintain.
- uses: actions/checkout@v6
with:
fetch-depth: 0 # required so `git diff <base>..HEAD` resolves
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
host: example.com
urls-from: |
git diff --name-only --diff-filter=AMR \
"${{ github.event.before }}..${{ github.event.after }}" -- 'content/**/*.md' |
sed 's#^content/\(.*\)\.md$#https://example.com/\1/#'
Watch out for the "force-push to a new branch" edge case where github.event.before is 0000000… — guard with an if: and fall back to origin/main..HEAD.
Recipe: sitemap with a content-filter¶
When --sitemap-since is not selective enough — e.g. you want only URLs whose path matches a prefix:
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
urls-from: |
curl -sSL https://example.com/sitemap.xml |
grep -oE '<loc>[^<]+</loc>' |
sed -E 's#</?loc>##g' |
grep '^https://example.com/blog/'
Recipe: pull URLs from a CMS API¶
- uses: jtprogru/indexnow@v0
with:
key: ${{ secrets.INDEXNOW_KEY }}
urls-from: |
curl -sSL -H "Authorization: Bearer ${{ secrets.CMS_TOKEN }}" \
https://cms.example.com/api/recently-published |
jq -r '.items[].url'
urls-from is action-only. From the CLI, the equivalent is a plain shell pipe into indexnow submit --stdin:
git diff --name-only HEAD~1..HEAD -- 'content/**/*.md' |
sed 's#^content/\(.*\)\.md$#https://example.com/\1/#' |
indexnow submit --stdin
Secrets¶
Put INDEXNOW_KEY in repository or org-level secrets. The action takes it as an input and passes it through env (INDEXNOW_KEY) — the value never appears in command-line traces, even with set -x.
Hosting the key file: drop a <key>.txt next to your static site's root that contains exactly the key. The verify subcommand on the CLI checks this; it isn't yet exposed as an action input — call indexnow verify as a follow-up step if you need the assertion in CI.
How install works¶
On the first use in a job:
- Resolve version (
inputs.version→ action's tag →latest). - Download
indexnow_{Linux|Darwin}_{x86_64|arm64}.tar.gzandchecksums.txtfrom the release. - Verify sha256 (
sha256sum -con Linux,shasum -a 256on macOS). - Extract to
$RUNNER_TOOL_CACHE/indexnow/<version>/<os-arch>/. - Cache that directory via
actions/cachekeyed by version+os+arch — subsequent jobs (and reruns) skip the download.
A repository pinning the action by commit SHA inherits the GPG-signed checksums.txt.sig only when verifying out-of-band; the action itself relies on TLS to GitHub Releases plus sha256.