---
title: "Astro вместо Ghost: переезд блога и грабли статического хостинга"
slug: ghost-to-astro-vk-cloud
canonical: https://shady2k.ru/posts/ghost-to-astro-vk-cloud/
date: 2026-09-13
kind: article
author: human
lang: ru
summary: Почему я не хотел оставлять публичный блог на домашнем интернете, как
  переехал с Ghost на Astro и на какие грабли наступил в VK Object Storage и
  CDN.
tags:
  - blogging
projects:
  - https://shady2k.ru/projects/oh-my-portal/
related:
  - https://shady2k.ru/posts/choosing-a-blog-platform/
  - https://shady2k.ru/posts/agent-first-api-and-blog/
---

В 2021 году я [выбирал платформу для блога](/posts/choosing-a-blog-platform/)[^1] и остановился на Ghost. Пять лет он честно работал. Но публичный блог должен быть доступен всегда, а мой зависел от домашнего электричества, интернета, Kubernetes, Ghost и MySQL. После очередного восстановления базы стало понятно: новый блог дома я оставлять не хочу.

Теперь этот блог собирается на Astro и лежит в VK Object Storage за CDN. Движок [oh-my-portal](/projects/oh-my-portal/)[^2] почти целиком написали агенты, а я ставил задачи, спорил и смотрел на скриншоты. Зачем мне блог, в который пишет агент и который удобно читать чужим агентам, я уже рассказывал в [статье про agent-first](/posts/agent-first-api-and-blog/)[^3]. Здесь про другое: как выбирался движок, как переезжали старые адреса и что сломалось, когда сборка уже была зелёной.

## Почему я ушёл с Ghost

Ghost жил в моём домашнем Kubernetes-кластере, данные лежали в MySQL. Резервная копия делалась каждый день, и пригодилась она не один раз.

После отключений электричества или других сбоев база периодически повреждалась. Контейнер Ghost просто не стартовал. Я брал вчерашний бэкап, восстанавливал базу и ждал, пока всё поднимется. Да и обычный запуск был небыстрым.

Но дело было даже не в количестве сервисов. Я хотел, чтобы блог был доступен всегда, а домашний интернет такой надёжности не обеспечивает. Любая проблема со светом, провайдером или кластером забирала с собой и публичный сайт. Поэтому новый блог я уже не хотел оставлять дома.

Тема оформления, которой я пользовался, тоже перестала обновляться. Можно было искать новую, чинить старую и дальше поднимать тот же стек. Но вести блог мне хотелось, а обслуживать Ghost — уже нет. Была и вторая причина: я хотел, чтобы статьи лежали в Markdown в git и агент мог править их обычным коммитом. У Ghost статьи живут в базе, в формате его редактора, и тут он мне уже ничем помочь не мог.

> [!TAKEAWAY]
> Если блог должен быть доступен всегда, не стоит привязывать его к домашнему электричеству и интернету.

## Hugo, Astro и движки, которых не существует

Первым кандидатом был Hugo. Он быстрый, ставится одним бинарником, старые адреса закрываются полем `aliases` прямо во frontmatter. Звучало разумно, но я всё-таки спросил: «Почему именно hugo? Есть другие варианты? Новые и современные для AI?» И главный довод за Hugo, скорость, перестал быть решающим. Оба варианта собирали мой блог быстро, а движок я собирался активно дописывать.

В итоге я остановился на Astro, и вот почему:

- Content Collections проверяют frontmatter схемой на Zod прямо во время сборки;
- любая страница или файл вроде `llms.txt` — это обычный код в `src/pages`, с шаблонизатором бороться не нужно;
- после `astro build` остаётся просто папка с файлами, которой не нужен сервер.

Самым важным оказалась схема. Она строгая: неизвестное поле, кривая дата или ссылка на несуществующий проект роняют сборку. Для блога, в который пишет агент, это предохранитель. Агент положил файл с ошибкой — сборка упала, а на сайте осталась прежняя рабочая версия.

Перед окончательным решением я ещё раз поискал альтернативы и принёс из поиска подборку «движков нового поколения, созданных специально для ИИ-агентов». Выглядело очень убедительно, я даже засомневался. Попросил агента проверить. Спойлер: Astro остался. Для двух движков из трёх я вообще не нашёл открытого репозитория — похоже, подборку писала модель и часть просто досочинила. Третий оказался проектом одного автора с парой десятков звёзд. А главный довод против Astro, что он «генерирует только HTML», был просто неверным.

> [!TAKEAWAY]
> Движок стоит выбирать по тому, насколько удобно его дописывать и насколько строго он проверяет контент. А подборки инструментов от ИИ лучше проверить, прежде чем менять из-за них решение.

