Payload Adsign Plugin

Revalidation

Opt collections and globals into plugin-managed revalidation via `custom.revalidate`

In autoMode (the default), anything with custom.revalidate set gets a revalidation hook attached at plugin init. true enables the defaults; object form lets you override.

Setup

src/collections/Articles/index.ts
import type { CollectionConfig } from 'payload';

export const Articles: CollectionConfig = {
    slug: 'articles',
    custom: {
        revalidate: {
            cacheTagFields: ['slug'],
        },
    },
    fields: [
        { name: 'slug', type: 'text' },
        { name: 'myProjects', type: 'relationship', relationTo: 'projects', hasMany: true },
    ],
};
src/payload.config.ts
adsignPlugin({
    revalidation: {
        url: process.env.REVALIDATE_CACHE_URL,
        secret: process.env.REVALIDATE_CACHE_SECRET_TOKEN,
        source: 'adsign-cms',
    },
});

Short form — just opt in with defaults:

custom: { revalidate: true }

What gets invalidated

When a doc changes, the plugin fires tags for:

  • The collection itself and each cacheTagFields value on the doc.
  • Every doc reachable through a relationship/upload field on the changed doc.
  • Every doc in other collections that references this one via a relationship/upload field.
  • Every global whose schema references the changed collection via a relationship/upload field — its slug tag is fired so embedded data stays fresh (e.g. renaming a product-categories doc busts a menu global that lists them).

All of that is automatic — no extra config.

The collection→global fan-out is schema-based: a global's tag is fired whenever it could embed the changed collection, without reading the global's data. It may occasionally invalidate a global that doesn't actually reference the specific doc — a single coarse tag, intentionally cheap.

Scoping the fan-out — revalidateRelationships

The incoming fan-out (the last two bullets above) is on by default. For a collection referenced by many documents, firing one revalidation per referencing doc on every save can be slow. Use revalidateRelationships to scope or disable it. It's a denylist — everything is included unless you set a target to false:

src/collections/ProductCategories/index.ts
custom: {
    revalidate: {
        cacheTagFields: ['slug'],
        revalidateRelationships: {
            collections: { products: false }, // skip the costly per-product fan-out
            // globals: { menu: false },      // (menu stays on — it embeds categories)
        },
    },
}
  • revalidateRelationships: true (or omitted) — fan out to every referencing collection and global.
  • revalidateRelationships: false — skip the incoming fan-out entirely (the collection still fires its own tags + cacheTagFields).
  • Object form — fan out to everything except targets set to false.

Only false is accepted per target. Setting a target to true is a type error — the previous allowlist form used true, so old usage surfaces at compile time.

Scoping the fan-out — revalidateRelated

There are two independent fan-out directions on every save:

  • Incoming (revalidateRelationships, above) — docs that point at the changed doc.
  • Outgoing (revalidateRelated, here) — the changed doc's own relationship/upload fields; the plugin fires the cache tags of the docs they point to.

Outgoing is on by default for every own relationship field, so its cost scales with list size on hasMany fields. Concrete foot-gun: a campaigns collection has a large products relationship. Every campaign save fires one revalidation POST per product — even though product pages render no campaign data. Pure churn. revalidateRelated is the opt-out, and like the incoming side it's a denylist:

src/collections/Campaigns/index.ts
custom: {
    revalidate: {
        cacheTagFields: ['slug'],
        revalidateRelated: { products: false }, // stop the per-product churn
        // every other relationship field is still followed outgoing
    },
}
  • revalidateRelated omitted — follow every own relationship/upload field.
  • Object form — follow every own field except those set to false.
  • revalidateRelated: false — disable the outgoing fan-out entirely (the collection still fires its own tags, cacheTagFields, and the incoming fan-out).

Same denylist rule as revalidateRelationships: only false is accepted per field — { products: true } is a type error. A denied name that isn't an actual relationship/upload field logs a [Revalidate] No relationship field named … warning at init so typos surface.

Globals

Same opt-in as collections. Globals are singletons so the short form is enough:

src/globals/Header.ts
export const Header: GlobalConfig = {
    slug: 'header',
    custom: { revalidate: true },
    fields: [/* ... */],
};

A change fires the header tag (plus ${source}_header when source is set).

Globals also follow their own relationship/upload fields outgoing, so the same revalidateRelated denylist applies — use the object form to scope it:

src/globals/Menu.ts
export const Menu: GlobalConfig = {
    slug: 'menu',
    custom: {
        revalidate: { revalidateRelated: { featuredProducts: false } },
    },
    fields: [/* ... */],
};

Wire format

One POST per tag. Body is always { secret, tag }:

POST <revalidation.url>
Content-Type: application/json

{ "secret": "…", "tag": "articles_hello-world" }

When source is configured, it's prefixed into every tag at the publisher:

{ "secret": "…", "tag": "adsign-cms_articles_hello-world" }

Opting out — legacyMode

To keep the old body shape and wire revalidateHook manually, set mode: 'legacyMode':

adsignPlugin({
    revalidation: {
        url: process.env.REVALIDATE_CACHE_URL,
        secret: process.env.REVALIDATE_CACHE_SECRET_TOKEN,
        mode: 'legacyMode',
    },
});

In legacyMode, source is forbidden and any collection/global with custom.revalidate set will cause the plugin to throw at init.

Next.js receiver

app/api/revalidate/route.ts
import type { NextRequest } from 'next/server';
import { revalidateFromPayload } from '@adsign/payload-adsign-plugin/next';

export async function POST(request: NextRequest): Promise<Response> {
    return revalidateFromPayload(request, {
        secret: process.env.REVALIDATE_CACHE_SECRET_TOKEN!,
    });
}

revalidateFromPayload parses the body, validates body.secret, and calls revalidateTag(body.tag, 'max'). Pass { secret, profile } to override the revalidate profile.

On this page