@plenary/next 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,34 +36,15 @@ function createTaggedFetch(baseFetch = fetch) {
36
36
 
37
37
  // src/resolve.ts
38
38
  import {
39
- isEntryWebhookPayload,
40
- WEBHOOK_TEST_EVENT
39
+ planRevalidation as planPaths,
40
+ isEntryWebhookPayload
41
41
  } from "@plenary/client";
42
- function entryUrl(payload) {
43
- const { slug, path } = payload.entry;
44
- if (!slug) return null;
45
- const dir = path && path !== "/" ? path : "";
46
- if (!dir && slug === "home") return "/";
47
- return `${dir}/${slug}`;
48
- }
42
+ import { entryUrl } from "@plenary/client";
49
43
  function planRevalidation(payload, options) {
50
- const none = { relevant: false, test: false, paths: [], tags: [] };
51
- if (!isEntryWebhookPayload(payload)) return none;
52
- const test = payload.event === WEBHOOK_TEST_EVENT;
53
- if (!test && options.events && !options.events.includes(payload.event)) {
54
- return none;
55
- }
56
- const modelKey = payload.model.key;
57
- const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
58
- let paths;
59
- if (mapped === void 0) {
60
- paths = (options.entryUrl ?? entryUrl)(payload);
61
- } else {
62
- paths = typeof mapped === "function" ? mapped(payload) : mapped;
63
- }
64
- const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
44
+ const plan = planPaths(payload, options);
65
45
  const tags = [];
66
- if (options.tags) {
46
+ if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {
47
+ const modelKey = payload.model.key;
67
48
  if (modelKey) {
68
49
  tags.push(modelTag(modelKey));
69
50
  if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
@@ -71,7 +52,7 @@ function planRevalidation(payload, options) {
71
52
  tags.push(entryTag(payload.entry.id));
72
53
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
73
54
  }
74
- return { relevant: true, test, paths: [...new Set(pathList)], tags };
55
+ return { ...plan, tags };
75
56
  }
76
57
 
77
58
  export {
@@ -79,7 +60,7 @@ export {
79
60
  entryTag,
80
61
  modelEntriesTag,
81
62
  createTaggedFetch,
82
- entryUrl,
83
- planRevalidation
63
+ planRevalidation,
64
+ entryUrl
84
65
  };
85
- //# sourceMappingURL=chunk-J5OXC4XR.js.map
66
+ //# sourceMappingURL=chunk-7XALU3L6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tags.ts","../src/resolve.ts"],"sourcesContent":["/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. The path\n * planning lives in `@plenary/client`; this adds the cache tags. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n planRevalidation as planPaths,\n isEntryWebhookPayload,\n type RevalidateOptions as PathOptions,\n type RevalidationPlan as PathPlan,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\nexport { entryUrl, type RevalidatePaths } from \"@plenary/client\";\n\nexport interface RevalidateOptions extends PathOptions {\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\nexport interface RevalidationPlan extends PathPlan {\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const plan = planPaths(payload, options);\n const tags: string[] = [];\n if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {\n const modelKey = payload.model.key;\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n return { ...plan, tags };\n}\n"],"mappings":";AAUO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAKO,SAAS,gBAAgB,UAA0B;AACxD,SAAO,yBAAyB,QAAQ;AAC1C;AAQA,IAAM,cAAc;AAGpB,SAAS,WAAW,KAAiC;AACnD,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC;AAC7C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,IAAI,gBAAgB,KAAK;AACxC,QAAM,SAAS,CAAC,GAAG,OAAO,OAAO,YAAY,GAAG,GAAG,OAAO,OAAO,gBAAgB,CAAC;AAClF,SAAO,OAAO,WAAW,IAAI,OAAO,CAAC,IAAI;AAC3C;AAeO,SAAS,kBAAkB,YAA0B,OAAqB;AAC/E,UAAQ,CAAC,OAA0B,SAAuB;AACxD,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,MACJ,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM;AAChF,UAAM,QAAQ,WAAW,QAAQ,YAAY,KAAK,GAAG,IAAI;AACzD,QAAI,CAAC,QAAQ,CAAC,EAAG,QAAO,UAAU,OAAO,IAAI;AAE7C,UAAM,QAAQ,mBAAmB,MAAM,CAAC,CAAC;AACzC,UAAM,QAAQ,MAAM,CAAC,IAAI,mBAAmB,MAAM,CAAC,CAAC,IAAI,WAAW,GAAG;AACtE,UAAM,OAAO,QAAQ,CAAC,gBAAgB,KAAK,GAAG,SAAS,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC;AACjF,UAAM,OAAQ,MAAsC;AACpD,UAAM,SAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,IAAI,EAAE;AAAA,IAC1D;AACA,WAAO,UAAU,OAAO,MAAM;AAAA,EAChC;AACF;;;ACpEA;AAAA,EACE,oBAAoB;AAAA,EACpB;AAAA,OAIK;AAGP,SAAS,gBAAsC;AAaxC,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,OAAO,UAAU,SAAS,OAAO;AACvC,QAAM,OAAiB,CAAC;AACxB,MAAI,KAAK,YAAY,QAAQ,QAAQ,sBAAsB,OAAO,GAAG;AACnE,UAAM,WAAW,QAAQ,MAAM;AAC/B,QAAI,UAAU;AACZ,WAAK,KAAK,SAAS,QAAQ,CAAC;AAC5B,UAAI,QAAQ,QAAQ,KAAK,CAAC,MAAM,MAAM,UAAU,MAAM,MAAM,EAAG,MAAK,KAAK,gBAAgB,QAAQ,CAAC;AAAA,IACpG;AACA,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AACA,SAAO,EAAE,GAAG,MAAM,KAAK;AACzB;","names":[]}
package/dist/index.cjs CHANGED
@@ -23,7 +23,7 @@ __export(index_exports, {
23
23
  createRevalidateHandler: () => createRevalidateHandler,
24
24
  createTaggedFetch: () => createTaggedFetch,
25
25
  entryTag: () => entryTag,
26
- entryUrl: () => entryUrl,
26
+ entryUrl: () => import_client2.entryUrl,
27
27
  modelEntriesTag: () => modelEntriesTag,
28
28
  modelTag: () => modelTag,
29
29
  planRevalidation: () => planRevalidation
@@ -32,7 +32,7 @@ module.exports = __toCommonJS(index_exports);
32
32
 
33
33
  // src/revalidate.ts
34
34
  var import_cache = require("next/cache");
35
- var import_client2 = require("@plenary/client");
35
+ var import_client3 = require("@plenary/client");
36
36
 
37
37
  // src/resolve.ts
38
38
  var import_client = require("@plenary/client");
@@ -74,31 +74,12 @@ function createTaggedFetch(baseFetch = fetch) {
74
74
  }
75
75
 
76
76
  // src/resolve.ts
77
- function entryUrl(payload) {
78
- const { slug, path } = payload.entry;
79
- if (!slug) return null;
80
- const dir = path && path !== "/" ? path : "";
81
- if (!dir && slug === "home") return "/";
82
- return `${dir}/${slug}`;
83
- }
77
+ var import_client2 = require("@plenary/client");
84
78
  function planRevalidation(payload, options) {
85
- const none = { relevant: false, test: false, paths: [], tags: [] };
86
- if (!(0, import_client.isEntryWebhookPayload)(payload)) return none;
87
- const test = payload.event === import_client.WEBHOOK_TEST_EVENT;
88
- if (!test && options.events && !options.events.includes(payload.event)) {
89
- return none;
90
- }
91
- const modelKey = payload.model.key;
92
- const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
93
- let paths;
94
- if (mapped === void 0) {
95
- paths = (options.entryUrl ?? entryUrl)(payload);
96
- } else {
97
- paths = typeof mapped === "function" ? mapped(payload) : mapped;
98
- }
99
- const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
79
+ const plan = (0, import_client.planRevalidation)(payload, options);
100
80
  const tags = [];
101
- if (options.tags) {
81
+ if (plan.relevant && options.tags && (0, import_client.isEntryWebhookPayload)(payload)) {
82
+ const modelKey = payload.model.key;
102
83
  if (modelKey) {
103
84
  tags.push(modelTag(modelKey));
104
85
  if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
@@ -106,7 +87,7 @@ function planRevalidation(payload, options) {
106
87
  tags.push(entryTag(payload.entry.id));
107
88
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
108
89
  }
109
- return { relevant: true, test, paths: [...new Set(pathList)], tags };
90
+ return { ...plan, tags };
110
91
  }
111
92
 
112
93
  // src/revalidate.ts
@@ -114,9 +95,9 @@ function createRevalidateHandler(options) {
114
95
  return async (request) => {
115
96
  let payload;
116
97
  try {
117
- payload = await (0, import_client2.parseWebhookRequest)(request, options);
98
+ payload = await (0, import_client3.parseWebhookRequest)(request, options);
118
99
  } catch (err) {
119
- if (err instanceof import_client2.PlenaryWebhookError) {
100
+ if (err instanceof import_client3.PlenaryWebhookError) {
120
101
  return Response.json({ message: err.message }, { status: err.status });
121
102
  }
122
103
  throw err;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/revalidate.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["/**\n * @plenary/next — Next.js glue for Plenary: webhook-driven cache revalidation\n * and cache-tagged content fetching. The Pages Router handler lives in\n * `@plenary/next/pages` so this entry stays free of `next` type imports.\n */\n\nexport { createRevalidateHandler } from \"./revalidate.js\";\nexport {\n entryUrl,\n planRevalidation,\n type RevalidateOptions,\n type RevalidatePaths,\n type RevalidationPlan,\n} from \"./resolve.js\";\nexport { createTaggedFetch, modelTag, modelEntriesTag, entryTag } from \"./tags.js\";\n","import { revalidatePath, revalidateTag } from \"next/cache\";\nimport { parseWebhookRequest, PlenaryWebhookError } from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * Builds an App Router route handler that turns Plenary entry webhooks into\n * `revalidatePath`/`revalidateTag` calls.\n *\n * ```ts\n * // app/api/revalidate/route.ts\n * export const POST = createRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * With no `paths` map, each event revalidates the entry's own URL (folder\n * path + slug). Point the webhook at the route, subscribed to the entry\n * events, with the secret in an `x-plenary-secret` header.\n *\n * The console's Test button gets a dry run: a 200 reporting what would have\n * been revalidated, without touching the cache.\n */\nexport function createRevalidateHandler(\n options: RevalidateOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n let payload;\n try {\n payload = await parseWebhookRequest(request, options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n return Response.json({ message: err.message }, { status: err.status });\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return Response.json({ revalidated: false, ignored: payload.event });\n }\n if (plan.test) {\n return Response.json({ test: true, paths: plan.paths, tags: plan.tags });\n }\n for (const path of plan.paths) revalidatePath(path);\n for (const tag of plan.tags) revalidateTag(tag);\n return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n isEntryWebhookPayload,\n WEBHOOK_TEST_EVENT,\n type EntryWebhookEvent,\n type EntryWebhookPayload,\n type ParseWebhookOptions,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home\n * page, `/`. Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n if (!dir && slug === \"home\") return \"/\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [], tags: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n const tags: string[] = [];\n if (options.tags) {\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":"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","names":["import_client"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/revalidate.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["/**\n * @plenary/next — Next.js glue for Plenary: webhook-driven cache revalidation\n * and cache-tagged content fetching. The Pages Router handler lives in\n * `@plenary/next/pages` so this entry stays free of `next` type imports.\n */\n\nexport { createRevalidateHandler } from \"./revalidate.js\";\nexport {\n entryUrl,\n planRevalidation,\n type RevalidateOptions,\n type RevalidatePaths,\n type RevalidationPlan,\n} from \"./resolve.js\";\nexport { createTaggedFetch, modelTag, modelEntriesTag, entryTag } from \"./tags.js\";\n","import { revalidatePath, revalidateTag } from \"next/cache\";\nimport { parseWebhookRequest, PlenaryWebhookError } from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * Builds an App Router route handler that turns Plenary entry webhooks into\n * `revalidatePath`/`revalidateTag` calls.\n *\n * ```ts\n * // app/api/revalidate/route.ts\n * export const POST = createRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * With no `paths` map, each event revalidates the entry's own URL (folder\n * path + slug). Point the webhook at the route, subscribed to the entry\n * events, with the secret in an `x-plenary-secret` header.\n *\n * The console's Test button gets a dry run: a 200 reporting what would have\n * been revalidated, without touching the cache.\n */\nexport function createRevalidateHandler(\n options: RevalidateOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n let payload;\n try {\n payload = await parseWebhookRequest(request, options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n return Response.json({ message: err.message }, { status: err.status });\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return Response.json({ revalidated: false, ignored: payload.event });\n }\n if (plan.test) {\n return Response.json({ test: true, paths: plan.paths, tags: plan.tags });\n }\n for (const path of plan.paths) revalidatePath(path);\n for (const tag of plan.tags) revalidateTag(tag);\n return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. The path\n * planning lives in `@plenary/client`; this adds the cache tags. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n planRevalidation as planPaths,\n isEntryWebhookPayload,\n type RevalidateOptions as PathOptions,\n type RevalidationPlan as PathPlan,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\nexport { entryUrl, type RevalidatePaths } from \"@plenary/client\";\n\nexport interface RevalidateOptions extends PathOptions {\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\nexport interface RevalidationPlan extends PathPlan {\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const plan = planPaths(payload, options);\n const tags: string[] = [];\n if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {\n const modelKey = payload.model.key;\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n return { ...plan, tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":"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","names":["import_client","import_client","planPaths"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { R as RevalidateOptions } from './resolve-RT8_HYfz.cjs';
2
- export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-RT8_HYfz.cjs';
3
- import '@plenary/client';
1
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.cjs';
2
+ export { a as RevalidationPlan, p as planRevalidation } from './resolve-CvKtOFT_.cjs';
3
+ export { RevalidatePaths, entryUrl } from '@plenary/client';
4
4
 
5
5
  /**
6
6
  * Builds an App Router route handler that turns Plenary entry webhooks into
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { R as RevalidateOptions } from './resolve-RT8_HYfz.js';
2
- export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-RT8_HYfz.js';
3
- import '@plenary/client';
1
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.js';
2
+ export { a as RevalidationPlan, p as planRevalidation } from './resolve-CvKtOFT_.js';
3
+ export { RevalidatePaths, entryUrl } from '@plenary/client';
4
4
 
5
5
  /**
6
6
  * Builds an App Router route handler that turns Plenary entry webhooks into
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  modelEntriesTag,
6
6
  modelTag,
7
7
  planRevalidation
8
- } from "./chunk-J5OXC4XR.js";
8
+ } from "./chunk-7XALU3L6.js";
9
9
 
10
10
  // src/revalidate.ts
11
11
  import { revalidatePath, revalidateTag } from "next/cache";
package/dist/pages.cjs CHANGED
@@ -23,7 +23,7 @@ __export(pages_exports, {
23
23
  createPagesRevalidateHandler: () => createPagesRevalidateHandler
24
24
  });
25
25
  module.exports = __toCommonJS(pages_exports);
26
- var import_client2 = require("@plenary/client");
26
+ var import_client3 = require("@plenary/client");
27
27
 
28
28
  // src/resolve.ts
29
29
  var import_client = require("@plenary/client");
@@ -40,31 +40,12 @@ function modelEntriesTag(modelKey) {
40
40
  }
41
41
 
42
42
  // src/resolve.ts
43
- function entryUrl(payload) {
44
- const { slug, path } = payload.entry;
45
- if (!slug) return null;
46
- const dir = path && path !== "/" ? path : "";
47
- if (!dir && slug === "home") return "/";
48
- return `${dir}/${slug}`;
49
- }
43
+ var import_client2 = require("@plenary/client");
50
44
  function planRevalidation(payload, options) {
51
- const none = { relevant: false, test: false, paths: [], tags: [] };
52
- if (!(0, import_client.isEntryWebhookPayload)(payload)) return none;
53
- const test = payload.event === import_client.WEBHOOK_TEST_EVENT;
54
- if (!test && options.events && !options.events.includes(payload.event)) {
55
- return none;
56
- }
57
- const modelKey = payload.model.key;
58
- const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
59
- let paths;
60
- if (mapped === void 0) {
61
- paths = (options.entryUrl ?? entryUrl)(payload);
62
- } else {
63
- paths = typeof mapped === "function" ? mapped(payload) : mapped;
64
- }
65
- const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
45
+ const plan = (0, import_client.planRevalidation)(payload, options);
66
46
  const tags = [];
67
- if (options.tags) {
47
+ if (plan.relevant && options.tags && (0, import_client.isEntryWebhookPayload)(payload)) {
48
+ const modelKey = payload.model.key;
68
49
  if (modelKey) {
69
50
  tags.push(modelTag(modelKey));
70
51
  if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
@@ -72,7 +53,7 @@ function planRevalidation(payload, options) {
72
53
  tags.push(entryTag(payload.entry.id));
73
54
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
74
55
  }
75
- return { relevant: true, test, paths: [...new Set(pathList)], tags };
56
+ return { ...plan, tags };
76
57
  }
77
58
 
78
59
  // src/pages.ts
@@ -91,15 +72,15 @@ function createPagesRevalidateHandler(options) {
91
72
  const fromHeader = req.headers[header];
92
73
  const fromQuery = req.query[query];
93
74
  const provided = (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ?? (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);
94
- if (!(0, import_client2.verifyWebhookSecret)(provided, options.secret)) {
75
+ if (!(0, import_client3.verifyWebhookSecret)(provided, options.secret)) {
95
76
  res.status(401).json({ message: "Invalid webhook secret" });
96
77
  return;
97
78
  }
98
79
  let payload;
99
80
  try {
100
- payload = (0, import_client2.parseWebhookPayload)(req.body);
81
+ payload = (0, import_client3.parseWebhookPayload)(req.body);
101
82
  } catch (err) {
102
- if (err instanceof import_client2.PlenaryWebhookError) {
83
+ if (err instanceof import_client3.PlenaryWebhookError) {
103
84
  res.status(err.status).json({ message: err.message });
104
85
  return;
105
86
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pages.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["import type { NextApiRequest, NextApiResponse } from \"next\";\nimport {\n parseWebhookPayload,\n verifyWebhookSecret,\n PlenaryWebhookError,\n} from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * The Pages Router counterpart of `createRevalidateHandler`, built on\n * `res.revalidate`.\n *\n * ```ts\n * // pages/api/revalidate.ts\n * export default createPagesRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * Cache tags are an App Router feature; the `tags` option is ignored here.\n */\nexport function createPagesRevalidateHandler(\n options: RevalidateOptions,\n): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n\n return async (req, res) => {\n if (req.method !== \"POST\") {\n res.status(405).json({ message: \"Method not allowed\" });\n return;\n }\n if (!options.secret) {\n res.status(500).json({ message: \"Webhook secret is not configured\" });\n return;\n }\n const fromHeader = req.headers[header];\n const fromQuery = req.query[query];\n const provided =\n (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ??\n (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);\n if (!verifyWebhookSecret(provided, options.secret)) {\n res.status(401).json({ message: \"Invalid webhook secret\" });\n return;\n }\n\n let payload;\n try {\n payload = parseWebhookPayload(req.body);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n res.status(err.status).json({ message: err.message });\n return;\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n res.json({ revalidated: false, ignored: payload.event });\n return;\n }\n if (plan.test) {\n res.json({ test: true, paths: plan.paths });\n return;\n }\n await Promise.all(plan.paths.map((p) => res.revalidate(p)));\n res.json({ revalidated: true, paths: plan.paths });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n isEntryWebhookPayload,\n WEBHOOK_TEST_EVENT,\n type EntryWebhookEvent,\n type EntryWebhookPayload,\n type ParseWebhookOptions,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home\n * page, `/`. Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n if (!dir && slug === \"home\") return \"/\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [], tags: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n const tags: string[] = [];\n if (options.tags) {\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":"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","names":["import_client"]}
1
+ {"version":3,"sources":["../src/pages.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["import type { NextApiRequest, NextApiResponse } from \"next\";\nimport {\n parseWebhookPayload,\n verifyWebhookSecret,\n PlenaryWebhookError,\n} from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * The Pages Router counterpart of `createRevalidateHandler`, built on\n * `res.revalidate`.\n *\n * ```ts\n * // pages/api/revalidate.ts\n * export default createPagesRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * Cache tags are an App Router feature; the `tags` option is ignored here.\n */\nexport function createPagesRevalidateHandler(\n options: RevalidateOptions,\n): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n\n return async (req, res) => {\n if (req.method !== \"POST\") {\n res.status(405).json({ message: \"Method not allowed\" });\n return;\n }\n if (!options.secret) {\n res.status(500).json({ message: \"Webhook secret is not configured\" });\n return;\n }\n const fromHeader = req.headers[header];\n const fromQuery = req.query[query];\n const provided =\n (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ??\n (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);\n if (!verifyWebhookSecret(provided, options.secret)) {\n res.status(401).json({ message: \"Invalid webhook secret\" });\n return;\n }\n\n let payload;\n try {\n payload = parseWebhookPayload(req.body);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n res.status(err.status).json({ message: err.message });\n return;\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n res.json({ revalidated: false, ignored: payload.event });\n return;\n }\n if (plan.test) {\n res.json({ test: true, paths: plan.paths });\n return;\n }\n await Promise.all(plan.paths.map((p) => res.revalidate(p)));\n res.json({ revalidated: true, paths: plan.paths });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. The path\n * planning lives in `@plenary/client`; this adds the cache tags. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n planRevalidation as planPaths,\n isEntryWebhookPayload,\n type RevalidateOptions as PathOptions,\n type RevalidationPlan as PathPlan,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\nexport { entryUrl, type RevalidatePaths } from \"@plenary/client\";\n\nexport interface RevalidateOptions extends PathOptions {\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\nexport interface RevalidationPlan extends PathPlan {\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const plan = planPaths(payload, options);\n const tags: string[] = [];\n if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {\n const modelKey = payload.model.key;\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n return { ...plan, tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,IAAAA,iBAIO;;;ACEP,oBAMO;;;ACHA,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAKO,SAAS,gBAAgB,UAA0B;AACxD,SAAO,yBAAyB,QAAQ;AAC1C;;;ADTA,IAAAC,iBAA+C;AAaxC,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,WAAO,cAAAC,kBAAU,SAAS,OAAO;AACvC,QAAM,OAAiB,CAAC;AACxB,MAAI,KAAK,YAAY,QAAQ,YAAQ,qCAAsB,OAAO,GAAG;AACnE,UAAM,WAAW,QAAQ,MAAM;AAC/B,QAAI,UAAU;AACZ,WAAK,KAAK,SAAS,QAAQ,CAAC;AAC5B,UAAI,QAAQ,QAAQ,KAAK,CAAC,MAAM,MAAM,UAAU,MAAM,MAAM,EAAG,MAAK,KAAK,gBAAgB,QAAQ,CAAC;AAAA,IACpG;AACA,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AACA,SAAO,EAAE,GAAG,MAAM,KAAK;AACzB;;;ADxBO,SAAS,6BACd,SAC8D;AAC9D,QAAM,SAAS,QAAQ,gBAAgB;AACvC,QAAM,QAAQ,QAAQ,eAAe;AAErC,SAAO,OAAO,KAAK,QAAQ;AACzB,QAAI,IAAI,WAAW,QAAQ;AACzB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,qBAAqB,CAAC;AACtD;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,QAAQ;AACnB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,mCAAmC,CAAC;AACpE;AAAA,IACF;AACA,UAAM,aAAa,IAAI,QAAQ,MAAM;AACrC,UAAM,YAAY,IAAI,MAAM,KAAK;AACjC,UAAM,YACH,MAAM,QAAQ,UAAU,IAAI,WAAW,CAAC,IAAI,gBAC5C,MAAM,QAAQ,SAAS,IAAI,UAAU,CAAC,IAAI;AAC7C,QAAI,KAAC,oCAAoB,UAAU,QAAQ,MAAM,GAAG;AAClD,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,yBAAyB,CAAC;AAC1D;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,oBAAU,oCAAoB,IAAI,IAAI;AAAA,IACxC,SAAS,KAAK;AACZ,UAAI,eAAe,oCAAqB;AACtC,YAAI,OAAO,IAAI,MAAM,EAAE,KAAK,EAAE,SAAS,IAAI,QAAQ,CAAC;AACpD;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,UAAI,KAAK,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM,CAAC;AACvD;AAAA,IACF;AACA,QAAI,KAAK,MAAM;AACb,UAAI,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,MAAM,CAAC;AAC1C;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,KAAK,MAAM,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC,CAAC,CAAC;AAC1D,QAAI,KAAK,EAAE,aAAa,MAAM,OAAO,KAAK,MAAM,CAAC;AAAA,EACnD;AACF;","names":["import_client","import_client","planPaths"]}
package/dist/pages.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
- import { R as RevalidateOptions } from './resolve-RT8_HYfz.cjs';
2
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.cjs';
3
3
  import '@plenary/client';
4
4
 
5
5
  /**
package/dist/pages.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
- import { R as RevalidateOptions } from './resolve-RT8_HYfz.js';
2
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.js';
3
3
  import '@plenary/client';
4
4
 
5
5
  /**
package/dist/pages.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  planRevalidation
3
- } from "./chunk-J5OXC4XR.js";
3
+ } from "./chunk-7XALU3L6.js";
4
4
 
5
5
  // src/pages.ts
6
6
  import {
@@ -0,0 +1,21 @@
1
+ import { RevalidateOptions as RevalidateOptions$1, RevalidationPlan as RevalidationPlan$1, WebhookPayload } from '@plenary/client';
2
+
3
+ /**
4
+ * The revalidation decision, shared by the App Router and Pages Router
5
+ * handlers: which paths (and tags) one webhook payload maps to. The path
6
+ * planning lives in `@plenary/client`; this adds the cache tags. Pure — the
7
+ * handlers own the framework calls.
8
+ */
9
+
10
+ interface RevalidateOptions extends RevalidateOptions$1 {
11
+ /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
12
+ * by `createTaggedFetch`. App Router only. */
13
+ tags?: boolean;
14
+ }
15
+ interface RevalidationPlan extends RevalidationPlan$1 {
16
+ tags: string[];
17
+ }
18
+ /** Decides what one payload revalidates. */
19
+ declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
20
+
21
+ export { type RevalidateOptions as R, type RevalidationPlan as a, planRevalidation as p };
@@ -0,0 +1,21 @@
1
+ import { RevalidateOptions as RevalidateOptions$1, RevalidationPlan as RevalidationPlan$1, WebhookPayload } from '@plenary/client';
2
+
3
+ /**
4
+ * The revalidation decision, shared by the App Router and Pages Router
5
+ * handlers: which paths (and tags) one webhook payload maps to. The path
6
+ * planning lives in `@plenary/client`; this adds the cache tags. Pure — the
7
+ * handlers own the framework calls.
8
+ */
9
+
10
+ interface RevalidateOptions extends RevalidateOptions$1 {
11
+ /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
12
+ * by `createTaggedFetch`. App Router only. */
13
+ tags?: boolean;
14
+ }
15
+ interface RevalidationPlan extends RevalidationPlan$1 {
16
+ tags: string[];
17
+ }
18
+ /** Decides what one payload revalidates. */
19
+ declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
20
+
21
+ export { type RevalidateOptions as R, type RevalidationPlan as a, planRevalidation as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plenary/next",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Plenary Next.js integration: webhook revalidation handlers and cache-tagged fetch.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -23,7 +23,7 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@plenary/client": "1.0.2"
26
+ "@plenary/client": "1.1.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "next": "^14 || ^15 || ^16"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tags.ts","../src/resolve.ts"],"sourcesContent":["/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n isEntryWebhookPayload,\n WEBHOOK_TEST_EVENT,\n type EntryWebhookEvent,\n type EntryWebhookPayload,\n type ParseWebhookOptions,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home\n * page, `/`. Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n if (!dir && slug === \"home\") return \"/\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [], tags: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n const tags: string[] = [];\n if (options.tags) {\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n"],"mappings":"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","names":[]}
@@ -1,44 +0,0 @@
1
- import { ParseWebhookOptions, EntryWebhookPayload, EntryWebhookEvent, WebhookPayload } from '@plenary/client';
2
-
3
- /**
4
- * The revalidation decision, shared by the App Router and Pages Router
5
- * handlers: which paths (and tags) one webhook payload maps to. Pure — the
6
- * handlers own the framework calls.
7
- */
8
-
9
- /** Paths for one model: a fixed path, a list, or a function of the payload.
10
- * A function may return `null`/`undefined` to revalidate nothing. */
11
- type RevalidatePaths = string | string[] | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);
12
- interface RevalidateOptions extends ParseWebhookOptions {
13
- /**
14
- * Extra or overriding paths per model key. A mapped model replaces the
15
- * default URL entirely — include `entryUrl(payload)` (exported) in the
16
- * returned list to keep it.
17
- */
18
- paths?: Record<string, RevalidatePaths>;
19
- /** Builds the default URL for unmapped models. Defaults to joining the
20
- * entry's folder `path` and `slug`; return `null` to skip. */
21
- entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;
22
- /** Entry events that trigger revalidation. Default: all five — revalidation
23
- * is idempotent and cheap, so over-firing beats a stale page. */
24
- events?: EntryWebhookEvent[];
25
- /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
26
- * by `createTaggedFetch`. App Router only. */
27
- tags?: boolean;
28
- }
29
- /** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
30
- * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home
31
- * page, `/`. Entries without a slug (singletons) get no default. */
32
- declare function entryUrl(payload: EntryWebhookPayload): string | null;
33
- interface RevalidationPlan {
34
- /** Whether this payload is one the handler acts on. */
35
- relevant: boolean;
36
- /** True for the console's Test button — report, don't revalidate. */
37
- test: boolean;
38
- paths: string[];
39
- tags: string[];
40
- }
41
- /** Decides what one payload revalidates. */
42
- declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
43
-
44
- export { type RevalidateOptions as R, type RevalidatePaths as a, type RevalidationPlan as b, entryUrl as e, planRevalidation as p };
@@ -1,44 +0,0 @@
1
- import { ParseWebhookOptions, EntryWebhookPayload, EntryWebhookEvent, WebhookPayload } from '@plenary/client';
2
-
3
- /**
4
- * The revalidation decision, shared by the App Router and Pages Router
5
- * handlers: which paths (and tags) one webhook payload maps to. Pure — the
6
- * handlers own the framework calls.
7
- */
8
-
9
- /** Paths for one model: a fixed path, a list, or a function of the payload.
10
- * A function may return `null`/`undefined` to revalidate nothing. */
11
- type RevalidatePaths = string | string[] | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);
12
- interface RevalidateOptions extends ParseWebhookOptions {
13
- /**
14
- * Extra or overriding paths per model key. A mapped model replaces the
15
- * default URL entirely — include `entryUrl(payload)` (exported) in the
16
- * returned list to keep it.
17
- */
18
- paths?: Record<string, RevalidatePaths>;
19
- /** Builds the default URL for unmapped models. Defaults to joining the
20
- * entry's folder `path` and `slug`; return `null` to skip. */
21
- entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;
22
- /** Entry events that trigger revalidation. Default: all five — revalidation
23
- * is idempotent and cheap, so over-firing beats a stale page. */
24
- events?: EntryWebhookEvent[];
25
- /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
26
- * by `createTaggedFetch`. App Router only. */
27
- tags?: boolean;
28
- }
29
- /** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
30
- * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home
31
- * page, `/`. Entries without a slug (singletons) get no default. */
32
- declare function entryUrl(payload: EntryWebhookPayload): string | null;
33
- interface RevalidationPlan {
34
- /** Whether this payload is one the handler acts on. */
35
- relevant: boolean;
36
- /** True for the console's Test button — report, don't revalidate. */
37
- test: boolean;
38
- paths: string[];
39
- tags: string[];
40
- }
41
- /** Decides what one payload revalidates. */
42
- declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
43
-
44
- export { type RevalidateOptions as R, type RevalidatePaths as a, type RevalidationPlan as b, entryUrl as e, planRevalidation as p };