From 5dca709ff240abdf22344a90071ef768e71c15b3 Mon Sep 17 00:00:00 2001 From: striker Date: Thu, 21 May 2026 13:41:52 +0300 Subject: [PATCH] feat(indexnow): add IndexNow key file and trigger script --- .gitea/workflows/deploy.yml | 11 ++ package.json | 3 +- public/9018cf11050b4f379b8cec01ae3239bb.txt | 1 + scripts/indexnow.js | 120 ++++++++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 public/9018cf11050b4f379b8cec01ae3239bb.txt create mode 100644 scripts/indexnow.js diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 15673f2..6260d0b 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -9,6 +9,9 @@ jobs: deploy: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install SSH client run: | apt-get update -qq @@ -49,3 +52,11 @@ 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: Setup Node.js for IndexNow + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Notify IndexNow + run: npm run indexnow || true diff --git a/package.json b/package.json index a40a75e..19ca3a9 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "indexnow": "node scripts/indexnow.js" }, "dependencies": { "@astrojs/rss": "^4.0.12", diff --git a/public/9018cf11050b4f379b8cec01ae3239bb.txt b/public/9018cf11050b4f379b8cec01ae3239bb.txt new file mode 100644 index 0000000..0d96912 --- /dev/null +++ b/public/9018cf11050b4f379b8cec01ae3239bb.txt @@ -0,0 +1 @@ +9018cf11050b4f379b8cec01ae3239bb diff --git a/scripts/indexnow.js b/scripts/indexnow.js new file mode 100644 index 0000000..73b5f56 --- /dev/null +++ b/scripts/indexnow.js @@ -0,0 +1,120 @@ +// 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'), +]);