Учебное руководство
Главы руководства
На этой странице

Статическая генерация, 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
Первый запрос после webhookNext.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.