diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 15673f2..350d1ad 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 @@ -22,6 +25,23 @@ jobs: printf '%s\n' "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts + - name: Build image for security scan + run: | + docker build -t pushkinohistory-ru-v2:scan . + + - name: Trivy image scan (HIGH+CRITICAL, warning only) + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD":/workspace \ + ghcr.io/aquasecurity/trivy:latest \ + image \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --no-progress \ + --exit-code 0 \ + pushkinohistory-ru-v2:scan + - name: Deploy to web.hhivp.com run: | ssh -i ~/.ssh/id_deploy striker@web.hhivp.com bash -s <<'REMOTE' @@ -49,3 +69,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/.well-known/security.txt b/public/.well-known/security.txt new file mode 100644 index 0000000..739e7a8 --- /dev/null +++ b/public/.well-known/security.txt @@ -0,0 +1,6 @@ +Contact: mailto:admin@pushkinohistory.ru +Contact: https://pushkinohistory.ru/privacy/ +Expires: 2027-05-21T00:00:00.000Z +Preferred-Languages: ru, en +Canonical: https://pushkinohistory.ru/.well-known/security.txt +Policy: https://pushkinohistory.ru/privacy/ 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/public/humans.txt b/public/humans.txt new file mode 100644 index 0000000..3ac7c84 --- /dev/null +++ b/public/humans.txt @@ -0,0 +1,10 @@ +/* SITE */ +Site: История города Пушкино — pushkinohistory.ru +Last update: 2026-05-21 +Standards: HTML5, CSS3, RSS 2.0 +Components: Astro 6, nginx 1.29 +Software: Docker, Gitea Actions + +/* TEAM */ +Maintained by: HHIVP +Contact: admin@pushkinohistory.ru 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'), +]); diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 08e2c16..bf84665 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -26,6 +26,33 @@ const { const fullTitle = title ? `${title} — ${SITE_TITLE}` : SITE_TITLE; const url = canonical ?? new URL(Astro.url.pathname, SITE_URL).toString(); const ogImageUrl = new URL(ogImage, SITE_URL).toString(); + +const jsonLd = { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'WebSite', + '@id': `${SITE_URL}/#website`, + url: `${SITE_URL}/`, + name: SITE_TITLE, + description: SITE_DESCRIPTION, + inLanguage: 'ru-RU', + publisher: { '@id': `${SITE_URL}/#publisher` }, + }, + { + '@type': 'NewsMediaOrganization', + '@id': `${SITE_URL}/#publisher`, + name: SITE_TITLE, + url: `${SITE_URL}/`, + logo: { + '@type': 'ImageObject', + url: `${SITE_URL}/favicon.svg`, + }, + description: + 'Краеведческое медиа: история, новости и фотолетопись подмосковного города Пушкино — от давних времён до наших дней.', + }, + ], +}; --- @@ -48,6 +75,8 @@ const ogImageUrl = new URL(ogImage, SITE_URL).toString(); +