feat(seo): discoverability + Schema.org + IndexNow + Trivy #1

Merged
striker merged 3 commits from feat/seo-discoverability-and-indexnow into main 2026-05-21 14:25:15 +03:00
4 changed files with 134 additions and 1 deletions
Showing only changes of commit 5dca709ff2 - Show all commits

View File

@@ -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

View File

@@ -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",

View File

@@ -0,0 +1 @@
9018cf11050b4f379b8cec01ae3239bb

120
scripts/indexnow.js Normal file
View File

@@ -0,0 +1,120 @@
// IndexNow: уведомить Yandex/Bing о новых/обновлённых URL.
// Запускается после деплоя из CI или вручную:
// node scripts/indexnow.js
//
// IndexNow ключ хранится в файле public/<key>.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>([^<]+)<\/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'),
]);