Skip to content

Commit f6bb80c

Browse files
seo: IndexNow integration (Bing, Yandex, Seznam, Naver, DuckDuckGo)
IndexNow is an open protocol that lets a site push fresh URLs to participating search engines instantly, no rate limit. Bing, Yandex, Seznam, and Naver are participants; DuckDuckGo gets it through Bing. Google does NOT participate (use GSC URL Inspection for Google). What landed: - public/7399afbfb102ee7600c82332c40cdb26.txt: key file at site root, required by the protocol - scripts/indexnow-submit.mjs: posts the live sitemap URLs (or specific URLs) to api.indexnow.org - .github/workflows/indexnow.yml: re-submits on every push to src/content/, with a 90s grace period for Pages to redeploy first. Also supports manual workflow_dispatch with custom URL list Tested locally with the full sitemap (42 URLs): 202 Accepted. Once this commit lands and Pages serves the key file, the workflow will re-submit on every content change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 137a916 commit f6bb80c

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

.github/workflows/indexnow.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: IndexNow
2+
3+
# Submit fresh URLs to IndexNow (Bing, Yandex, Seznam, Naver, DuckDuckGo)
4+
# whenever docs content changes. Google is NOT an IndexNow participant; for
5+
# Google, use Search Console URL Inspection.
6+
7+
on:
8+
push:
9+
branches: [main]
10+
paths:
11+
- 'src/content/**'
12+
- 'public/sitemap.xml'
13+
workflow_dispatch:
14+
inputs:
15+
urls:
16+
description: 'Specific URLs (newline-separated). Empty = submit full sitemap.'
17+
required: false
18+
type: string
19+
20+
jobs:
21+
submit:
22+
runs-on: ubuntu-latest
23+
if: github.repository == 'BrowyHQ/browyhq.github.io'
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: actions/setup-node@v4
27+
with:
28+
node-version: '20'
29+
30+
- name: Wait for GitHub Pages to redeploy
31+
# Give Pages a minute to publish the new sitemap before we submit.
32+
run: sleep 90
33+
34+
- name: Submit to IndexNow
35+
env:
36+
MANUAL_URLS: ${{ github.event.inputs.urls }}
37+
run: |
38+
if [ -n "$MANUAL_URLS" ]; then
39+
echo "$MANUAL_URLS" | xargs node scripts/indexnow-submit.mjs
40+
else
41+
node scripts/indexnow-submit.mjs
42+
fi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7399afbfb102ee7600c82332c40cdb26

scripts/indexnow-submit.mjs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// scripts/indexnow-submit.mjs
2+
// Submit URLs from the live sitemap to IndexNow.
3+
// Hits api.indexnow.org once, which fans out to Bing, Yandex, Seznam, Naver,
4+
// and any other future IndexNow participants. Google is NOT a participant.
5+
//
6+
// Usage:
7+
// node scripts/indexnow-submit.mjs # submit all sitemap URLs
8+
// node scripts/indexnow-submit.mjs <url> [<url>] # submit specific URLs
9+
//
10+
// CI mode: triggered by .github/workflows/indexnow.yml on every push to main
11+
// that touches src/content/.
12+
13+
import fs from 'node:fs';
14+
import path from 'node:path';
15+
import { fileURLToPath } from 'node:url';
16+
17+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
18+
const ROOT = path.resolve(__dirname, '..');
19+
20+
const SITE = 'https://browyhq.github.io';
21+
const KEY = '7399afbfb102ee7600c82332c40cdb26';
22+
const KEY_LOCATION = `${SITE}/${KEY}.txt`;
23+
const ENDPOINT = 'https://api.indexnow.org/IndexNow';
24+
25+
async function fetchSitemapURLs() {
26+
const res = await fetch(`${SITE}/sitemap-0.xml`);
27+
if (!res.ok) throw new Error(`sitemap fetch failed: ${res.status}`);
28+
const xml = await res.text();
29+
const urls = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]);
30+
return urls;
31+
}
32+
33+
function chunk(arr, n) {
34+
const out = [];
35+
for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n));
36+
return out;
37+
}
38+
39+
async function submit(urls) {
40+
if (urls.length === 0) {
41+
console.log('no urls to submit');
42+
return;
43+
}
44+
45+
// IndexNow caps a single POST at 10,000 URLs. We're well under that, but be defensive.
46+
for (const batch of chunk(urls, 1000)) {
47+
const body = {
48+
host: new URL(SITE).host,
49+
key: KEY,
50+
keyLocation: KEY_LOCATION,
51+
urlList: batch,
52+
};
53+
54+
const res = await fetch(ENDPOINT, {
55+
method: 'POST',
56+
headers: {
57+
'Content-Type': 'application/json; charset=utf-8',
58+
'Accept': 'application/json',
59+
},
60+
body: JSON.stringify(body),
61+
});
62+
63+
const text = await res.text();
64+
console.log(`POST ${ENDPOINT} ${res.status} ${res.statusText} (${batch.length} URL${batch.length === 1 ? '' : 's'})`);
65+
if (text.trim()) console.log(` response: ${text.slice(0, 300)}`);
66+
67+
// 200 OK = accepted. 202 = received but key still validating.
68+
// 400/422 = bad payload. 403 = key file missing. 429 = throttled.
69+
if (res.status !== 200 && res.status !== 202) {
70+
throw new Error(`IndexNow returned ${res.status}`);
71+
}
72+
}
73+
}
74+
75+
async function main() {
76+
const args = process.argv.slice(2);
77+
let urls = args.length > 0 ? args : await fetchSitemapURLs();
78+
79+
// De-dupe and keep only URLs on our host.
80+
const host = new URL(SITE).host;
81+
urls = [...new Set(urls)].filter((u) => {
82+
try {
83+
return new URL(u).host === host;
84+
} catch {
85+
return false;
86+
}
87+
});
88+
89+
console.log(`submitting ${urls.length} URL${urls.length === 1 ? '' : 's'} to IndexNow`);
90+
await submit(urls);
91+
console.log('done');
92+
}
93+
94+
main().catch((err) => {
95+
console.error(err);
96+
process.exit(1);
97+
});

0 commit comments

Comments
 (0)