Compare commits
3 Commits
cf17c6e432
...
bafd42b774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bafd42b774 | ||
|
|
5dca709ff2 | ||
|
|
a34a0c6093 |
@@ -9,6 +9,9 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install SSH client
|
- name: Install SSH client
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -22,6 +25,23 @@ jobs:
|
|||||||
printf '%s\n' "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
printf '%s\n' "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
chmod 644 ~/.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
|
- name: Deploy to web.hhivp.com
|
||||||
run: |
|
run: |
|
||||||
ssh -i ~/.ssh/id_deploy striker@web.hhivp.com bash -s <<'REMOTE'
|
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"
|
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: Setup Node.js for IndexNow
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Notify IndexNow
|
||||||
|
run: npm run indexnow || true
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"indexnow": "node scripts/indexnow.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/rss": "^4.0.12",
|
"@astrojs/rss": "^4.0.12",
|
||||||
|
|||||||
6
public/.well-known/security.txt
Normal file
6
public/.well-known/security.txt
Normal file
@@ -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/
|
||||||
1
public/9018cf11050b4f379b8cec01ae3239bb.txt
Normal file
1
public/9018cf11050b4f379b8cec01ae3239bb.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
9018cf11050b4f379b8cec01ae3239bb
|
||||||
10
public/humans.txt
Normal file
10
public/humans.txt
Normal file
@@ -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
|
||||||
120
scripts/indexnow.js
Normal file
120
scripts/indexnow.js
Normal 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'),
|
||||||
|
]);
|
||||||
@@ -26,6 +26,33 @@ const {
|
|||||||
const fullTitle = title ? `${title} — ${SITE_TITLE}` : SITE_TITLE;
|
const fullTitle = title ? `${title} — ${SITE_TITLE}` : SITE_TITLE;
|
||||||
const url = canonical ?? new URL(Astro.url.pathname, SITE_URL).toString();
|
const url = canonical ?? new URL(Astro.url.pathname, SITE_URL).toString();
|
||||||
const ogImageUrl = new URL(ogImage, 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:
|
||||||
|
'Краеведческое медиа: история, новости и фотолетопись подмосковного города Пушкино — от давних времён до наших дней.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -48,6 +75,8 @@ const ogImageUrl = new URL(ogImage, SITE_URL).toString();
|
|||||||
<meta property="og:site_name" content={SITE_TITLE} />
|
<meta property="og:site_name" content={SITE_TITLE} />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<script type="application/ld+json" is:inline set:html={JSON.stringify(jsonLd)} />
|
||||||
|
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user