payload-plugin-cache
v0.9.8
Published
Payload CMS plugin for cache utilities.
Downloads
494
Maintainers
Readme
payload-plugin-cache
Payload CMS plugin for cache invalidation utilities.
This package connects Payload write events to external cache systems through stable cache tags. You configure collections, globals, and tag generation; adapters run invalidation against your cache implementation.
Usage
Use defineCachePlugin when your frontend also needs the same registry to resolve
render-time tags. Use cachePlugin when you only need the Payload plugin.
import { buildConfig } from "payload"
import { cacheTags, defineCachePlugin, findAllReferences } from "payload-plugin-cache"
import { nextCacheAdapter } from "payload-plugin-cache/next"
const { plugin: cachePlugin, registry: cacheRegistry } = defineCachePlugin({
adapter: nextCacheAdapter(),
collections: {
pages: {
path: ({ doc }) => (typeof doc.populatedUrl === "string" ? doc.populatedUrl : undefined),
predicates: {
isSearchPage: ({ doc }) => doc.slug === "search",
},
renderTags: async ({ doc, locale, path, searchParams, predicates }) => [
path ? cacheTags.route({ path, locale }) : null,
cacheTags.document({ collection: "pages", id: doc.id, locale }),
...findAllReferences(doc).map((reference) =>
cacheTags.document({
collection: reference.collection,
id: reference.id,
locale,
}),
),
cacheTags.custom("sitemap"),
predicates.isSearchPage ? cacheTags.custom("searches") : null,
predicates.isSearchPage && searchParams?.get("q")
? cacheTags.custom(`search:${searchParams.get("q")}`)
: null,
],
},
team: {
revalidateOn: ["update", "delete"],
renderTags: () => [cacheTags.custom("page")],
},
},
globals: {
settings: {
renderTags: () => [cacheTags.global("settings"), cacheTags.custom("sitemap")],
},
header: {
renderTags: () => [cacheTags.global("header")],
revalidatePaths: () => [{ path: "/", type: "layout" }],
},
footer: {
renderTags: () => [cacheTags.global("footer")],
revalidatePaths: () => [{ path: "/", type: "layout" }],
},
},
})
export { cacheRegistry }
export default buildConfig({
plugins: [cachePlugin],
collections: [
{
slug: "pages",
fields: [],
},
{
slug: "team",
fields: [],
},
],
globals: [
{
slug: "settings",
fields: [],
},
{
slug: "header",
fields: [],
},
{
slug: "footer",
fields: [],
},
],
})Configured collections receive an afterOperation hook. Configured globals receive an afterChange
hook. Existing hooks are preserved.
Optional globalRenderTags and globalRevalidateTags on the plugin are copied onto the returned
registry and merged into every successful resolution from resolveRenderCacheTags,
resolveRenderGlobalCacheTags, resolveInvalidationCacheTags, and
resolveInvalidationGlobalCacheTags (including when invalidation uses renderTags because
revalidateTags is omitted). Each may be a static tag array or a function ({ scope }) => tags
where scope is "collection" or "global" depending on which strategy is being resolved (async
returns are allowed; nested tag shapes match renderTags).
defineCachePlugin({
adapter,
globalRenderTags: ({ scope }) =>
scope === "collection"
? [cacheTags.custom("collections-root")]
: [cacheTags.custom("globals-root")],
globalRevalidateTags: [cacheTags.custom("searches")],
collections: {
/* … */
},
})Core Concepts
Strategies
Strategies compute tags from collection or global documents (and optional path, locale, or predicates). Return values are tag strings or nested arrays of them.
import { cacheTags, type CollectionCacheStrategy } from "payload-plugin-cache"
export const postStrategy: CollectionCacheStrategy<"posts"> = {
path: ({ doc }) => (doc.slug ? `/blog/${doc.slug}` : undefined),
revalidateOn: ["create", "update", "delete"],
renderTags: ({ doc, locale, path }) => [
path ? cacheTags.route({ path, locale }) : null,
cacheTags.document({ collection: "posts", id: doc.id, locale }),
cacheTags.collection("posts"),
cacheTags.custom("sitemap"),
],
}renderTags() may return nested arrays and empty values; results are flattened, filtered, and
deduped.
cacheTags helpers
cacheTags.document, cacheTags.collection, cacheTags.global, cacheTags.layout, and
cacheTags.route build stable tag strings. That differs from revalidatePaths, which
resolves revalidatePath
targets (paths and optional layout / page) through the Next adapter.
Slug arguments are generic over your Payload config (PayloadTypes / augmented GeneratedTypes),
so unknown collection or global keys fail the type checker when your project provides generated
types. cacheTags.route accepts an optional query as a string or URLSearchParams; pairs are
canonicalized (sorted by key, then value) so reordering query parameters does not change the tag.
Predicates
On collections, predicates receive CollectionPredicateRenderArgs while renderTags is resolved,
and CollectionPredicateInvalidationArgs while revalidateTags is resolved. If there is no
revalidateTags, invalidation uses renderTags and predicates are only evaluated with render args.
When both renderTags and revalidateTags are configured, ctx.renderTags() resolves render-time
predicates lazily and reuses the already resolved path from the invalidation pass.
const pageStrategy = {
predicates: {
isSearchPage: async ({ payload, doc, locale }) => {
const settings = await payload.findGlobal({ slug: "settings", depth: 0, locale })
const searchPageId =
typeof settings.searchPage === "string" ? settings.searchPage : settings.searchPage?.id
return doc.id === searchPageId
},
},
renderTags: ({ predicates }) => [predicates.isSearchPage ? "searches" : null],
}revalidateTags
Collections
renderTags(ctx)→CollectionRenderTagsContextrevalidateTags(ctx)→CollectionInvalidationTagsContext(includesoperation)await ctx.renderTags()insiderevalidateTags→ same tags asresolveRenderCacheTagsfor that document (includingglobalRenderTags) withsearchParamsomitted and render-time predicate results; path resolution runs once for the invalidation pass- Omit
revalidateTags→resolveInvalidationCacheTagsusesrenderTagswith render context only
import { cacheTags, type CollectionCacheStrategy } from "payload-plugin-cache"
export const postStrategy: CollectionCacheStrategy<"posts"> = {
revalidateOn: ["update", "delete"],
renderTags: ({ doc, locale, path }) => [
path ? cacheTags.route({ path, locale }) : null,
cacheTags.document({ collection: "posts", id: doc.id, locale }),
],
revalidateTags: async ({ doc, previousDoc, locale, operation, renderTags }) => [
...(await renderTags()),
previousDoc ? cacheTags.document({ collection: "posts", id: previousDoc.id, locale }) : null,
operation === "delete" || operation === "deleteByID" ? cacheTags.collection("posts") : null,
],
}Globals
renderTags(ctx)→GlobalRenderTagsContextrevalidateTags(ctx)→GlobalInvalidationTagsContext(includesoperation)await ctx.renderTags()insiderevalidateTags→ same tags asresolveRenderGlobalCacheTagswhen the strategy definesrenderTags(includingglobalRenderTags); otherwise[]- Omit
revalidateTags→resolveInvalidationGlobalCacheTagsusesrenderTagswithGlobalRenderTagsContext
import { cacheTags, type GlobalCacheStrategy } from "payload-plugin-cache"
export const settingsStrategy: GlobalCacheStrategy<"settings"> = {
renderTags: () => [cacheTags.global("settings"), cacheTags.custom("sitemap")],
revalidateTags: async ({ doc, previousDoc, locale, renderTags }) => [
...(await renderTags()),
doc.homePage
? cacheTags.document({ collection: "pages", id: doc.homePage as string | number, locale })
: null,
previousDoc?.homePage
? cacheTags.document({
collection: "pages",
id: previousDoc.homePage as string | number,
locale,
})
: null,
],
}revalidatePaths
Invalidation-only (no render-time analogue). Mirrors revalidateTags context: collections use
CollectionInvalidationTagsContext, globals GlobalInvalidationTagsContext (including lazy
ctx.renderTags() when renderTags is configured). Return values use the same nested
/ null / false pattern as renderTags, flattened and deduped by a stable key from path
and type ( "page" when type is omitted). If revalidatePaths is omitted, path
revalidation is skipped. The Next adapter invokes revalidatePath(path) with one argument when
type is omitted (Next’s "page" default).
import { cacheTags, type GlobalCacheStrategy } from "payload-plugin-cache"
export const footerStrategy: GlobalCacheStrategy<"footer"> = {
renderTags: () => [cacheTags.global("footer")],
revalidatePaths: () => [{ path: "/", type: "layout" }],
}Next.js
Next.js helpers are exported from payload-plugin-cache/next.
import { cacheLife } from "next/cache"
import { resolveRenderCacheTags } from "payload-plugin-cache"
import { setCacheTags } from "payload-plugin-cache/next"
import { cacheRegistry } from "@/payload.config"
export async function CachedPageContent({
payload,
page,
locale,
path,
searchParams,
}: {
payload: import("payload").Payload
page: Record<string, unknown>
locale: string
path: string
searchParams?: Record<string, string | string[] | undefined>
}) {
"use cache"
cacheLife("max")
const tags = await resolveRenderCacheTags({
registry: cacheRegistry,
payload,
collection: "pages",
doc: page,
locale,
path,
searchParams,
})
setCacheTags(tags)
return page
}setCacheTags() calls cacheTag() for each resolved tag. Pass a second argument
{ payload, logRenderTags: true } to log each tag with payload.logger. Call cacheLife() in the
cached function or component where your app owns the caching profile.
The Next.js adapter calls revalidateTag(tag, profile) and, when path entries are passed, invokes
revalidatePath(path) or revalidatePath(path, type) for each resolved path (profile
still applies only to revalidateTag).
import { nextCacheAdapter } from "payload-plugin-cache/next"
nextCacheAdapter({
profile: "max",
logRevalidatingTags: true,
logRevalidatingPaths: true,
})Other Adapters
Built-in adapters: noopCacheAdapter, webhookCacheAdapter, multiCacheAdapter. Custom adapters
implement CacheAdapter and receive deduped tags plus InvalidationContext, and optionally
paths (payload-plugin-cache → next/cache uses these for revalidatePath).
webhookCacheAdapter posts JSON { tags, paths } ( paths may be [] ).
import {
multiCacheAdapter,
noopCacheAdapter,
webhookCacheAdapter,
type CacheAdapter,
} from "payload-plugin-cache"
const loggingAdapter: CacheAdapter = {
invalidate(tags, context, paths) {
context.payload.logger.info(`Invalidating ${tags.length} tags, ${paths?.length ?? 0} paths`)
},
}
const adapter = multiCacheAdapter(
loggingAdapter,
webhookCacheAdapter({ endpoint: "https://frontend.example.com/api/revalidate" }),
noopCacheAdapter(),
)createRecordingAdapter() is exported for tests.
Reference Tagging
findAllReferences() recursively scans common Payload relationship shapes, dedupes references, and
guards circular objects. It supports polymorphic relationships, populated relationship values, and
objects with { collection, id }.
import { cacheTags, findAllReferences } from "payload-plugin-cache"
function referencedDocumentTags(doc: Record<string, unknown>, locale: string) {
return findAllReferences(doc).map((reference) =>
cacheTags.document({
collection: reference.collection,
id: reference.id,
locale,
}),
)
}Revalidation Rules
revalidateOn defaults to ["create", "update", "delete"].
Payload operations map as follows:
create:createupdate:update,updateByIDdelete:delete,deleteByID
Set req.context.disableRevalidate = true to skip invalidation for migrations, imports, nested
writes, or bulk updates (for example payload-plugin-urls update-urls recalculates
populatedUrl with this flag). Draft documents (_status: "draft") are skipped automatically.
When req.locale === "all", invalidation runs for each code in localization.locales (or from the
plugin option locales when set). If that list is empty, "en" is used once. Missing req.locale:
use localization.defaultLocale when it appears in configured locales; otherwise the first locale
in the list.
Exports
- Plugins:
defineCachePlugin,cachePlugin - Tag helpers:
cacheTags,compactTags,dedupeTags,compactPaths,dedupePaths,revalidatePathKey - Resolvers:
resolveRenderCacheTags,resolveInvalidationCacheTags,resolveRenderGlobalCacheTags,resolveInvalidationGlobalCacheTags,resolveInvalidationCachePaths,resolveInvalidationGlobalCachePaths,normalizeSearchParams(collection/global render vs invalidation use distinctResolve*CacheTagsArgstypes; invalidation requiresoperationtyped asPayloadCollectionAfterOperationorPayloadGlobalBeforeOperationfrom Payload hooks) - Dependencies:
findAllReferences,toDocumentReference,isPayloadId,documentReferenceKey,parseDocumentReferenceKey,hasDefaultPopulateChanges - Adapters:
nextCacheAdapterfrompayload-plugin-cache/next, plusnoopCacheAdapter,webhookCacheAdapter,multiCacheAdapter,createRecordingAdapter - Next helper:
setCacheTagsfrompayload-plugin-cache/next
