diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index ba1c3f0..784975c 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -48,3 +48,14 @@ jobs: curl -fsS -o /dev/null -w "HEALTH HTTP %{http_code}\n" "$HEALTH_URL" docker image prune -af --filter "until=168h" >/dev/null 2>&1 || true REMOTE + + - name: Checkout repo for IndexNow + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Notify IndexNow (Yandex/Bing) + run: npm run indexnow || true diff --git a/package.json b/package.json index 3edabc2..ee4b0a9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "migrate": "node scripts/migrate-wp.mjs" + "migrate": "node scripts/migrate-wp.mjs", + "indexnow": "node scripts/indexnow.js" }, "dependencies": { "@astrojs/rss": "^4.0.12", diff --git a/public/15455d9f2c7b473bb04336055b792ec9.txt b/public/15455d9f2c7b473bb04336055b792ec9.txt new file mode 100644 index 0000000..efd306f --- /dev/null +++ b/public/15455d9f2c7b473bb04336055b792ec9.txt @@ -0,0 +1 @@ +15455d9f2c7b473bb04336055b792ec9 \ No newline at end of file diff --git a/scripts/indexnow.js b/scripts/indexnow.js new file mode 100644 index 0000000..792561e --- /dev/null +++ b/scripts/indexnow.js @@ -0,0 +1,133 @@ +// 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/.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>/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 = / /^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'), +]);