Что было

Сайт, который вы сейчас читаете, долго жил как полноценное серверное приложение. Next.js с серверным рендерингом, PostgreSQL через Prisma, Redis, очереди и воркеры, Docker Compose с несколькими контейнерами. Всё это было нужно не блогу, а сервису онлайн-аудита: краулер обходил сайт клиента, снимал скриншоты, считал метрики, складывал отчёт в личный кабинет, принимал оплату. Плюс авторизация, админка, поиск по сайту на Meilisearch и программные страницы по городам.

Проблема в том, что контентная часть — блог, услуги, кейсы, калькулятор — тащила за собой всю эту инфраструктуру. Чтобы поправить опечатку в статье, нужно было пересобрать контейнер и убедиться, что база, Redis и воркеры живы. Любое обновление зависимостей превращалось в проверку десятка движущихся частей.

Что стало

Я разделил проект. Всё, что требует сервера, ушло из этого репозитория. Осталась чистая контентная часть, и она собирается в статику.

Технически это Next.js 16 с App Router и одной строкой в конфиге — output: export. Плюс trailingSlash, чтобы каждый маршрут был папкой с index.html, отключённая оптимизация изображений (её некому выполнять без сервера) и убранный заголовок X-Powered-By. React 19, TypeScript, Tailwind 4.

Контент — не в базе, а в коде. Каждая статья блога — TypeScript-модуль с метаданными и markdown-текстом. При сборке markdown разбирается в блоки (абзацы, заголовки, списки, таблицы), и из них рендерятся страницы. Услуги, кейсы, навигация устроены так же. Аналитика — Яндекс Метрика, вставленная в корневой layout. Никаких переменных окружения, никаких секретов.

Сборка выдаёт папку out с готовым сайтом: HTML, ассеты, sitemap.xml и robots.txt. На сегодня это 867 статических страниц, из них 748 — статьи блога с пагинацией по 18 на страницу и четырьмя категориями.

Как устроен деплой

Хостинг — Timeweb Cloud Apps, тип приложения «Фронтенд». Настроек ровно три: команда сборки npm run build, каталог результата out, Node.js 20. Приложение привязано к ветке репозитория, и каждый push запускает сборку. Дальше содержимое out раздаётся через CDN.

Я специально измерил, сколько проходит от коммита до продакшена. Последний коммит в ветку — 11:19 UTC, заголовок Last-Modified у файлов на сайте — 11:22 UTC. Три минуты. Сборка 867 страниц идёт в 11 потоков и укладывается в это время вместе с выкладкой.

Маршрутизация работает из коробки: адрес без слэша на конце получает 308-редирект на версию со слэшем, папка отдаёт свой index.html. Никаких правил переписывания, никакого сервера приложений.

Что я выиграл

  • Нечему падать. Нет базы — нет проблем с коннектами, миграциями и бэкапами. Нет процесса Node на сервере — нет утечек памяти и перезапусков в три ночи.
  • Безопасность. Августовский security-релиз Next.js закрывал две критические уязвимости с удалённым выполнением кода на сервере. Статический сайт этим не задеть: сервера, который выполнял бы код, просто нет. Обновиться всё равно стоит, но не в панике.
  • Скорость. HTML лежит на CDN и отдаётся с ближайшего узла. Время до первого байта не зависит от того, проснулась ли база.
  • Воспроизводимость. Любой коммит можно собрать локально и получить байт в байт тот же сайт. Откат — это откат коммита.
  • Стоимость. Тариф статического фронтенда против VPS с Postgres, Redis и воркерами — разница в разы.

Чем заплатил

Честно про ограничения, потому что статика подходит не всем.

  • Нет серверной логики. Формы, оплата, личный кабинет, поиск с ранжированием — всё это либо внешние сервисы, либо отдельное приложение. Поиска по блогу сейчас нет; Meilisearch ушёл вместе с сервером.
  • Время сборки растёт с контентом. 867 страниц собираются за минуты, но каждая новая сотня статей добавляет время. Здесь помогает дисковый кэш Turbopack из Next.js 16.3 — повторные сборки читают неизменившиеся страницы из кэша.
  • Никакой персонализации на сервере. Всё, что зависит от пользователя, делается на клиенте.
  • Программные страницы по городам пришлось убрать: сотни почти одинаковых страниц, генерируемых по шаблону, — это то, за что и Яндекс, и я сам не хотим отвечать в статике.

Грабли после запуска: мягкая 404

Самое интересное нашлось уже на работающем сайте. Я проверил несуществующий адрес — и получил главную страницу с кодом 200. Хостинг настроен так, что любой неизвестный путь падает на index.html, как у SPA. При этом в папке out лежит готовый 404.html, который Next.js генерирует сам.

Почему это плохо: поисковик, зайдя по битой или выдуманной ссылке, получает «настоящую» страницу с кодом 200 и может её проиндексировать. Яндекс Вебмастер называет это «мягкими 404» и считает ошибкой. Плюс любой мусорный URL с внешнего сайта начинает выглядеть как дубль главной.

Лечение — на стороне хостинга: указать, что для неизвестных путей нужно отдавать 404.html с кодом 404, а не index.html с 200. Проверить просто: запросить заведомо несуществующий адрес и посмотреть код ответа. Если 200 — у вас та же история. Об этом стоит помнить всем, кто выкладывает статику на «фронтенд»-хостинги: настройка SPA-fallback там часто включена по умолчанию.

Кому подходит статика

Мой критерий простой. Если ответ «да» на все три вопроса — экспортируйте в статику и не сомневайтесь:

  1. Контент меняется руками, а не пользователями? Блог, услуги, кейсы, лендинги, документация — да. Форум, маркетплейс, личный кабинет — нет.
  2. Все интерактивные штуки можно вынести во внешние сервисы или отдельное приложение? Формы, чат, оплата — обычно да.
  3. Готовы принять, что «опубликовать» означает «пересобрать»? С деплоем за три минуты — да.

Если хотя бы одно «нет» — нужен сервер, и это нормально. Но для большинства сайтов услуг и корпоративных сайтов статика — самый дешёвый и самый надёжный вариант из существующих. Как я делаю такие проекты — на странице корпоративных сайтов на Next.js. А если у вас сейчас WordPress или Битрикс и хочется понять, стоит ли переезжать, начните с разбора какую CMS выбрать.