// архитектура
Для бота вся твоя страница — превью-карточка. Почему я впекаю контент в билд

Кидаешь ссылку на свою свежую статью в Телеграм — и вместо аккуратной карточки с заголовком и картинкой вылезает голый URL. Ни title, ни описания, ни превью.
Открываешь ту же ссылку в браузере — всё на месте. Разница в одном: браузер исполнил твой JavaScript, а бот Телеграма — нет. Я уткнулся в это, когда собирал единый блог-сервис на два сайта — ishabanov.ru и hire.ishabanov.ru.
Тезис простой: баг рендера и кривой Open Graph тут ни при чём. Это фундаментальное свойство того, как ты доставляешь контент. От ответа на вопрос, на каком этапе контент попадает в HTML, зависит, увидит ли статью поисковик и нарисуется ли карточка в мессенджере.
Боты не исполняют JS — и точка
Краулер или превью-бот заходит на страницу, качает HTML и читает его как текст. Он не поднимает движок, не ждёт гидрации, не дёргает твой fetch к API. Берёт ровно те байты, что сервер отдал на первый GET. А классический CSR на любом маршруте отдаёт одно и то же: пустой div и подключённый бандл. Пользователь разницы не заметит — у него всё дорисуется. Для бота страница так и остаётся пустым div'ом.
Превью-карточка для бота — это и есть весь твой сайт. Если в первом HTML её нет, для него её нет нигде.
Три способа положить контент в первый HTML
- CSR — сервер отдаёт пустую оболочку, контент дорисовывает клиент. Дёшево, но боты и краулеры видят пустоту.
- SSR — на каждый запрос сервер рендерит готовый HTML. Боты счастливы, но появляется живой Node-рантайм, который надо держать, греть и чинить под нагрузкой.
- SSG + head-stamping — контент впекается в статический HTML на этапе сборки. Боты видят всё, а в рантайме крутится только nginx с готовыми файлами. Платишь только временем билда — прод остаётся простым.
Для блога, который обновляется только по факту публикации, третий вариант почти всегда выигрывает. Контент известен заранее — значит, его можно зафиксировать заранее. Рантайм не должен делать работу, которую можно сделать один раз на сборке.
Почему я не пошёл в полный SSR
У меня была конкретная причина. Главный экран сайта — это ASCII-арт, который растеризуется на canvas прямо в браузере: я рисую глиф на canvas, считываю пиксели и собираю из них строку для тега pre. Это принципиально клиентская операция — на сервере нет ни canvas, ни той же шрифтовой растеризации. Полный SSR этот экран просто сломал бы — либо пустота, либо рассинхрон между серверной и клиентской вёрсткой.
А ботам и не нужно серверно рендерить всё тело. Им нужна голова документа — правильные title, description, og:image, canonical. Это и есть head-stamping: на сборке я генерирую per-route index.html с проштампованной головой под конкретный маршрут, а тело остаётся SPA-оболочкой с живым ASCII. SEO-скрипт на билде собирает robots.txt, sitemap.xml, rss.xml и штампует head, а nginx отдаёт нужный per-route файл.
# nginx: сначала конкретный per-route файл, потом SPA-фоллбэк
location / {
try_files $uri $uri/ /index.html;
}Снапшот, закоммиченный в репозиторий
Сайты не ходят в блог-сервис на клиенте. Они забирают контент на этапе сборки: prebuild-скрипт делает один fetch к GET /v1/posts?site=main, получает срез ровно для этого сайта (курация — через поле audiences) и пишет снапшот в posts.data.json. Этот снапшот закоммичен в репозиторий как фоллбэк. Лежит блог-сервис в момент сборки — билд спокойно берёт последний закоммиченный срез. В худшем случае выкатится чуть устаревший контент, но выкатится.
И тут тонкость с Docker. Контент впекается в образ на билде, а Docker агрессивно кэширует слои. Просто пнёшь пересборку — слой с SPA не пересоберётся, и контент не обновится. Поэтому на публикацию сервис шлёт repository_dispatch в репы сайтов, а их workflow форсит build --no-cache для web — иначе свежий prebuild-фетч не отработает поверх старого кэша.
Что это даёт на проде
- Боты и краулеры получают наполненный head с первого запроса — карточки рисуются, страницы индексируются.
- В проде нет рендер-рантайма под фронт: только nginx и статика, нечему падать под нагрузкой.
- Билд отказоустойчив — закоммиченный снапшот гарантирует сборку даже при лежащем контент-сервисе.
- Уникальный ASCII-экран жив, потому что я не потащил тело в SSR ради задачи, которую решает голова.
Правильная архитектура доставки контента — это не выбор фреймворка, а выбор момента. Ответь на один вопрос — когда контент попадает в HTML, на сборке или в рантайме — и от этого зависит, существует ли страница для всех, кто не исполняет твой JavaScript. А таких — половина важных гостей.
P.S. Скоро покажу, как публикация поста сама пересобирает сайт — почему repository_dispatch вместо ручного деплоя, и как один вебхук доводит свежую статью до прода без меня.
Есть похожая задача?
Расскажите — вернусь с оценкой в тот же день.


