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

Путь публикации: из Git через Astro build в Object Storage, затем через CDN к читателю

Что сломалось уже на настоящем домене

Перед запуском агент устроил плану стресс-тест и начал один за другим приносить спорные места. В какой-то момент план превратился в отдельный проект, и я его остановил. А самые неприятные сюрпризы всё равно ждали на живом домене.

Листинг бакета вместо главной

Переключил 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 раздаёт им страницы и картинки. И работает блог теперь очень быстро.

Если совсем коротко, у меня получилось пять выводов:

  1. Публичный блог, который должен быть доступен всегда, не должен зависеть от домашнего интернета и электричества.
  2. Строгая схема контента оказалась важнее скорости сборки: именно она поймала проблемы миграции.
  3. Переезд нужно проверять по sitemap, а не по списку статей на главной.
  4. Статический хостинг убирает сервер, но не редиректы, кеши, заголовки и коды ответов.
  5. Зелёной сборки мало — проверять нужно собранную страницу через настоящий домен и CDN.

Не всё закрыто: тело страницы для несуществующего адреса показывается правильное, но код ответа всё ещё 403. Где он возникает — в правах Object Storage, поведении S3 или на CDN, — я пока не разобрался. С этим ещё нужно идти в поддержку VK Cloud.

Старый Ghost пока работает дома как запасной вариант: если что, откат — это одна правка DNS. Но теперь от него не зависит публичный сайт. Наконец можно не восстанавливать CMS, а писать. Следующий шаг — дать писать в этот блог моему агенту. Но это уже другая история.

Источники

  1. Муки выбора платформы для блога

  2. oh-my-portal — страница проекта, исходники на GitHub

  3. Как я строил agent-first REST API и agent-first блог

  4. 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.»

  5. Деплой frontend-приложений — Timeweb Cloud — «Сервис автоматически предложит команду и директорию сборки для вашего проекта».

  6. Тарификация фронтенд-приложений — Timeweb Cloud — «Учитываются в том числе запросы на загрузку статических файлов, таких как стили, скрипты, изображения и другие ресурсы».

  7. Развертывание Astro-сайта на объектном хранилище с CDN — VK Cloud — «С помощью VK Object Storage и CDN можно развернуть статический сайт на фреймворке Astro с глобальной доставкой контента и низкой задержкой».

  8. Хостинг статических сайтов — VK Cloud — «Хостинг статических сайтов позволяет комбинировать статический контент с правилами перенаправления, а также задавать индексную страницу (например, index.html) и страницу ошибки, возвращаемую при ошибках запроса (например, 404)». 2

  9. Тарификация VK Object Storage — «В Hotbox и Icebox тарифицируется: […] Объем хранимых данных (в ГБ). […] Исходящий трафик (в ГБ)»; «Не тарифицируется: […] Входящий трафик».

похожие записи

// ближайшие соседи · cosine 0–1

  1. [0]0.839
    Муки выбора платформы для блога

    Прежде чем писать свой движок, я перебрал Notion через fruitionsite, генераторы статических сайтов и WordPress и остановился на Ghost.

  2. [1]0.796
    Сбор логов из подов Kubernetes в домашней лаборатории

    Централизованные логи домашнего кластера на VictoriaLogs и Vector: почему не ELK, Loki или Graylog, установка из Helm и сколько это съедает ресурсов.

  3. [2]0.789
    Оказалось, что после создания блога, там еще и писать нужно

    Блог наконец появился, а писать оказалось не о чем, поэтому здесь сразу несколько находок, от пермского корпуса DNK-H до домашней коптильни.

cosine embeddings · шкала 0–1 · вычислено при сборке

Набросок

Открыть оригинал