102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
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, string> | 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, string> | string,
|
|
): null | string {
|
|
const slugField = doc.slug
|
|
const slug =
|
|
typeof slugField === 'string'
|
|
? slugField
|
|
: slugField && typeof slugField === 'object'
|
|
? ((slugField as Record<string, unknown>)[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<string>()
|
|
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<string>()
|
|
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 }
|
|
}
|