У вас есть готовый пирог. Нужно восстановить рецепт — без записей, по вкусу.

Примерно так я себя почувствовал, когда обнаружил, что исходников блога blog.graywrk.ru больше нет. Локальный каталог, где жил Hugo-проект, пуст. Бэкапов не было. На сервере осталась только собранная статика — готовые HTML, CSS, ассеты.

Хорошая новость: в 2026 году потеря исходников статического сайта — это не катастрофа, а интересный рабочий день. Рассказываю, как за одну сессию с ZCode я развернул блог обратно, заодно переоформив его по своему брендбуку.

Разведка: где что лежит

Сначала — понять, что вообще уцелело. Блог жил на выделенной виртуалке в Selectel и отдавался через Caddy. Все детали инфраструктуры я аккуратно веду в репозитории homelab — он-то и стал точкой опоры. Документация подсказала адрес сервера и что сайт — статическая сборка на Hugo.

Подключаюсь по SSH, осматриваю /var/www/blog.graywrk.ru. Внутри — классическая картина Hugo-вывода: index.html, каталог posts/ с подкаталогами-слагами, index.xml (RSS), sitemap.xml, пагинация в page/. И никаких markdown-исходников, темы, hugo.toml — вообще ничего, из чего это собиралось.

Зато HTML много говорит о происхождении. В футере честно написано: «Hugo & PaperMod». Заголовок поста несёт мета-теги Open Graph и Twitter Cards с заголовком, датой публикации, описанием, тегами. В <head> — canonical-ссылки на каждый пост. То есть собранная статика — это не тупой набор файлов, а структурированный документ с метаданными.

Ищу следы исходников по всему серверу: домашние каталоги пусты, git-репозиториев нет, .bash_history не содержит ничего про деплой. Отчётливый вывод — исходник деплоился снаружи (с моей рабочей машины) и при потере исчез бесследно. Восстанавливать предстояло из того, что лежит на проде.

Реконструкция: контент из готовой статики

Главное наблюдение: PaperMod рендерит страницу поста так, что почти всё нужное для markdown лежит прямо в HTML.

  • Заголовок — в <title> (с суффиксом сайта, который я отрезаю).
  • Дата публикации — в <meta property="article:published_time">.
  • Описание — в <meta name="description"> и OG-описании.
  • Теги — в <meta property="article:tag"> (по одному на тег, PaperMod так и кладёт).
  • Тело поста — внутри <article class="post-single">, точнее в <div class="post-content"> до <footer class="post-footer">.

Остаётся вытащить метаданные в Hugo front matter, а тело прогнать через HTML→markdown-конвертер. На Python это markdownify — маленькая, толковая библиотека на BeautifulSoup.

