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:13 +03:00
4 changed files with 147 additions and 1 deletions
Showing only changes of commit 291d5a5a98 - Show all commits

View File

@@ -48,3 +48,14 @@ jobs:
curl -fsS -o /dev/null -w "HEALTH HTTP %{http_code}\n" "$HEALTH_URL" 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 docker image prune -af --filter "until=168h" >/dev/null 2>&1 || true
REMOTE 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

View File

@@ -11,7 +11,8 @@
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"migrate": "node scripts/migrate-wp.mjs" "migrate": "node scripts/migrate-wp.mjs",
"indexnow": "node scripts/indexnow.js"
}, },
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.12",

View File

@@ -0,0 +1 @@
15455d9f2c7b473bb04336055b792ec9

133
scripts/indexnow.js Normal file
View File

@@ -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/<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'),
]);