134 lines
4.1 KiB
JavaScript
134 lines
4.1 KiB
JavaScript
// IndexNow: уведомить Yandex/Bing о новых/обновлённых URL.
|
||
// Запускается после деплоя из CI или вручную:
|
||
// node scripts/indexnow.js
|
||
//
|
||
// Источник URL — sitemap. Сначала пробуем локальный dist/sitemap*.xml
|
||
// (если запускаем сразу после `npm run build`), иначе тянем
|
||
// https://anotherreflections.ru/sitemap-index.xml через сеть.
|
||
//
|
||
// IndexNow ключ хранится в файле public/<key>.txt — он должен быть доступен
|
||
// по тому же URL что и сайт, чтобы поисковики могли подтвердить ownership.
|
||
|
||
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://anotherreflections.ru';
|
||
const HOST = 'anotherreflections.ru';
|
||
const KEY = '15455d9f2c7b473bb04336055b792ec9'; // 32-char hex
|
||
|
||
// Создать key-файл если кто-то его удалил.
|
||
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>([^<]+)<\/loc>/g)].map((m) => m[1]);
|
||
}
|
||
|
||
async function fetchText(url) {
|
||
const r = await fetch(url);
|
||
if (!r.ok) throw new Error(`HTTP ${r.status} for ${url}`);
|
||
return r.text();
|
||
}
|
||
|
||
async function collectFromRemoteIndex(indexUrl) {
|
||
const indexXml = await fetchText(indexUrl);
|
||
const locs = extractLocs(indexXml);
|
||
// Если это sitemap-index — locs указывают на дочерние sitemap-N.xml.
|
||
// Если это обычный sitemap — locs уже URL'ы страниц.
|
||
const looksLikeIndex = /<sitemapindex/i.test(indexXml);
|
||
if (!looksLikeIndex) return locs;
|
||
const childUrls = [];
|
||
for (const loc of locs) {
|
||
try {
|
||
const childXml = await fetchText(loc);
|
||
childUrls.push(...extractLocs(childXml));
|
||
} catch (e) {
|
||
console.error(` skip ${loc}: ${e.message}`);
|
||
}
|
||
}
|
||
return childUrls;
|
||
}
|
||
|
||
function collectFromLocalDist() {
|
||
const distDir = path.join(root, 'dist');
|
||
if (!fs.existsSync(distDir)) return null;
|
||
const sitemapFiles = fs
|
||
.readdirSync(distDir)
|
||
.filter((f) => /^sitemap-\d+\.xml$/.test(f));
|
||
let urls = [];
|
||
if (sitemapFiles.length > 0) {
|
||
for (const f of sitemapFiles) {
|
||
const xml = fs.readFileSync(path.join(distDir, f), 'utf-8');
|
||
urls.push(...extractLocs(xml));
|
||
}
|
||
return urls;
|
||
}
|
||
for (const c of ['sitemap-index.xml', 'sitemap.xml']) {
|
||
const p = path.join(distDir, c);
|
||
if (fs.existsSync(p)) {
|
||
const xml = fs.readFileSync(p, 'utf-8');
|
||
return extractLocs(xml);
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
let urls = collectFromLocalDist();
|
||
if (urls === null || urls.length === 0) {
|
||
console.log('no local dist/sitemap*.xml — fetching remote sitemap-index.xml');
|
||
try {
|
||
urls = await collectFromRemoteIndex(`${BASE}/sitemap-index.xml`);
|
||
} catch (e) {
|
||
console.error(`failed to fetch remote sitemap: ${e.message}`);
|
||
process.exit(0);
|
||
}
|
||
}
|
||
|
||
// Уникальные URL только нашего хоста.
|
||
urls = [...new Set(urls)].filter((u) => {
|
||
try {
|
||
return new URL(u).host === HOST;
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
|
||
if (urls.length === 0) {
|
||
console.error('no URLs found; nothing to submit');
|
||
process.exit(0);
|
||
}
|
||
|
||
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'),
|
||
]);
|