← Blog

ISR and on-demand revalidation in Next.js 15

Web development

How dailerc.com serves static pages fast and still updates the moment an admin edits content: tagged fetches, revalidateTag after every change, and a guarded revalidate route for deploys.

A company website should be as fast as a static site and still show a new post the moment it is published. For dailerc.com we use Incremental Static Regeneration (ISR) in Next.js 15 together with on-demand revalidation. This post describes exactly how the code does it, and the details that are easy to get wrong.

The Next.js 15 change that matters

Since Next.js 15, fetch in a Server Component is no longer cached by default. If you declare nothing, every request goes back to the API. So every call to our public API states a revalidation time and a cache tag explicitly:

const res = await fetch(API_URL + '/api/v1/posts?locale=' + locale, {
  next: { revalidate: 3600, tags: ['posts'] },
})

At the page level, the public routes export revalidate = 3600 and use generateStaticParams to prerender the detail pages (projects, posts, services) for both vi and en. Pages are served from the cache, and at most an hour later a visit triggers a rebuild in the background.

Cache tags: one fixed list

An hour is far too long when an editor has just fixed a typo. The fix is to tag the data and invalidate exactly that tag when the data changes. We keep the tags in one place:

export const PUBLIC_TAGS = ['settings', 'service-categories', 'services', 'projects', 'posts'] as const
export type PublicTag = (typeof PUBLIC_TAGS)[number]

export function revalidatePublic(...tags: PublicTag[]): void {
  for (const tag of new Set(tags)) revalidateTag(tag)
}

The PublicTag type makes TypeScript reject a misspelled tag at compile time, and revalidatePublic de-duplicates so a tag is never revalidated twice in one call.

Revalidate after every admin mutation

The dailerc.com admin runs inside the same Next.js app. Every Server Action that creates, updates or deletes content calls the API and then calls revalidatePublic with the affected tags. Editing a service category, for example, invalidates both service-categories and services, because the category page lists its services. The next visit sees the new content without waiting for the hour to pass.

Note that revalidateTag only marks the data as stale. The page is regenerated on the next request, so the very first visitor after an edit may still be served the old version while the new one is built.

Two cache layers must be cleared together

The NestJS API also caches its public responses in Redis. When data changes, the API service deletes the matching cache keys before it returns. If you clear only the Next.js cache and forget Redis, Next.js happily rebuilds the page from stale API data. The right order is: the API writes the data and clears Redis, then the web app calls revalidateTag.

A revalidate route for deploys

There is one case a Server Action cannot cover. When the Docker image is built, the API is not running, so pages prerendered at build time have no real data. For that we have POST /api/revalidate:

  • It only exists when REVALIDATE_SECRET is set; otherwise it answers 404.
  • It compares the x-revalidate-secret header with timingSafeEqual, so response time leaks nothing about the secret.
  • It accepts only tags from PUBLIC_TAGS, so nobody can use it to flush arbitrary cache entries.
const disallowed = tags.filter((t) => !PUBLIC_TAGS.includes(t))
if (disallowed.length) return json({ error: 'Tags not allowed' }, 400)
for (const tag of new Set(tags)) revalidateTag(tag)

One more layer: Caddy answers 404 for /api/revalidate from the outside. The deploy script calls the route from inside the web container once every service is healthy, so the prerendered pages pick up real data right after a release.

Takeaways

  • In Next.js 15, declare next.revalidate and next.tags on every fetch you want cached.
  • Keep the tag list in one place and let a TypeScript type catch typos.
  • Invalidate from the source outwards: the API cache first, Next.js second.
  • A revalidate route needs a secret, an allow-list of tags and, ideally, no route from the Internet.

With this setup most visits are served from the cache, while editors still see their changes appear almost immediately.

Related Posts