blogging23.9 КБ
Astro вместо Ghost: переезд блога и грабли статического хостинга
Почему я не хотел оставлять публичный блог на домашнем интернете, как переехал с Ghost на Astro и на какие грабли наступил в VK Object Storage и CDN.
проект: oh-my-portal
В 2021 году я выбирал платформу для блога1 и остановился на Ghost. Пять лет он честно работал. Но публичный блог должен быть доступен всегда, а мой зависел от домашнего электричества, интернета, Kubernetes, Ghost и MySQL. После очередного восстановления базы стало понятно: новый блог дома я оставлять не хочу.
Теперь этот блог собирается на Astro и лежит в VK Object Storage за CDN. Движок oh-my-portal2 почти целиком написали агенты, а я ставил задачи, спорил и смотрел на скриншоты. Зачем мне блог, в который пишет агент и который удобно читать чужим агентам, я уже рассказывал в статье про agent-first3. Здесь про другое: как выбирался движок, как переезжали старые адреса и что сломалось, когда сборка уже была зелёной.
Почему я ушёл с Ghost
Ghost жил в моём домашнем Kubernetes-кластере, данные лежали в MySQL. Резервная копия делалась каждый день, и пригодилась она не один раз.
После отключений электричества или других сбоев база периодически повреждалась. Контейнер Ghost просто не стартовал. Я брал вчерашний бэкап, восстанавливал базу и ждал, пока всё поднимется. Да и обычный запуск был небыстрым.
Но дело было даже не в количестве сервисов. Я хотел, чтобы блог был доступен всегда, а домашний интернет такой надёжности не обеспечивает. Любая проблема со светом, провайдером или кластером забирала с собой и публичный сайт. Поэтому новый блог я уже не хотел оставлять дома.
Тема оформления, которой я пользовался, тоже перестала обновляться. Можно было искать новую, чинить старую и дальше поднимать тот же стек. Но вести блог мне хотелось, а обслуживать Ghost — уже нет. Была и вторая причина: я хотел, чтобы статьи лежали в Markdown в git и агент мог править их обычным коммитом. У Ghost статьи живут в базе, в формате его редактора, и тут он мне уже ничем помочь не мог.
Hugo, Astro и движки, которых не существует
Первым кандидатом был Hugo. Он быстрый, ставится одним бинарником, старые адреса закрываются полем aliases прямо во frontmatter. Звучало разумно, но я всё-таки спросил: «Почему именно hugo? Есть другие варианты? Новые и современные для AI?» И главный довод за Hugo, скорость, перестал быть решающим. Оба варианта собирали мой блог быстро, а движок я собирался активно дописывать.
В итоге я остановился на Astro, и вот почему:
- Content Collections проверяют frontmatter схемой на Zod прямо во время сборки;
- любая страница или файл вроде
llms.txt— это обычный код вsrc/pages, с шаблонизатором бороться не нужно; - после
astro buildостаётся просто папка с файлами, которой не нужен сервер.
Самым важным оказалась схема. Она строгая: неизвестное поле, кривая дата или ссылка на несуществующий проект роняют сборку. Для блога, в который пишет агент, это предохранитель. Агент положил файл с ошибкой — сборка упала, а на сайте осталась прежняя рабочая версия.
Перед окончательным решением я ещё раз поискал альтернативы и принёс из поиска подборку «движков нового поколения, созданных специально для ИИ-агентов». Выглядело очень убедительно, я даже засомневался. Попросил агента проверить. Спойлер: Astro остался. Для двух движков из трёх я вообще не нашёл открытого репозитория — похоже, подборку писала модель и часть просто досочинила. Третий оказался проектом одного автора с парой десятков звёзд. А главный довод против Astro, что он «генерирует только HTML», был просто неверным.
Считать нужно адреса
Импортёр из Ghost написал агент: выгрузить данные, скачать картинки, превратить публикации в Markdown. Так переехали 21 статья и 36 картинок.
Первая полезная ошибка была в подсчёте. По главной странице были видны только адреса статей. А в sitemap их оказалось 49: кроме статей там были страницы, теги и страница автора. Проверь я миграцию по главной, остальные адреса молча превратились бы в 404.
Раз уж всё равно переезжаю, я заодно навёл порядок: английские слаги вместо транслита вроде sovriemiennaia-zashchita-domashniei-laboratorii и шесть тегов вместо двадцати с лишним. Агент поначалу предлагал сохранить старые адреса как есть, но готовую схему решили не тянуть: для новых адресов сделали свою, а на старые поставили редиректы. Менять адреса дёшево только во время переезда. Если оставить сейчас, второй переезд адресов потом уже никто не захочет устраивать.
Тут же всплыло ограничение Astro. Его штатные redirects в статической сборке превращаются в HTML-страницы с meta refresh, а настоящий HTTP-статус должен выставить адаптер или сам хостинг.4 Мне для старых ссылок нужен был настоящий 301 в один переход, так что карта старых адресов стала входными данными для деплоя. Страниц-переходников у меня нет.
Вторую ошибку нашла строгая схема. Она требует у каждой статьи короткое описание, а ручное описание в Ghost было только у семи. Остальные импортёр пометил черновиками, и тестовый сайт честно показал семь статей. Пришлось заново написать недостающие описания: старые делались под очень маленький предпросмотр Ghost.
Куда положить папку с файлами
Первой мыслью было вернуть новый блог в тот же домашний кластер: технически это самый простой путь. Агент тут же увлёкся Docker-образами, пока я не вернул разговор к данным статей. Да и так я убрал бы MySQL и Ghost, а главную проблему оставил на месте: публичный сайт по-прежнему зависел бы от бытового провайдера и от того, что происходит дома.
Следующей была Timeweb App Platform. Сначала я решил, что там придётся собирать Docker-образ и настраивать ещё один сервис. Это было неверно: frontend-приложение подключается прямо из Git-репозитория, а команду и каталог сборки платформа предлагает сама.5 Отказался я по другой причине. Платформа умеет больше, чем нужно моему блогу, и тарифицирует входящие запросы, включая загрузку стилей, скриптов и картинок.6 А у меня после сборки остаётся папка с файлами. Зачем мне для неё платформа приложений?
А потом в поиске нашлась документация VK Cloud, и у них оказался пример именно для Astro: собрать dist, загрузить в Object Storage и раздавать через CDN.7 В настройках бакета есть переключатель «Использовать CDN для данного бакета», но я его не включал. CDN создаётся отдельно: сначала группа источников с website-адресом бакета, потом CDN-ресурс с этой группой. Хостинг статических сайтов сам подставляет index.html, показывает страницу ошибки и умеет правила перенаправлений.8 Платишь за хранение и исходящий трафик, входящий не тарифицируется.9

