JJourney AI

Caching and preview

The site template's ISR and cache layers, panel-triggered revalidation, the preview site, redirects, language prefixes, and the sitemap.

Updated 27 October 20265 min read

This page covers how the site caches content, how a change made in the panel reaches the site, and how the preview site differs from the live site.

Two cache layers

The site template has two separate caches. If either one isn't cleared, stale content stays visible.

LayerWhat it holdsLifetime
Fetch cacheCMS responses, all written with the cms-api tag.300 seconds
ISR cacheHTML generated from pages.300 seconds (revalidate = 300)
src/lib/api/cms.ts
export const CMS_CACHE_TAG = "cms-api";
export const CMS_REVALIDATE_SECONDS = 300;

Even if nothing triggers revalidation, a page refreshes itself within five minutes. sitemap.xml, robots.txt, and the redirect map are cached for the same duration.

Revalidation

The /api/revalidate route clears both layers together: it expires the cms-api tag, then invalidates pages. With ?path=/blog/post, only that path is invalidated. Without it, every page is.

src/app/api/revalidate/route.ts
revalidateTag(CMS_CACHE_TAG, "max");

const path = new URL(request.url).searchParams.get("path");
if (path) revalidatePath(path);
else revalidatePath("/", "layout");

After saving a page, sending it for approval, approving, rejecting, deleting, or reordering, the panel calls /api/revalidate on both the preview site and the live site. This call always refreshes the whole site. The Refresh Cache button in the panel triggers the same refresh manually; after use, the button is disabled for 60 seconds. Because the panel runs on a different domain, the origins allowed to make that call are set in REVALIDATE_ALLOWED_ORIGINS.

When the site runs on several server instances, each instance keeps its ISR cache on its own disk. List the other instances in REVALIDATE_NODES, and revalidation requests are forwarded to them.

Scheduled publishing

Scheduled publishing and unpublishing run in a CMS background job that doesn't call the site. These changes appear on the site once the ISR window expires. The delivery API's GET /api/v1/page/public-revision endpoint returns a fingerprint of the published content, which a site can watch to notice changes sooner.

When the CMS is unreachable

When the CMS can't be reached, the site doesn't report "page not found". The error propagates and ISR keeps serving the last good copy. After an outage, the site briefly stops calling the CMS at all, so visitors don't wait on timeouts.

The site's own health endpoint, /api/health, always returns 200. Whether the CMS is reachable is reported in the body:

GET /api/health
{ "status": "ok", "preview": false, "cms": { "reachable": true, "unreachableSince": null, "lastSuccessAt": "…" } }

In production, the site warms its pages from sitemap.xml on startup.

Preview site and live site

The same template is deployed twice: once as the preview site and once as the live site.

Preview siteLive site
PREVIEW_MODEtruenot set
ContentPreview copy (draft)Published
CachingDynamic render on every request, no cacheISR and fetch cache
NEXT_PUBLIC_INSPECTOR_ENABLEDtruenot set
robots.txtBlocks the whole siteBuilt from CMS parameters

PREVIEW_MODE is read on the server only. With the inspector flag off, inspector code never reaches the live bundle. To let the panel open the site in an iframe, the frame-ancestors header is derived from the site URL. Panels on other domains are configured in next.config.ts.

Redirect rules apply on the preview site too. A page with a redirect doesn't open at its own address in preview either.

Redirects

Redirects happen in the proxy, before rendering. The map is built from the CMS list at GET /api/v1/redirect/all/{language}. Chains are collapsed to a single hop and loops are dropped. The proxy runs in this order:

Static files are skipped.

Malformed URL leftovers are cleaned up with a 301.

The redirect map is applied.

Uppercase paths are redirected to lowercase with a 301.

The language prefix is resolved and the request continues to the content route.

Only navigation requests are redirected, so a form POST never gets a 301 at an old address. If the redirect lookup fails, navigation is not blocked.

The content route adds a safety net for anything the proxy misses. A page with redirectTo set is never rendered with content. It redirects to the target through the same map.

Language prefixes

The unprefixed language is DEFAULT_LANGUAGE in src/lib/template-config.ts. Other languages use a URL prefix, such as /en/about. Enabled languages are listed in TEMPLATE_LANGUAGES in src/lib/middleware/languages.ts.

src/lib/middleware/languages.ts
export const TEMPLATE_LANGUAGES: TemplateLanguage[] = [
  { code: "en", name: "English" },
  { code: "tr", name: "Turkish" },
];

If DEFAULT_LANGUAGE is missing from this list, the module throws on startup. That's deliberate: a wrong language prefix would silently break redirects.

Sitemap, robots, and IndexNow

  • sitemap.xml is built from the CMS page tree. Pages that are unpublished, not yet or no longer within their publish dates, marked noIndex, preview copies, or redirects are left out. Add any other paths you want to exclude to SITEMAP_EXCLUDED_PATHS.
  • robots.txt is built from the CMS's public parameters: ROBOTS_AI_CRAWLERS sets the policy for AI crawlers, and ROBOTS_EXTRA_DISALLOW adds blocked paths. Don't add a public/robots.txt file; it overrides the route.
  • /indexnow.txt returns the CMS INDEXNOW_KEY parameter as plain text. When IndexNow is enabled, the CMS notifies search engines of changed URLs, and the search engine verifies the key against this file.

Related pages

On this page