Статическая генерация, ISR и страница 404
Оглавление · Код страниц · Практикум
Эта глава объясняет реализованную схему. Сайт строится из опубликованного контента Strapi, отдаёт готовый HTML и обновляет его без полной пересборки. Hatchet для обновления страниц не используется.
SSG, SSR и ISR на одном примере
Представьте новость /news/sample. При SSR сервер получает данные и собирает HTML
во время запроса. При SSG HTML появляется во время next build. ISR позволяет
заменить этот готовый HTML после изменения контента, сохранив статическую выдачу
между обновлениями.
Наш сайт использует SSG вместе с ISR. Это серверное приложение Next.js с
output: "standalone", а не экспорт папки HTML через output: "export".
Node.js нужен для новых адресов, ревалидации, webhook, tRPC и media proxy.
| Ситуация | Что происходит |
|---|---|
| Сборка релиза | Next.js читает CMS и генерирует известные страницы |
| Повторное посещение | Сервер отдаёт готовую страницу из кеша |
| Публикация изменения | Strapi сообщает Next.js об изменении через webhook |
| Первый запрос после webhook | Next.js заново получает контент и генерирует страницу |
| Новый опубликованный slug | Страница генерируется при первом посещении |
| Неизвестный или снятый slug | Возвращается HTTP 404 |
Какие файлы задают это поведение
В корневом layout задан revalidate = 3600.
В CMS client запросы используют:
fetch(url, {
headers: { Authorization: `Bearer ${token}` },
cache: "force-cache",
next: { revalidate: 3600, tags: ["cms"] },
});
force-cache включает межзапросный кеш данных. Тег cms связывает все чтения CMS
с одним сигналом инвалидирования. Часовая ревалидация — страховка от потерянного
webhook: она запускается запросом, а не таймером, который каждые 60 минут обходит сайт.
При обновлении по времени первый посетитель может получить прежнюю версию, пока
новая генерируется в фоне. Это отличается от немедленного истечения через webhook.
React cache в queries.ts решает другую
задачу: объединяет одинаковые чтения внутри одного render. Сам по себе он не
создаёт кеш HTML между посетителями.
Детальная страница новости экспортирует
generateStaticParams, который получает опубликованные новости и возвращает
{ slug }[]. dynamic = "force-static" сохраняет статическую генерацию новых
адресов. dynamicParams = false здесь не задан: такой запрет потребовал бы новую
сборку для каждого нового slug. Во время ISR generateStaticParams заново не вызывается.
Как работает webhook
Исходник схемы
sequenceDiagram
participant Editor as Редактор
participant CMS as Strapi
participant Web as Next.js
participant Visitor as Посетитель
Editor->>CMS: Publish / republish / unpublish
CMS->>Web: POST /api/webhooks/strapi
Web->>Web: Проверка Bearer-секрета и Zod-контракта
Web->>Web: revalidateTag("cms", { expire: 0 })
Web->>Web: revalidatePath("/", "layout")
Web-->>CMS: 200 revalidated
Visitor->>Web: GET страницы
Web->>CMS: Чтение опубликованного контента
Web-->>Visitor: Новый HTML
Настройка отправителя описана в главе CMS.
В обработчике { expire: 0 }
не даёт следующему чтению данных пользоваться устаревшим кешем. Сброс корневого
layout также затрагивает старые адреса и ранее закешированные 404. Поэтому смена
slug и публикация ранее отсутствовавшей страницы не требуют rebuild.
Для небольшого учебного сайта выбран общий сброс редакционного кеша. Это легко проследить и проверить. Теги отдельных моделей — возможное упражнение, которое нужно делать с учётом зависимостей главной страницы, меню и списков.
Ответ webhook подтверждает инвалидирование, а не готовность нового HTML. Повторное уведомление безопасно повторяет сброс кеша. Собственной очереди доставки уведомлений и transactional outbox нет. Уже открытая вкладка браузера сама не обновляется: пользователь должен перейти на страницу или обновить её.
Сборка и проверка состояния
Обычному pnpm build нужны доступная CMS, опубликованный Site и read-only token.
Нет подстановки пустого контента, если CMS не работает. В Docker token передаётся
через BuildKit secret; параметры CI описаны в главе деплоя.
Число одновременно генерируемых страниц ограничено в next.config.ts, чтобы
массовая сборка не перегружала единственный экземпляр CMS.
API остаются динамическими. Content health
создаёт CMS client с cache: false: готовая страница в кеше не доказывает, что
CMS доступна прямо сейчас. При нескольких экземплярах Next.js потребуется общий
cache handler; нынешний файловый кеш принадлежит одному серверу.
Страница 404 на UI kit
not-found.tsx использует PageContainer из UI kit
и Title, Text, Group, Button из Mantine. Палитра и шрифт приходят из общей
темы, геометрия — из соседнего CSS Module. Шапка и подвал принадлежат root layout.
Текст объясняет отсутствие страницы и предлагает два пути: главную и программу.
Это Server Component. Для кнопок-ссылок используется component="a" и href:
так сервер не передаёт функцию рендеринга через границу клиентского компонента.
В мобильной версии колонки становятся вертикальными, кнопки переносятся.
Отсутствие опубликованной записи приводит к notFound(). Ошибка CMS остаётся
ошибкой сервиса: превращать её в 404 означало бы сообщить, что существующий контент
удалён. Не заменяйте CmsUnavailableError возвратом null.
В Next.js 16.3.8 при ISR fallback для неизвестного slug воспроизведено ограничение: HTTP-код равен 404, но интерфейс находится в Flight payload и появляется после загрузки JavaScript. Для обычного несовпавшего маршрута используется готовая 404-страница. Похожий отчёт в репозитории Next.js не является подтверждением исправления; ограничение пока сохраняется.
Проверить самостоятельно
node scripts/check-static-site.mjs
Скрипт запускает изолированную CMS, выполняет pnpm check, затем поднимает
production standalone-сервер. Он проверяет prerender manifest, cache HIT,
авторизацию webhook, обновление главной и новости, новый slug, снятие публикации,
404-текст и ссылки, а также живую проверку CMS. Секреты стенда не нужны.
Для ручного эксперимента локально опубликуйте новость, дважды откройте адрес и
посмотрите x-nextjs-cache. Затем измените заголовок, выполните republish и
проверьте detail и список. Отдельно отключите webhook: объясните, почему обновление
по времени не означает немедленного обновления каждой страницы.
Официальные справочники: ISR, generateStaticParams, revalidateTag, not-found.