|
| 1 | +import fs from "node:fs/promises"; |
| 2 | +import path from "node:path"; |
| 3 | +import { parseArgs } from "node:util"; |
| 4 | + |
| 5 | +const DEFAULT_HOST = "www.javajub.com"; |
| 6 | +const DEFAULT_ENDPOINT = "https://yandex.com/indexnow"; |
| 7 | +const DEFAULT_KEY = "e83be258c353fa1282249ebe3e69ab3595ad9667f969b329449721fdcaab3b8b"; |
| 8 | +const EXCLUDED_PATHS = new Set(["/404/", "/404.html"]); |
| 9 | + |
| 10 | +const { values } = parseArgs({ |
| 11 | + options: { |
| 12 | + "dry-run": { type: "boolean", default: false }, |
| 13 | + endpoint: { type: "string", default: DEFAULT_ENDPOINT }, |
| 14 | + host: { type: "string", default: process.env.INDEXNOW_HOST || DEFAULT_HOST }, |
| 15 | + key: { type: "string", default: process.env.INDEXNOW_KEY || DEFAULT_KEY }, |
| 16 | + "key-location": { type: "string" }, |
| 17 | + limit: { type: "string" }, |
| 18 | + sitemap: { type: "string" }, |
| 19 | + }, |
| 20 | +}); |
| 21 | + |
| 22 | +const host = values.host.replace(/^https?:\/\//, "").replace(/\/$/, ""); |
| 23 | +const siteUrl = `https://${host}`; |
| 24 | +const key = values.key; |
| 25 | +const keyLocation = values["key-location"] || `${siteUrl}/${key}.txt`; |
| 26 | +const sitemapLocation = values.sitemap || `${siteUrl}/sitemap.xml`; |
| 27 | +const limit = values.limit ? Number.parseInt(values.limit, 10) : undefined; |
| 28 | + |
| 29 | +if (!/^[a-z0-9_-]{8,128}$/i.test(key)) { |
| 30 | + throw new Error("IndexNow key must be 8-128 URL-safe characters."); |
| 31 | +} |
| 32 | + |
| 33 | +if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) { |
| 34 | + throw new Error("--limit must be a positive integer."); |
| 35 | +} |
| 36 | + |
| 37 | +async function readText(location) { |
| 38 | + if (/^https?:\/\//i.test(location)) { |
| 39 | + const response = await fetch(location, { |
| 40 | + headers: { |
| 41 | + "User-Agent": "JavaJub-IndexNow/1.0", |
| 42 | + }, |
| 43 | + }); |
| 44 | + if (!response.ok) { |
| 45 | + throw new Error(`Failed to fetch ${location}: ${response.status} ${response.statusText}`); |
| 46 | + } |
| 47 | + return response.text(); |
| 48 | + } |
| 49 | + |
| 50 | + return fs.readFile(path.resolve(location), "utf8"); |
| 51 | +} |
| 52 | + |
| 53 | +function decodeXml(value) { |
| 54 | + return value |
| 55 | + .replaceAll("&", "&") |
| 56 | + .replaceAll("<", "<") |
| 57 | + .replaceAll(">", ">") |
| 58 | + .replaceAll(""", '"') |
| 59 | + .replaceAll("'", "'"); |
| 60 | +} |
| 61 | + |
| 62 | +function extractUrls(sitemapXml) { |
| 63 | + const urls = [...sitemapXml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)] |
| 64 | + .map((match) => decodeXml(match[1].trim())) |
| 65 | + .filter((url) => { |
| 66 | + try { |
| 67 | + const parsedUrl = new URL(url); |
| 68 | + return parsedUrl.host === host && !EXCLUDED_PATHS.has(parsedUrl.pathname); |
| 69 | + } catch { |
| 70 | + return false; |
| 71 | + } |
| 72 | + }); |
| 73 | + |
| 74 | + return [...new Set(urls)].slice(0, limit); |
| 75 | +} |
| 76 | + |
| 77 | +async function submit(urlList) { |
| 78 | + const payload = { |
| 79 | + host, |
| 80 | + key, |
| 81 | + keyLocation, |
| 82 | + urlList, |
| 83 | + }; |
| 84 | + |
| 85 | + if (values["dry-run"]) { |
| 86 | + console.log(`IndexNow dry-run: ${urlList.length} URL(s) would be sent to ${values.endpoint}`); |
| 87 | + console.log(`Key location: ${keyLocation}`); |
| 88 | + console.log(urlList.slice(0, 20).join("\n")); |
| 89 | + if (urlList.length > 20) console.log(`...and ${urlList.length - 20} more`); |
| 90 | + return; |
| 91 | + } |
| 92 | + |
| 93 | + const response = await fetch(values.endpoint, { |
| 94 | + method: "POST", |
| 95 | + headers: { |
| 96 | + "Content-Type": "application/json; charset=utf-8", |
| 97 | + "User-Agent": "JavaJub-IndexNow/1.0", |
| 98 | + }, |
| 99 | + body: JSON.stringify(payload), |
| 100 | + }); |
| 101 | + |
| 102 | + const body = await response.text(); |
| 103 | + if (!response.ok) { |
| 104 | + throw new Error(`IndexNow request failed: ${response.status} ${response.statusText}\n${body}`); |
| 105 | + } |
| 106 | + |
| 107 | + console.log(`IndexNow submitted ${urlList.length} URL(s) to ${values.endpoint}`); |
| 108 | + if (body.trim()) console.log(body.trim()); |
| 109 | +} |
| 110 | + |
| 111 | +const sitemapXml = await readText(sitemapLocation); |
| 112 | +const urls = extractUrls(sitemapXml); |
| 113 | + |
| 114 | +if (urls.length === 0) { |
| 115 | + throw new Error(`No ${host} URLs found in ${sitemapLocation}`); |
| 116 | +} |
| 117 | + |
| 118 | +await submit(urls); |
0 commit comments