One SVG sprite for site icons
We bundle site icons into a single inline SVG sprite and reference them by symbol ID. Here's why it's a better pattern than individual SV...
The Blog
Headless CMS sites often choose between stale content and full rebuilds. Cache tags give you a third option, update exactly the pages and fragments that changed, nothing more.
The problem
Every headless CMS site faces the same caching dilemma. You want aggressive caching for performance, pages should load fast, and servers shouldn't re-render content that hasn't changed. But when an editor hits publish, the site needs to reflect that change immediately.
Most setups solve this in one of two ways. Either they set short cache lifetimes so content refreshes frequently (which defeats the point of caching), or they trigger a full site rebuild on every publish (which is slow and wasteful when only one page changed).
Neither approach scales. A site with hundreds of pages shouldn't rebuild everything because someone fixed a typo on one blog post. And a cache that expires every 60 seconds isn't really caching, it's just adding latency to every request.
Cache tags offer a third option: update exactly what changed, nothing more, the moment it changes.
The model
Next.js gives you two ways to invalidate cached content, and understanding the difference is key to getting this right.
Path revalidation clears the cache for a specific URL. When a blog post is published, you call revalidatePath('/blog/post-slug') and that page is regenerated on the next request. This is straightforward, one publish, one page refreshed.
But pages aren't the only cached content. A blog post appears on the blog listing page, in category archive pages, in the "related posts" carousel on other pages, and in the sitemap. Path revalidation alone means you'd need to know every URL that displays content from the thing that just changed. That list grows fast.
Tag revalidation works differently. Instead of targeting URLs, you tag cached data by what it contains. The blog listing is tagged posts. The sitemap is tagged posts-sitemap. The carousel is tagged with its own tag. When a post is published, you revalidate the tags, and every cached fragment using that data refreshes, regardless of which URL it appears on.
We use both. Path revalidation for the specific page that changed. Tag revalidation for every cached fragment that references that content type.
The hooks
Every content type in our Payload CMS has collection hooks that fire on publish, unpublish, and delete. Each hook knows exactly which caches to bust. Here's the pattern across our main collections.
On publish: revalidate the post's URL path, the posts sitemap tag, the archive categories tag, and the archive carousel tag. On unpublish: also revalidate the old path so the page returns a 404 rather than serving stale content. On delete: same as unpublish.
On publish: revalidate the page path using the breadcrumb trail (since pages can be nested), plus the pages sitemap tag. Nested pages use breadcrumbs to construct their URL, so the hook resolves the full path before invalidating.
On publish: revalidate the work item path, the work sitemap tag, and the work carousel tag. The pattern mirrors blog posts but with its own tag namespace.
Global content appears on every page but doesn't have its own URL. On change: revalidate the global's cache tag. Every page that references the footer or CTA data picks up the change on next request without rebuilding every page.
The readers
On the frontend, data fetching functions wrap Payload queries with unstable_cache and declare which tags they depend on. When a hook revalidates a tag, every function using that tag refetches on the next request.
The blog listing function is tagged posts. The sitemap route is tagged posts-sitemap. The carousel component is tagged with a composite key based on collection and category. Each function declares its own dependencies, so tag revalidation is precise, busting the posts-sitemap tag doesn't touch the carousel cache, even though both reference blog posts.
For globals like the footer, the cache tag covers the entire global. Change the footer in Payload, the tag revalidates, and every page picks up the new footer content without any of those pages being individually revalidated.
The editor experience
For the content team, none of this is visible. They edit a blog post in Payload, hit publish, and the site reflects the change within seconds. No waiting for a build. No Slack message asking a developer to deploy. No "clear the cache" button.
The sitemaps update automatically, Google sees the new lastmod timestamp on the next crawl. The carousel on the homepage picks up the new post. The category archive includes it. Everything that should change, changes. Everything that shouldn't, doesn't.
This is the editorial experience that headless CMS architecture should deliver. The performance benefits of aggressive caching with the freshness of a system that knows exactly what to invalidate and when.
When we still use time-based revalidation
Cache tags handle the publish-and-update workflow. But some pages use time-based revalidation as a pragmatic fallback.
Archive pages that aggregate content across multiple collections, like a homepage that pulls from posts, work, and testimonials, sometimes use revalidate = 600 (ten minutes) as a safety net alongside tag revalidation. If a tag revalidation is missed for any reason, the page still refreshes within ten minutes.
This isn't a replacement for tags. It's a belt-and-braces approach for pages where staleness would be noticeable and the cost of occasional re-renders is low.