Files
hidden11/scripts/submit-indexnow.mjs
claude-code 0de85cbc51
Build and push image / build (push) Failing after 15m20s
Localize the archive, add reminders, named challenges and a funnel
Archive and answer pages now exist in all six languages and are prerendered
instead of rendered per request, and each answer page carries a sentence of
real context per player (club, league, nationality, former clubs, honours)
plus Article and BreadcrumbList JSON-LD. The sitemap grows from 121 to 691
URLs, every one with hreflang alternates.

Adds an optional display name to shared results, so a challenge link reads
"<name> challenges you" in the page, the OG image and the head-to-head
verdict; the name is sanitised both when written and when parsed back.

Adds an opt-in daily reminder (Web Push without payload, so no content
encryption) and a PWA install prompt shown once the day is finished, plus
first-party event ingestion at /api/events that mirrors GA4 so the funnel
survives ad blockers.

Wires the missing N8N_PLAYS_WEBHOOK_URL into the deployment along with the
new events, push and search-verification secrets, and ships the n8n
workflows, the Postgres schema with funnel/retention views, and an IndexNow
submitter.

Refs #2 #3 #5 #6 #7 #8 #9 #10

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvQ1ErZNRUcWgCEkJ9uyrn
2026-08-26 12:41:28 +00:00

84 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
// Submits the sitemap URLs to IndexNow (Bing, Yandex, Seznam, Naver).
//
// Google dropped its sitemap ping endpoint in 2023, so the Search Console
// submission stays a one-off manual step; IndexNow is the part that can run
// unattended, which is why the daily n8n job calls this.
//
// Usage:
// node scripts/submit-indexnow.mjs # every URL in the sitemap
// node scripts/submit-indexnow.mjs --today # only today-sensitive URLs
// INDEXNOW_KEY=... SITE_URL=... node scripts/...
const SITE_URL = (process.env.SITE_URL ?? "https://hidden11.app").replace(/\/$/, "");
const KEY = process.env.INDEXNOW_KEY;
const ENDPOINT = "https://api.indexnow.org/indexnow";
if (!KEY) {
console.error("INDEXNOW_KEY is required (see pass show hidden11/indexnow-key)");
process.exit(1);
}
async function sitemapUrls() {
const response = await fetch(`${SITE_URL}/sitemap.xml`);
if (!response.ok) {
throw new Error(`sitemap.xml responded ${response.status}`);
}
const xml = await response.text();
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => match[1]);
}
async function main() {
const all = await sitemapUrls();
// The daily run only needs the surfaces that changed overnight: the home and
// archive pages, plus the answer pages for the day that just became public.
const puzzleNumber = (url) => {
const match = url.match(/\/puzzle\/(\d+)$/);
return match ? Number(match[1]) : null;
};
const newest = Math.max(0, ...all.map(puzzleNumber).filter((value) => value !== null));
const urls = process.argv.includes("--today")
? all.filter((url) => {
const number = puzzleNumber(url);
return number === null || number === newest;
})
: all;
if (urls.length === 0) {
console.log("nothing to submit");
return;
}
// IndexNow caps a batch at 10 000 URLs.
const batches = [];
for (let index = 0; index < urls.length; index += 10000) {
batches.push(urls.slice(index, index + 10000));
}
for (const [index, batch] of batches.entries()) {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({
host: new URL(SITE_URL).host,
key: KEY,
keyLocation: `${SITE_URL}/${KEY}.txt`,
urlList: batch,
}),
});
// 200 = accepted, 202 = accepted but key still being validated.
console.log(`batch ${index + 1}/${batches.length}: ${batch.length} urls → ${response.status}`);
if (!response.ok && response.status !== 202) {
console.error(await response.text());
process.exitCode = 1;
}
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});