Что сломалось уже на настоящем домене
Перед запуском агент устроил плану стресс-тест и начал один за другим приносить спорные места. В какой-то момент план превратился в отдельный проект, и я его остановил. А самые неприятные сюрпризы всё равно ждали на живом домене.
Листинг бакета вместо главной
Переключил DNS — и публичный домен вместо главной показал XML-листинг бакета. Сборка зелёная, файлы в бакете, а читатель видит XML. Сначала грешили на источник CDN, потом на кеш, но ответ шёл мимо кеша и всё равно был листингом. Настоящая причина нашлась в заголовке Host. У бакета два адреса, обычный API и website, и сайт отдаётся, только если CDN приходит к источнику с website-адресом в Host. Одна настройка в консоли CDN — и главная наконец отдала HTML.
Старый HTML ссылается на уже удалённый CSS
В первой версии деплой синхронизировал бакет со сборкой и удалял всё лишнее. Имена CSS и JavaScript у Astro содержат хэш, так что после новой сборки старый файл выглядит ненужным.
Но CDN ещё держал в кеше старый HTML со ссылкой на старый CSS. Деплой удалял файл, читатель получал страницу без стилей, а сам CSS отвечал 403. Теперь у страниц короткий кеш, у _astro/ — месяц, а старые хэшированные файлы деплой просто не трогает. Стоит это несколько килобайт на деплой.
Редиректы тоже стали файлами
В бакете нет nginx, который прочитает карту старых адресов. Поэтому деплой создаёт пустые объекты с метаданными x-amz-website-redirect-location, и старый адрес отвечает настоящим 301. Загружаются они до удаления устаревших файлов и в само удаление не попадают. Иначе каждый деплой открывал бы окно, в котором старые ссылки отвечали бы ошибкой.
404.html не гарантирует код 404
VK Cloud позволяет назначить страницу ошибки, и документация описывает именно сценарий с несуществующей страницей и кодом 404.8 На деле тело нашей 404-й страницы показывается, но запрос к несуществующему адресу отвечает 403. Где именно он возникает — в правах Object Storage, поведении S3 или на CDN, — я пока не разобрался. Посетитель разницы не заметит, а вот поисковому роботу код ответа важнее оформления. Это вопрос уже к поддержке VK Cloud, так что проблема пока открыта.
Грабли самого Astro
Не всё было про хостинг. Пара вещей в самом Astro тоже стоила времени.
astro preview отдаёт то, что уже собрано. Он показывает содержимое dist и ничего не пересобирает. В какой-то момент правки уже были, а новой сборки ещё не было — поэтому в браузере действительно ничего не менялось.
Ошибка в плагине Markdown может молча дать пустые статьи. Astro 7 рендерит Markdown через Sätteri, и старые rehype-плагины там не работают. Первая версия нашего плагина для внешних ссылок была написана не в том формате. Sätteri падал на каждом документе, а Astro выдавал статьи с пустым телом — и ни слова в логе сборки. Спасли тесты, которые проверяют, что в собранной странице действительно есть текст статьи.
localhost бывает только IPv6. Dev-сервер слушал ::1, и SSH-туннель на 127.0.0.1 получал отказ, хотя в консоли было написано localhost:4321 и всё выглядело исправным. Лечится явным host: '127.0.0.1' в конфиге.
Что в итоге
Теперь публикация — это правка Markdown и merge. Дальше GitHub Actions собирает сайт и загружает файлы. Старые адреса отвечают 301, RSS живёт там же, где и жил, а для смены темы оформления не нужно менять CMS. Отключение света дома больше не мешает открыть старую статью.
На момент, когда я дописываю эту статью, за последние 48 часов CDN отдал 110.88 МБ общего трафика. Это уже не тестовая сборка: читатели приходят на обычный адрес, а CDN раздаёт им страницы и картинки. И работает блог теперь очень быстро.
Если совсем коротко, у меня получилось пять выводов:
- Публичный блог, который должен быть доступен всегда, не должен зависеть от домашнего интернета и электричества.
- Строгая схема контента оказалась важнее скорости сборки: именно она поймала проблемы миграции.
- Переезд нужно проверять по sitemap, а не по списку статей на главной.
- Статический хостинг убирает сервер, но не редиректы, кеши, заголовки и коды ответов.
- Зелёной сборки мало — проверять нужно собранную страницу через настоящий домен и CDN.
Не всё закрыто: тело страницы для несуществующего адреса показывается правильное, но код ответа всё ещё 403. Где он возникает — в правах Object Storage, поведении S3 или на CDN, — я пока не разобрался. С этим ещё нужно идти в поддержку VK Cloud.
Старый Ghost пока работает дома как запасной вариант: если что, откат — это одна правка DNS. Но теперь от него не зависит публичный сайт. Наконец можно не восстанавливать CMS, а писать. Следующий шаг — дать писать в этот блог моему агенту. Но это уже другая история.
Источники
-
Routing — Astro Docs — «When running
astro build, Astro will output HTML files with the meta refresh tag by default. Supported adapters will instead write out the host’s configuration file with the redirects.» ↩ -
Деплой frontend-приложений — Timeweb Cloud — «Сервис автоматически предложит команду и директорию сборки для вашего проекта». ↩
-
Тарификация фронтенд-приложений — Timeweb Cloud — «Учитываются в том числе запросы на загрузку статических файлов, таких как стили, скрипты, изображения и другие ресурсы». ↩
-
Развертывание Astro-сайта на объектном хранилище с CDN — VK Cloud — «С помощью VK Object Storage и CDN можно развернуть статический сайт на фреймворке Astro с глобальной доставкой контента и низкой задержкой». ↩
-
Хостинг статических сайтов — VK Cloud — «Хостинг статических сайтов позволяет комбинировать статический контент с правилами перенаправления, а также задавать индексную страницу (например,
index.html) и страницу ошибки, возвращаемую при ошибках запроса (например,404)». ↩ ↩2 -
Тарификация VK Object Storage — «В Hotbox и Icebox тарифицируется: […] Объем хранимых данных (в ГБ). […] Исходящий трафик (в ГБ)»; «Не тарифицируется: […] Входящий трафик». ↩