## Считать нужно адреса

Импортёр из Ghost написал агент: выгрузить данные, скачать картинки, превратить публикации в Markdown. Так переехали 21 статья и 36 картинок.

Первая полезная ошибка была в подсчёте. По главной странице были видны только адреса статей. А в sitemap их оказалось 49: кроме статей там были страницы, теги и страница автора. Проверь я миграцию по главной, остальные адреса молча превратились бы в 404.

Раз уж всё равно переезжаю, я заодно навёл порядок: английские слаги вместо транслита вроде `sovriemiennaia-zashchita-domashniei-laboratorii` и шесть тегов вместо двадцати с лишним. Агент поначалу предлагал сохранить старые адреса как есть, но готовую схему решили не тянуть: для новых адресов сделали свою, а на старые поставили редиректы. Менять адреса дёшево только во время переезда. Если оставить сейчас, второй переезд адресов потом уже никто не захочет устраивать.

Тут же всплыло ограничение Astro. Его штатные `redirects` в статической сборке превращаются в HTML-страницы с `meta refresh`, а настоящий HTTP-статус должен выставить адаптер или сам хостинг.[^4] Мне для старых ссылок нужен был настоящий `301` в один переход, так что карта старых адресов стала входными данными для деплоя. Страниц-переходников у меня нет.

Вторую ошибку нашла строгая схема. Она требует у каждой статьи короткое описание, а ручное описание в Ghost было только у семи. Остальные импортёр пометил черновиками, и тестовый сайт честно показал семь статей. Пришлось заново написать недостающие описания: старые делались под очень маленький предпросмотр Ghost.

> [!TAKEAWAY]
> Список старых адресов берётся из sitemap. А строгая схема — лучший тест импорта: она вытаскивает долги контента, которые старая платформа прятала.

## Куда положить папку с файлами

Первой мыслью было вернуть новый блог в тот же домашний кластер: технически это самый простой путь. Агент тут же увлёкся 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 к читателю](/images/ghost-to-astro-vk-cloud/01.webp)

> [!TAKEAWAY]
> Хостинг стоит выбирать по тому, что получается после сборки. Если это папка с файлами, сначала посмотрите на объектное хранилище со статическим хостингом, и только потом на ещё один сервер.

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

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

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

Переключил 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, так что проблема пока открыта.

> [!TAKEAWAY]
> Статический сайт убирает из каждого запроса сервер и базу, но редиректы, кеши, заголовки и коды ответов никуда не деваются. Проверять их нужно через тот же домен и CDN, через которые придёт читатель.

## Грабли самого 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'` в конфиге.

> [!TAKEAWAY]
> Зелёная сборка ещё не значит, что на странице есть текст. Проверять стоит собранную страницу.

## Что в итоге

Теперь публикация — это правка 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]: [Муки выбора платформы для блога](/posts/choosing-a-blog-platform/)
[^2]: [oh-my-portal — страница проекта](/projects/oh-my-portal/), [исходники на GitHub](https://github.com/shady2k/oh-my-portal)
[^3]: [Как я строил agent-first REST API и agent-first блог](/posts/agent-first-api-and-blog/)
[^4]: [Routing — Astro Docs](https://docs.astro.build/en/guides/routing/) — «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](https://timeweb.cloud/docs/apps/deploying-frontend-apps) — «Сервис автоматически предложит команду и директорию сборки для вашего проекта».
[^6]: [Тарификация фронтенд-приложений — Timeweb Cloud](https://timeweb.cloud/docs/apps/frontend-pricing) — «Учитываются в том числе запросы на загрузку статических файлов, таких как стили, скрипты, изображения и другие ресурсы».
[^7]: [Развертывание Astro-сайта на объектном хранилище с CDN — VK Cloud](https://cloud.vk.ru/docs/ru/storage/s3/how-to-guides/cdn-ssh) — «С помощью VK Object Storage и CDN можно развернуть статический сайт на фреймворке Astro с глобальной доставкой контента и низкой задержкой».
[^8]: [Хостинг статических сайтов — VK Cloud](https://cloud.vk.ru/docs/storage/s3/concepts/static-site-hosting) — «Хостинг статических сайтов позволяет комбинировать статический контент с правилами перенаправления, а также задавать индексную страницу (например, `index.html`) и страницу ошибки, возвращаемую при ошибках запроса (например, `404`)».
[^9]: [Тарификация VK Object Storage](https://cloud.vk.ru/docs/ru/storage/s3/tariffication) — «В Hotbox и Icebox тарифицируется: […] Объем хранимых данных (в ГБ). […] Исходящий трафик (в ГБ)»; «Не тарифицируется: […] Входящий трафик».
