Skip to main content
Contentstack MCP profile hub is in general availability!

Static Generation

Production pre-renders every content page at build time and never re-renders it at runtime. Content changes go live through a rebuild that a Contentstack publish triggers.

Why

On Launch, every request that reaches the origin costs ~2.7 s or more, and there is no shared ISR cache. With 30-minute ISR, each page was rendered on its first visit and again after every expiry, and an expired page re-rendered in the background on every origin hit. Pre-rendering takes rendering off the visitor's path: pages come from the build output and carry a long CDN lifetime.

How it works

  • Content routes export revalidate = false and generateStaticParams:
    • app/[[...slug]], app/blog/[slug], app/guides/[...slug], app/kickstarts/[...slug], app/livestreams/[...slug] and app/person/[name].
    • The markdown handlers under app/md/*. For guides this includes {parent}/full for parent guides that have chapters.
  • lib/contentstack/staticParams.ts lists every entry with a url for each content type:
    • It pages through results 100 at a time and fetches only url and chapter.
    • It doesn't filter on include_in_sitemap, so noindex pages are pre-rendered too.
  • dynamicParams stays true. An entry published after the last build still renders on its first request.
  • Unknown URLs return 404.
  • Responses send Cache-Control: s-maxage=31536000, so Cloudflare keeps them until the next deploy purges the cache.
  • sitemap.xml, sitemap.md, llms.txt, agents.md, robots.txt, /.well-known/mcp, /api/openapi.json and /index.md also use revalidate = false and refresh on each build.
  • /search and /api/* stay dynamic.

Preview

  • When NEXT_PUBLIC_CONTENTSTACK_PREVIEW=true, staticParams.ts returns no params, so nothing is pre-rendered.
  • Pages render on demand as the Preview shell, which fetches content in the browser for Live Preview.
  • next.config.ts adds Cache-Control: private, no-store to every response in preview builds, so preview pages never sit in a CDN.

Don't call connection(), headers() or cookies() in these routes. Next returns a 500 when a route configured as static switches to dynamic at request time.

Rebuild on publish (setup)

Do this once per production environment:

  1. Launch: open the production environment, then Settings → Deploy Hooks. Create a hook and copy its URL.
  2. Contentstack: go to Settings → Webhooks → New webhook.
    • URL: the deploy hook URL.
    • Method: POST.
    • Channels: entry published, entry unpublished, entry deleted and asset published, scoped to the production environment.
    • Content types: page, blogpost, guide, kickstart, live_stream, person, header and footer.
  3. Publish a test entry. A new deployment should start in Launch, and the change should appear once it finishes, which takes a few minutes.

Notes:

  • Bulk publishes and releases send one webhook per item. Check how Launch handles a burst of deploy hook calls (whether it queues or collapses them). If it queues each one, put the webhook behind a short debounce, for example a Contentstack Automate flow with a delay.
  • CDN purge: after a deploy, confirm the page's x-deployment-uid header changes and Cloudflare serves the new HTML. If Launch doesn't purge Cloudflare on deploy, call the Launch Revalidate CDN Cache API after the deploy finishes, or the year-long s-maxage will keep serving old pages.
  • Cache priming: after each deploy, launch.json warms the top 50 URLs (see CachePriming).

Verify

  • npm run build should show ● (SSG) for every route listed above, with no ƒ except /search and /api/*.
  • next start: a content page returns x-nextjs-cache: HIT and Cache-Control: s-maxage=31536000 without calling Contentstack. An unknown URL returns 404.
  • NEXT_PUBLIC_CONTENTSTACK_PREVIEW=true npm run build && next start: pages return Cache-Control: private, no-store.