Конвертер получился простой: обойти posts/*/index.html, вытащить мета-теги в YAML-front-matter, отдать .post-content в markdownify. Результат — content/posts/<slug>/index.md для каждого из 16 постов. Slug’и я брал как есть из имён каталогов: my-first-post, processor-200-mhz, docker-image-optimization — все совпали с тем, как PaperMod их сгенерил изначально.

Честные оговорки. Два поста оказались без тегов — и в исходном HTML их тоже не было, конвертер тут ни при чём. Язык код-блоков (какой именно ```bash или ```yaml) PaperMod в <pre><code> не сохраняет — это не восстановимо без ручной проверки, и я оставил блоки без подсветки как факт. Inline-код, списки, таблицы, эмодзи и русские кавычки-«ёлочки» перенеслись честно.

Итог этапа: 16 файлов, около 180 КБ чистого контента. Сравнил список slug’ов с сервером — один в один. Контент снова стал текстом, который можно редактировать и пересобирать.

Параллельный акт: рескин по брендбуку

Пока контент восстанавливался, возникла мысль — зачем собирать блог обратно в том же виде? У меня уже есть брендбук: дизайн-система с тёмной палитрой (#0A0A0B canvas), янтарным акцентом (#F59E0B), шрифтами Golos Text и JetBrains Mono. Блог и брендбук принадлежат одному автору — мне. Логично привести их к одному визуальному языку.

PaperMod управляется через CSS-переменные: --theme, --entry, --primary, --border, --link и так далее. Это значит, что для рескина не нужно переписывать шаблоны темы — достаточно переопределить переменные значениями из брендбука. Я собрал всё в один assets/css/brand.css: токены, override CSS-переменных PaperMod под тёмную и светлую темы, плюс стили для брендовых компонентов — SVG-марка «СД» в хедере, eyebrow-меток над заголовками, lede-абзацев из description.

Шрифты — отдельный разговор. В брендбуке они подключены через Google Fonts CDN, но для блога я решил сделать self-hosting: скачал woff2 (Golos Text и JetBrains Mono оказались variable-шрифтами, так что уложился в 4 файла — по два subset’а, кириллица и латиница, на каждое семейство) и прописал @font-face с font-display: swap. Блог теперь самодостаточен: деплоится без внешних зависимостей, работает за файрволами, не звонит в Google при каждом заходе.

Тут же полезла первая заноза. После деплоя читатель сообщил: на странице /posts/ стили слетели. Смотрю вычисленные значения — переменные брендбука не применяются нигде, сайт выглядит как дефолтный PaperMod. Причина — в специфичности CSS-селекторов. Тема объявляет переменные через :root[data-theme="dark"] (специфичность 0,1,1), а мой override был написан через [data-theme="dark"] (всего 0,1,0). Браузер честно выбирал более конкретное правило — то есть из темы. Лечится одной строкой: поменять селектор на :root[data-theme="dark"]. После этого каскад снова на стороне брендбука.

Урок банальный, но полезный: проверять надо не то, что CSS-файл подключён, а то, какие значения переменных resolved на живой странице. Я раньше смотрел в исходник — там всё было на месте. Заглянул в DevTools — увидел, что побеждает тема. Это не бага Hugo или PaperMod, это классический каскад.

Деплой и verification

С деплоем всё просто: Hugo собирает статикой, rsync перетирает файлы на сервере, Caddy их раздаёт. Один bash-скрипт, никаких CI/CD ради личного блога.

Перед деплоем — обязательный бэкап: копия /var/www/blog.graywrk.ru в .bak.<дата>. Если новая сборка что-то сломает, откат в одну команду.

После rsync — проверки curl’ом. Главная отдаёт 200. Пост доступен по старому URL — это критично, потому что на посты где-то могут вести внешние ссылки, и сломать slug’и значит сломать SEO и чужие закладки. Слава богу, ничего не сломалось: 16 slug’ов на сервере и в новой сборке совпали один в один. Шрифты и favicon отдаются. Метаданные в HTML корректные.

Бэкап удаляется. Блог снова живой.

Что значит владеть контентом

Когда-то потеря исходников сайта означала катастрофу. Сегодня AI-агент за одну сессию делает то, что раньше заняло бы мне пару вечеров с кофе и матом. Это не магия — это просто хорошо структурированная задача: статика предсказуема, PaperMod предсказуем, брендбук уже существует. ZCode не делал ничего, чего я бы не смог сделать сам — он делал это быстрее и не путался в опечатках.

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

Что реально ценно в блоге — не движок и не тема (их можно заменить за вечер), а тексты. 16 постов с моим опытом, наблюдениями, иронией. Восстановить их из готового HTML получилось потому, что контент был отделён от оформления — и слава богу, что я выбрал статический генератор, а не Wix. Готовая статика навсегда остаётся на сервере; markdown-исходник восстанавливается.

Ну и главный парадокс истории. Потеря стала поводом не просто вернуть блог, а привести его в соответствие с брендом, который я тем временем построил. До этого дня блог жил сам по себе, брендбук — сам по себе. Теперь они наконец-то разговаривают на одном визуальном языке. Исходники лежат в приватном репозитории на GitHub, деплой — одной командой, контент снова мой в полном смысле этого слова.

Если у вас статический сайт и его исходники где-то в одном экземпляре — закоммитьте их прямо сегодня. А если уже потеряли — что ж, кое-что восстановить всё-таки можно.