// IndexNow: уведомить Yandex/Bing о новых/обновлённых URL. // Запускается после деплоя из CI или вручную: // node scripts/indexnow.js // // IndexNow ключ хранится в файле public/.txt — он должен быть доступен по // тому же URL что и сайт, чтобы поисковики могли подтвердить ownership. // // Источник URL (по приоритету): // 1) dist/sitemap-index.xml (Astro @astrojs/sitemap) + входящие sitemap-*.xml // 2) dist/sitemap.txt / dist/sitemap.xml // 3) fetch с прод-сайта: https://pushkinohistory.ru/sitemap-index.xml // (для запуска из CI после удалённого деплоя — без локального build) import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const root = path.resolve(__dirname, '..'); const BASE = 'https://pushkinohistory.ru'; const HOST = 'pushkinohistory.ru'; const KEY = '9018cf11050b4f379b8cec01ae3239bb'; // На случай если кто-то удалит файл — пересоздадим автоматически. const keyFile = path.join(root, 'public', `${KEY}.txt`); if (!fs.existsSync(keyFile)) { fs.writeFileSync(keyFile, KEY, 'utf-8'); console.log(`created key file: public/${KEY}.txt`); } function extractLocs(xml) { return [...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1].trim()); } let urls = []; const distDir = path.join(root, 'dist'); const sitemapIndex = path.join(distDir, 'sitemap-index.xml'); const sitemapTxt = path.join(distDir, 'sitemap.txt'); const fallbackTxt = path.join(distDir, 'sitemap.xml'); if (fs.existsSync(sitemapIndex)) { const idx = fs.readFileSync(sitemapIndex, 'utf-8'); const subs = extractLocs(idx); for (const sub of subs) { // sub — абсолютный URL вида https://pushkinohistory.ru/sitemap-0.xml const fname = sub.split('/').pop(); const localPath = path.join(distDir, fname); if (fs.existsSync(localPath)) { const subXml = fs.readFileSync(localPath, 'utf-8'); urls.push(...extractLocs(subXml)); } else { console.warn(` sub-sitemap not found locally: ${localPath}`); } } } else if (fs.existsSync(sitemapTxt)) { urls = fs .readFileSync(sitemapTxt, 'utf-8') .split(/\r?\n/) .map((l) => l.trim()) .filter((l) => l.startsWith('http')); } else if (fs.existsSync(fallbackTxt)) { const xml = fs.readFileSync(fallbackTxt, 'utf-8'); urls = extractLocs(xml); } else { console.log('no local sitemap found, fetching from production…'); try { const idxResp = await fetch(`${BASE}/sitemap-index.xml`); if (!idxResp.ok) throw new Error(`HTTP ${idxResp.status}`); const idxXml = await idxResp.text(); const subs = extractLocs(idxXml); for (const sub of subs) { const subResp = await fetch(sub); if (!subResp.ok) { console.warn(` ${sub}: HTTP ${subResp.status}`); continue; } const subXml = await subResp.text(); urls.push(...extractLocs(subXml)); } } catch (e) { console.error(`failed to fetch sitemap from ${BASE}: ${e.message}`); process.exit(1); } } // Дедупликация и фильтрация на всякий случай. urls = [...new Set(urls)].filter((u) => u.startsWith(BASE)); if (urls.length === 0) { console.error('no URLs collected — nothing to submit'); process.exit(1); } console.log(`Submitting ${urls.length} URLs to IndexNow…`); const payload = { host: HOST, key: KEY, keyLocation: `${BASE}/${KEY}.txt`, urlList: urls, }; async function submit(endpoint) { try { const r = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(payload), }); console.log(` ${endpoint}: HTTP ${r.status}`); } catch (e) { console.error(` ${endpoint}: ${e.message}`); } } await Promise.all([ submit('https://yandex.com/indexnow'), submit('https://api.indexnow.org/indexnow'), ]);