11 KiB
pushkinohistory.ru — Astro v2
Сайт «История города Пушкино». Редизайн с WordPress (v1) на статический Astro 5 + Content Collections + markdown.
Прод: https://pushkinohistory.ru
Репо: git.striker.su/striker/pushkinohistory-ru-v2
Хост: web.hhivp.com (45.10.53.206 / 45.10.53.242)
Контейнер: pushkinohistory-ru-v2 на 127.0.0.1:4146 (nginx:alpine + Astro SSG)
Cutover: 2026-05-21 со старого WP-контейнера pushkinohistory-ru:4143
Стек
- Astro 5 + Content Collections + markdown
- nginx:1.29-alpine в runtime-контейнере (статика + bind-mount для агрегатора новостей)
- sanitize-html — очистка тела поста для RSS
<content:encoded>(с CDATA) - fast-xml-parser — изолированно в
scripts/(только для cron-агрегатора) - sharp (devDep, опц.) — генерация OG-image PNG из SVG
- PT Serif (заголовки/тело статьи) + IBM Plex Sans (UI)
- @astrojs/sitemap —
sitemap-index.xmlавтоматически
Структура
src/
├── content/
│ ├── posts/*.md (7 постов, мигрированы из WP DB ph_posts)
│ ├── pages/*.md (4 страницы: Главная-приветствие, История, Фото, Форум)
│ └── _categories.json (через categorySlugs в frontmatter)
├── components/
│ ├── Header.astro (фото-шапка + аэрофото Пушкино + sepia overlay)
│ ├── Sidebar.astro (Транспорт, Рубрики с подсчётом, Партнёры, Объявления)
│ ├── Footer.astro
│ ├── PostCard.astro (featured / has-thumb / no-thumb варианты)
│ ├── CookieConsent.astro (152-ФЗ баннер, ph-consent в localStorage+cookie)
│ └── Analytics.astro (Яндекс.Метрика + GA, gating через type=text/plain)
├── layouts/BaseLayout.astro
├── pages/
│ ├── index.astro (Главная история + Ещё из истории + Хроника)
│ ├── [slug].astro (один пост или одна страница)
│ ├── cat/[slug].astro (рубрика)
│ ├── cat/[slug]/feed.xml.ts (per-category RSS)
│ ├── news.astro (агрегатор внешних RSS, фетчит /api/news.json)
│ ├── 404.astro
│ ├── privacy.astro (политика + кнопка «Отозвать согласие»)
│ ├── feed.xml.ts (общий RSS, с RSS_CUTOFF)
│ └── sitemap.txt.ts (plain-text карта)
├── lib/
│ ├── extract.ts (firstImage, plainText, formatDateRu)
│ └── rss-helpers.ts (buildRss, sanitizeForRss, cdata, plainTextExcerpt)
├── data/ (внешний контент-конфиг, JSON)
│ ├── transport.json (ссылки на Yandex.Schedules / mostransport)
│ ├── partners.json
│ ├── ads.json
│ └── feeds.json (внешние RSS-источники для cron-агрегатора)
├── styles/global.css
└── consts.ts (SITE_TITLE, MAIN_NAV, RSS_CUTOFF, ANALYTICS, plural)
public/
├── uploads/ (6 картинок, перенесены из WP /wp-content/uploads/)
├── favicon.svg
├── robots.txt
├── ai.txt
└── llms.txt
nginx/pushkinohistory.ru.conf (vhost для хост-nginx, симлинкуется в /etc/nginx/conf.d/)
scripts/
├── convert_posts.py (WP DB → src/content/posts.json + pages.json, fix WP-resized URL)
├── convert_to_markdown.py (posts.json → src/content/posts/*.md с frontmatter)
├── pull-external-rss.mjs (cron на хосте: feeds.json → data/news.json)
├── install-cron.sh (установка cron на web.hhivp.com)
└── package.json (изолированный fast-xml-parser)
Dockerfile (multi-stage: node:22-alpine build → nginx:1.29-alpine serve)
nginx.conf (внутри контейнера: gzip, кэш _astro/, MIME для RSS, /api/news.json из bind-mount)
docker-compose.yml (контейнер на 127.0.0.1:4146, bind-mount data/ → /var/lib/pushkino/data:ro)
.gitea/workflows/deploy.yml (push в main → SSH-деплой на web.hhivp.com)
Контент
- 7 постов + 4 страницы скрейпом из WP DB
pushkinohistory_ruнаdb.hhivp.com - URL-encoded кириллические slug'и WP → транслитерированы (
/segodnya-nochyu-rossiyane-uvidyat-pervoe/); старые URL → 301 через nginx map по$uri - WP-resized URL (
-1024x768.png) → оригинал автоматически вconvert_posts.py:RESIZED_RE - Frontmatter-флаги для иерархии главной:
featured: true+featuredImage— пин на верх как «Главная история» (Воронино, Старое Село)hideFromList: true— скрыть с главной (3 «технические работы»), доступ только через рубрику
- Категория
main— псевдо-флаг «попадает на главную»; не показывается в плашках и в сайдбаре
RSS
Свой /feed.xml (для IPB Importer)
- Полный HTML тела поста в
<content:encoded>с CDATA (sanitize-html → buildRss) - Стабильные
<guid isPermaLink="true">= URL поста (IPB дедуплицирует) <dc:creator>,<category>, корректный<pubDate>в RFC-822- Контент-Type
application/rss+xml; charset=utf-8 RSS_CUTOFFвsrc/consts.ts(default2010-01-01) — отрезает старый архив. Чтобы IPB не флудил при следующем рестарте — поменять на новую дату и пересобрать- Per-category RSS:
/cat/<slug>/feed.xml
Агрегатор внешних RSS
src/data/feeds.json— список источников (enabled: falseпо умолчанию)scripts/pull-external-rss.mjs— cron на web.hhivp.com (каждый час в:12)- Пишет
/opt/docker/sites/pushkinohistory-ru-v2/data/news.json(bind-mount в контейнер как/var/lib/pushkino/data:ro) - Фронт
/news/фетчит client-side через/api/news.json(отдаёт nginx внутри контейнера alias на bind-mount) - Логи
/var/log/pushkino-rss-aggregator.log+ logrotate weekly × 4
Чтобы добавить источник:
- В
src/data/feeds.jsonдобавить{name, url, enabled: true, max} - Push → CI → деплой
- На следующем cron-tick'е появится в
/news/
Деплой
Автоматический (Gitea Actions)
Push в main → .gitea/workflows/deploy.yml:
- SSH на
web.hhivp.comс ключом из секретаSSH_DEPLOY_KEY git fetch + reset --hard origin/maindocker compose build && up -dcurl -fsS http://127.0.0.1:4146/— health checkdocker image prune --filter "until=168h"
Секреты в Gitea (/repos/striker/pushkinohistory-ru-v2/actions/secrets):
SSH_DEPLOY_KEY— приватный ключ~/.ssh/pushkino-v2-deploy(генерён локально, pubkey вstriker@web:~/.ssh/authorized_keys)SSH_KNOWN_HOSTS— fingerprintweb.hhivp.com ssh-ed25519 AAAAC3...
Вручную
cd E:\Projects\pushkinohistory-ru-v2
git add . && git commit -m "..." && git push
# или с локалки на сервер:
ssh striker@web.hhivp.com 'cd /opt/docker/sites/pushkinohistory-ru-v2 && git pull && docker compose up -d --build'
nginx vhost
Симлинк: /etc/nginx/conf.d/pushkinohistory.ru → nginx/pushkinohistory.ru.conf в репо. После правки nginx-конфига: push → CI заберёт → sudo nginx -t && sudo systemctl reload nginx.
301-редиректы со старых WP-URL — через map $uri $legacy_redirect (см. файл). Если нужно добавить новый редирект — отредактировать map и pushнуть.
Откат на WP v1
Старый WP-контейнер pushkinohistory-ru:4143 + БД pushkinohistory_ru на db.hhivp.com сохранены. Откат за ~1 минуту:
ssh striker@web.hhivp.com
echo "Gh_lpx2017!" | sudo -S ln -sfn /opt/docker/sites/pushkinohistory-ru/nginx/pushkinohistory.ru.conf /etc/nginx/conf.d/pushkinohistory.ru
sudo nginx -t && sudo systemctl reload nginx
После 1-2 недель стабильной работы v2 — старый WP можно удалить (контейнер + БД + репо).
Форум
forum.pushkinohistory.ru — IPB 4.x в отдельном контейнере forum-pushkinohistory-ru:4144. v2 редизайном не затронут.
Аналитика
ANALYTICS в src/consts.ts (yandexMetrika, googleGtag) пустые — впишите ID после регистрации счётчиков. Скрипты в Analytics.astro имеют type="text/plain" data-cookieconsent="statistics" и активируются только после согласия в баннере CookieConsent.astro.
Согласие хранится в localStorage + cookie ph-consent (12 мес). На /privacy/ есть кнопка «Отозвать согласие».
Локальная разработка
npm install
npm run dev # → http://localhost:4321
npm run build # → dist/ (статика)
npm run preview
Если 4321 занят — Astro сам найдёт следующий свободный (4322 и т.д.).
SEO — правила для контента
Обязательно для каждого нового поста/страницы в src/content/{posts,pages}/*.md:
description:— уникальное, 120–160 символов. Не оставлять пустым — пустая строка не падает на дефолтSITE_DESCRIPTION, а передаётся в<meta>как есть.[slug].astroпередаётdescription={entry.data.description ?? ''}→ BaseLayout. Если''— meta пустой.
RSS-агрегатор новостей: источники добавляются в src/data/feeds.json ({name, url, enabled: true, max}). После push → CI → на следующем cron-тике (каждый час в :12) появятся на /news/.
История
- 2026-05-08: v1 (WordPress 6.x) контейнеризован, миграция со str-u-01 на web.hhivp.com
- 2026-05-14: фикс trust-proxy.conf для Docker bridges (Better WP Security)
- 2026-05-21: v2 редизайн — Vite+React → Astro 5 (стек как у
anotherreflections-website-v2). Cutover, бэкап старого WP в репоpushkinohistory-ru+ БД наdb.hhivp.com(~2 недели на наблюдение). - 2026-05-30: заполнены descriptions всем 11 страницам/постам (были пустыми → дубли в Я.Вебмастере).