import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' import type { I18nConfig } from '../i18n/index.js' import { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js' type RevalidateFn = (path: string) => void type BuildRevalidateHookArgs = { config: I18nConfig /** Home slug (string or per-locale map) — home revalidates the root. */ homeSlug?: Record | string /** * next/cache revalidatePath, INJECTED by the project. The plugin never imports * next/cache itself — that would crash when Payload runs as plain Node * (generate:importmap). The project passes it: `revalidatePath` from 'next/cache'. */ revalidatePath: RevalidateFn } type DocWithSlug = { slug?: unknown } /** Resolve a doc's path in one locale (root for home). Null if no slug there. */ function pathForLocale( doc: DocWithSlug, locale: string, config: I18nConfig, homeSlug?: Record | string, ): null | string { const slugField = doc.slug const slug = typeof slugField === 'string' ? slugField : slugField && typeof slugField === 'object' ? ((slugField as Record)[locale] as string | undefined) : undefined if (!slug) {return null} return buildLocalizedPath({ config, homeSlug, locale, slugs: { [locale]: slug } }) ?? null } /** * Builds afterChange + afterDelete hooks that revalidate a page's ISR cache when * an editor saves or deletes it — so changes appear immediately instead of * waiting for the revalidate window. Without this, ISR means editors wait; with * it, ISR is usable for a CMS. * * Handles every locale, the root (home), AND a changed slug (revalidates both the * old and new path so neither goes stale). revalidatePath is injected — the * plugin never imports next/cache (safe under generate:importmap / plain Node). * * // in your Media/Pages collection config, from a project file that CAN import next/cache: * import { revalidatePath } from 'next/cache' * import { buildRevalidateHook } from '@intecion/ipal-kit' * const { afterChange, afterDelete } = buildRevalidateHook({ revalidatePath, config: i18nConfig }) * // hooks: { afterChange: [afterChange], afterDelete: [afterDelete] } */ export function buildRevalidateHook({ config, homeSlug, revalidatePath, }: BuildRevalidateHookArgs): { afterChange: CollectionAfterChangeHook afterDelete: CollectionAfterDeleteHook } { const locales = getLocaleCodes(config) const afterChange: CollectionAfterChangeHook = ({ doc, previousDoc }) => { const seen = new Set() for (const locale of locales) { // New path. const newPath = pathForLocale(doc as DocWithSlug, locale, config, homeSlug) if (newPath && !seen.has(newPath)) { revalidatePath(newPath) seen.add(newPath) } // Old path, if the slug changed — so the old URL doesn't serve stale content. if (previousDoc) { const oldPath = pathForLocale(previousDoc as DocWithSlug, locale, config, homeSlug) if (oldPath && oldPath !== newPath && !seen.has(oldPath)) { revalidatePath(oldPath) seen.add(oldPath) } } } return doc } const afterDelete: CollectionAfterDeleteHook = ({ doc }) => { const seen = new Set() for (const locale of locales) { const path = pathForLocale(doc as DocWithSlug, locale, config, homeSlug) if (path && !seen.has(path)) { revalidatePath(path) seen.add(path) } } return doc } return { afterChange, afterDelete } }