@plenary/next 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick DeLoughry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @plenary/next
2
+
3
+ Next.js glue for Plenary: turn entry webhooks into cache revalidation, and tag
4
+ content fetches so dependent pages revalidate themselves.
5
+
6
+ ## Webhook revalidation (App Router)
7
+
8
+ ```ts
9
+ // app/api/revalidate/route.ts
10
+ import { createRevalidateHandler } from "@plenary/next";
11
+
12
+ export const POST = createRevalidateHandler({
13
+ secret: process.env.PLENARY_WEBHOOK_SECRET!,
14
+ });
15
+ ```
16
+
17
+ In the Plenary console, add a webhook on each model pointing at
18
+ `/api/revalidate`, subscribed to the entry events, with the secret in an
19
+ `x-plenary-secret` custom header (marked secret). That's the whole setup: every
20
+ event revalidates the entry's own URL, built from its folder path and slug
21
+ (`/news` + `hello-world` → `/news/hello-world`).
22
+
23
+ When a model's URLs don't mirror folder + slug, or other pages show its
24
+ content, map it:
25
+
26
+ ```ts
27
+ export const POST = createRevalidateHandler({
28
+ secret: process.env.PLENARY_WEBHOOK_SECRET!,
29
+ paths: {
30
+ "blog-post": ({ entry }) => ["/blog", `/blog/${entry.slug}`],
31
+ "global-settings": "/", // fixed path or list also fine
32
+ },
33
+ });
34
+ ```
35
+
36
+ A mapped model replaces the default URL; compose with the exported `entryUrl`
37
+ to keep it. Unmapped slugless entries (singletons) revalidate nothing. The
38
+ `events` option narrows which entry events fire (default: all five —
39
+ revalidation is idempotent, so over-firing beats a stale page). The console's
40
+ **Test** button gets a dry run: a 200 reporting the paths it would have
41
+ revalidated.
42
+
43
+ The handler answers 401/405/400 for requests that will never succeed, so bad
44
+ deliveries fail fast in the delivery log instead of retrying for seven hours.
45
+
46
+ ## Cache tags — no path map at all
47
+
48
+ Path maps drift: add a page that renders a menu, forget to add it to the map,
49
+ serve stale content. Tags don't. Give the content client a tagged fetch:
50
+
51
+ ```ts
52
+ import { createClient } from "@plenary/client";
53
+ import { createTaggedFetch } from "@plenary/next";
54
+
55
+ export const plenary = createClient({
56
+ baseUrl: process.env.PLENARY_URL!,
57
+ siteKey: "main",
58
+ apiKey: process.env.PLENARY_API_KEY!,
59
+ locale: "en",
60
+ fetch: createTaggedFetch(),
61
+ });
62
+ ```
63
+
64
+ Every content read is now tagged `plenary:model:<key>` (and
65
+ `plenary:entry:<slugOrId>` for single-entry reads). Enable tags on the handler:
66
+
67
+ ```ts
68
+ export const POST = createRevalidateHandler({
69
+ secret: process.env.PLENARY_WEBHOOK_SECRET!,
70
+ tags: true,
71
+ });
72
+ ```
73
+
74
+ Each event then also revalidates the model and entry tags — every page that
75
+ fetched that content regenerates, including the ones you forgot you built.
76
+
77
+ ## Pages Router
78
+
79
+ ```ts
80
+ // pages/api/revalidate.ts
81
+ import { createPagesRevalidateHandler } from "@plenary/next/pages";
82
+
83
+ export default createPagesRevalidateHandler({
84
+ secret: process.env.PLENARY_WEBHOOK_SECRET!,
85
+ });
86
+ ```
87
+
88
+ Same options, built on `res.revalidate`. Cache tags are an App Router feature,
89
+ so `tags` is ignored here.
90
+
91
+ ## Authentication
92
+
93
+ Plenary does not sign webhook requests yet, so authentication is the shared
94
+ secret above (constant-time compared; also accepted as a `?secret=` query
95
+ parameter, names configurable via `secretHeader`/`secretQuery`). Payload
96
+ parsing and verification live in `@plenary/client`
97
+ (`parseWebhookRequest`, `verifyWebhookSecret`, and the payload types) for use
98
+ outside Next.js; HMAC verification will land there when signing ships.
@@ -0,0 +1,69 @@
1
+ // src/tags.ts
2
+ function modelTag(modelKey) {
3
+ return `plenary:model:${modelKey}`;
4
+ }
5
+ function entryTag(slugOrId) {
6
+ return `plenary:entry:${slugOrId}`;
7
+ }
8
+ var CONTENT_URL = /\/api\/v1\/content\/([^/?#]+)(?:\/([^/?#]+))?/;
9
+ function createTaggedFetch(baseFetch = fetch) {
10
+ return ((input, init) => {
11
+ const method = init?.method ?? "GET";
12
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
13
+ const match = method === "GET" ? CONTENT_URL.exec(url) : null;
14
+ if (!match?.[1]) return baseFetch(input, init);
15
+ const tags = [modelTag(decodeURIComponent(match[1]))];
16
+ if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));
17
+ const next = init?.next;
18
+ const merged = {
19
+ ...init,
20
+ next: { ...next, tags: [...next?.tags ?? [], ...tags] }
21
+ };
22
+ return baseFetch(input, merged);
23
+ });
24
+ }
25
+
26
+ // src/resolve.ts
27
+ import {
28
+ isEntryWebhookPayload,
29
+ WEBHOOK_TEST_EVENT
30
+ } from "@plenary/client";
31
+ function entryUrl(payload) {
32
+ const { slug, path } = payload.entry;
33
+ if (!slug) return null;
34
+ const dir = path && path !== "/" ? path : "";
35
+ return `${dir}/${slug}`;
36
+ }
37
+ function planRevalidation(payload, options) {
38
+ const none = { relevant: false, test: false, paths: [], tags: [] };
39
+ if (!isEntryWebhookPayload(payload)) return none;
40
+ const test = payload.event === WEBHOOK_TEST_EVENT;
41
+ if (!test && options.events && !options.events.includes(payload.event)) {
42
+ return none;
43
+ }
44
+ const modelKey = payload.model.key;
45
+ const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
46
+ let paths;
47
+ if (mapped === void 0) {
48
+ paths = (options.entryUrl ?? entryUrl)(payload);
49
+ } else {
50
+ paths = typeof mapped === "function" ? mapped(payload) : mapped;
51
+ }
52
+ const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
53
+ const tags = [];
54
+ if (options.tags) {
55
+ if (modelKey) tags.push(modelTag(modelKey));
56
+ tags.push(entryTag(payload.entry.id));
57
+ if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
58
+ }
59
+ return { relevant: true, test, paths: [...new Set(pathList)], tags };
60
+ }
61
+
62
+ export {
63
+ modelTag,
64
+ entryTag,
65
+ createTaggedFetch,
66
+ entryUrl,
67
+ planRevalidation
68
+ };
69
+ //# sourceMappingURL=chunk-BMLKEIKX.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 every read of a model's content. */\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/** 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/**\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 * GET requests to `/api/v1/content/{model}` are tagged with the model, and\n * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything\n * 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 tags = [modelTag(decodeURIComponent(match[1]))];\n if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));\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, 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`). 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 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) tags.push(modelTag(modelKey));\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":";AASO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAQA,IAAM,cAAc;AAcb,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,OAAO,CAAC,SAAS,mBAAmB,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,QAAI,MAAM,CAAC,EAAG,MAAK,KAAK,SAAS,mBAAmB,MAAM,CAAC,CAAC,CAAC,CAAC;AAC9D,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;;;AClDA;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AA8BA,SAAS,SAAS,SAA6C;AACpE,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,QAAQ,SAAS,MAAM,OAAO;AAC1C,SAAO,GAAG,GAAG,IAAI,IAAI;AACvB;AAYO,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,OAAO,EAAE,UAAU,OAAO,MAAM,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC,EAAE;AACjE,MAAI,CAAC,sBAAsB,OAAO,EAAG,QAAO;AAE5C,QAAM,OAAO,QAAQ,UAAU;AAC/B,MAAI,CAAC,QAAQ,QAAQ,UAAU,CAAC,QAAQ,OAAO,SAAS,QAAQ,KAA0B,GAAG;AAC3F,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,QAAQ,MAAM;AAC/B,QAAM,SAAS,aAAa,SAAY,SAAY,QAAQ,QAAQ,QAAQ;AAC5E,MAAI;AACJ,MAAI,WAAW,QAAW;AACxB,aAAS,QAAQ,YAAY,UAAU,OAAO;AAAA,EAChD,OAAO;AACL,YAAQ,OAAO,WAAW,aAAa,OAAO,OAAO,IAAI;AAAA,EAC3D;AACA,QAAM,WAAW,SAAS,OAAO,CAAC,IAAI,OAAO,UAAU,WAAW,CAAC,KAAK,IAAI;AAE5E,QAAM,OAAiB,CAAC;AACxB,MAAI,QAAQ,MAAM;AAChB,QAAI,SAAU,MAAK,KAAK,SAAS,QAAQ,CAAC;AAC1C,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AAEA,SAAO,EAAE,UAAU,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,GAAG,KAAK;AACrE;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createRevalidateHandler: () => createRevalidateHandler,
24
+ createTaggedFetch: () => createTaggedFetch,
25
+ entryTag: () => entryTag,
26
+ entryUrl: () => entryUrl,
27
+ modelTag: () => modelTag,
28
+ planRevalidation: () => planRevalidation
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/revalidate.ts
33
+ var import_cache = require("next/cache");
34
+ var import_client2 = require("@plenary/client");
35
+
36
+ // src/resolve.ts
37
+ var import_client = require("@plenary/client");
38
+
39
+ // src/tags.ts
40
+ function modelTag(modelKey) {
41
+ return `plenary:model:${modelKey}`;
42
+ }
43
+ function entryTag(slugOrId) {
44
+ return `plenary:entry:${slugOrId}`;
45
+ }
46
+ var CONTENT_URL = /\/api\/v1\/content\/([^/?#]+)(?:\/([^/?#]+))?/;
47
+ function createTaggedFetch(baseFetch = fetch) {
48
+ return ((input, init) => {
49
+ const method = init?.method ?? "GET";
50
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
51
+ const match = method === "GET" ? CONTENT_URL.exec(url) : null;
52
+ if (!match?.[1]) return baseFetch(input, init);
53
+ const tags = [modelTag(decodeURIComponent(match[1]))];
54
+ if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));
55
+ const next = init?.next;
56
+ const merged = {
57
+ ...init,
58
+ next: { ...next, tags: [...next?.tags ?? [], ...tags] }
59
+ };
60
+ return baseFetch(input, merged);
61
+ });
62
+ }
63
+
64
+ // src/resolve.ts
65
+ function entryUrl(payload) {
66
+ const { slug, path } = payload.entry;
67
+ if (!slug) return null;
68
+ const dir = path && path !== "/" ? path : "";
69
+ return `${dir}/${slug}`;
70
+ }
71
+ function planRevalidation(payload, options) {
72
+ const none = { relevant: false, test: false, paths: [], tags: [] };
73
+ if (!(0, import_client.isEntryWebhookPayload)(payload)) return none;
74
+ const test = payload.event === import_client.WEBHOOK_TEST_EVENT;
75
+ if (!test && options.events && !options.events.includes(payload.event)) {
76
+ return none;
77
+ }
78
+ const modelKey = payload.model.key;
79
+ const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
80
+ let paths;
81
+ if (mapped === void 0) {
82
+ paths = (options.entryUrl ?? entryUrl)(payload);
83
+ } else {
84
+ paths = typeof mapped === "function" ? mapped(payload) : mapped;
85
+ }
86
+ const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
87
+ const tags = [];
88
+ if (options.tags) {
89
+ if (modelKey) tags.push(modelTag(modelKey));
90
+ tags.push(entryTag(payload.entry.id));
91
+ if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
92
+ }
93
+ return { relevant: true, test, paths: [...new Set(pathList)], tags };
94
+ }
95
+
96
+ // src/revalidate.ts
97
+ function createRevalidateHandler(options) {
98
+ return async (request) => {
99
+ let payload;
100
+ try {
101
+ payload = await (0, import_client2.parseWebhookRequest)(request, options);
102
+ } catch (err) {
103
+ if (err instanceof import_client2.PlenaryWebhookError) {
104
+ return Response.json({ message: err.message }, { status: err.status });
105
+ }
106
+ throw err;
107
+ }
108
+ const plan = planRevalidation(payload, options);
109
+ if (!plan.relevant) {
110
+ return Response.json({ revalidated: false, ignored: payload.event });
111
+ }
112
+ if (plan.test) {
113
+ return Response.json({ test: true, paths: plan.paths, tags: plan.tags });
114
+ }
115
+ for (const path of plan.paths) (0, import_cache.revalidatePath)(path);
116
+ for (const tag of plan.tags) (0, import_cache.revalidateTag)(tag);
117
+ return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });
118
+ };
119
+ }
120
+ // Annotate the CommonJS export names for ESM import in node:
121
+ 0 && (module.exports = {
122
+ createRevalidateHandler,
123
+ createTaggedFetch,
124
+ entryTag,
125
+ entryUrl,
126
+ modelTag,
127
+ planRevalidation
128
+ });
129
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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, 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, 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`). 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 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) tags.push(modelTag(modelKey));\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 every read of a model's content. */\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/** 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/**\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 * GET requests to `/api/v1/content/{model}` are tagged with the model, and\n * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything\n * 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 tags = [modelTag(decodeURIComponent(match[1]))];\n if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));\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"]}
@@ -0,0 +1,51 @@
1
+ import { R as RevalidateOptions } from './resolve-DzNy1GAi.cjs';
2
+ export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-DzNy1GAi.cjs';
3
+ import '@plenary/client';
4
+
5
+ /**
6
+ * Builds an App Router route handler that turns Plenary entry webhooks into
7
+ * `revalidatePath`/`revalidateTag` calls.
8
+ *
9
+ * ```ts
10
+ * // app/api/revalidate/route.ts
11
+ * export const POST = createRevalidateHandler({
12
+ * secret: process.env.PLENARY_WEBHOOK_SECRET!,
13
+ * });
14
+ * ```
15
+ *
16
+ * With no `paths` map, each event revalidates the entry's own URL (folder
17
+ * path + slug). Point the webhook at the route, subscribed to the entry
18
+ * events, with the secret in an `x-plenary-secret` header.
19
+ *
20
+ * The console's Test button gets a dry run: a 200 reporting what would have
21
+ * been revalidated, without touching the cache.
22
+ */
23
+ declare function createRevalidateHandler(options: RevalidateOptions): (request: Request) => Promise<Response>;
24
+
25
+ /**
26
+ * Cache-tag conventions shared by the tagged fetch and the revalidation
27
+ * handler. A page that fetches Plenary content through
28
+ * {@link createTaggedFetch} is tagged with the model and entry it read, so the
29
+ * webhook handler can revalidate every page that used a piece of content
30
+ * without a path map.
31
+ */
32
+ /** Tag carried by every read of a model's content. */
33
+ declare function modelTag(modelKey: string): string;
34
+ /** Tag for one entry. The content API addresses entries by slug or UUID, and a
35
+ * webhook payload carries both, so both spellings are revalidated. */
36
+ declare function entryTag(slugOrId: string): string;
37
+ /**
38
+ * Wraps a fetch implementation so Plenary content reads carry cache tags.
39
+ * Pass it to `createClient`:
40
+ *
41
+ * ```ts
42
+ * const plenary = createClient({ ...config, fetch: createTaggedFetch() });
43
+ * ```
44
+ *
45
+ * GET requests to `/api/v1/content/{model}` are tagged with the model, and
46
+ * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything
47
+ * else passes through untouched.
48
+ */
49
+ declare function createTaggedFetch(baseFetch?: typeof fetch): typeof fetch;
50
+
51
+ export { RevalidateOptions, createRevalidateHandler, createTaggedFetch, entryTag, modelTag };
@@ -0,0 +1,51 @@
1
+ import { R as RevalidateOptions } from './resolve-DzNy1GAi.js';
2
+ export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-DzNy1GAi.js';
3
+ import '@plenary/client';
4
+
5
+ /**
6
+ * Builds an App Router route handler that turns Plenary entry webhooks into
7
+ * `revalidatePath`/`revalidateTag` calls.
8
+ *
9
+ * ```ts
10
+ * // app/api/revalidate/route.ts
11
+ * export const POST = createRevalidateHandler({
12
+ * secret: process.env.PLENARY_WEBHOOK_SECRET!,
13
+ * });
14
+ * ```
15
+ *
16
+ * With no `paths` map, each event revalidates the entry's own URL (folder
17
+ * path + slug). Point the webhook at the route, subscribed to the entry
18
+ * events, with the secret in an `x-plenary-secret` header.
19
+ *
20
+ * The console's Test button gets a dry run: a 200 reporting what would have
21
+ * been revalidated, without touching the cache.
22
+ */
23
+ declare function createRevalidateHandler(options: RevalidateOptions): (request: Request) => Promise<Response>;
24
+
25
+ /**
26
+ * Cache-tag conventions shared by the tagged fetch and the revalidation
27
+ * handler. A page that fetches Plenary content through
28
+ * {@link createTaggedFetch} is tagged with the model and entry it read, so the
29
+ * webhook handler can revalidate every page that used a piece of content
30
+ * without a path map.
31
+ */
32
+ /** Tag carried by every read of a model's content. */
33
+ declare function modelTag(modelKey: string): string;
34
+ /** Tag for one entry. The content API addresses entries by slug or UUID, and a
35
+ * webhook payload carries both, so both spellings are revalidated. */
36
+ declare function entryTag(slugOrId: string): string;
37
+ /**
38
+ * Wraps a fetch implementation so Plenary content reads carry cache tags.
39
+ * Pass it to `createClient`:
40
+ *
41
+ * ```ts
42
+ * const plenary = createClient({ ...config, fetch: createTaggedFetch() });
43
+ * ```
44
+ *
45
+ * GET requests to `/api/v1/content/{model}` are tagged with the model, and
46
+ * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything
47
+ * else passes through untouched.
48
+ */
49
+ declare function createTaggedFetch(baseFetch?: typeof fetch): typeof fetch;
50
+
51
+ export { RevalidateOptions, createRevalidateHandler, createTaggedFetch, entryTag, modelTag };
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ import {
2
+ createTaggedFetch,
3
+ entryTag,
4
+ entryUrl,
5
+ modelTag,
6
+ planRevalidation
7
+ } from "./chunk-BMLKEIKX.js";
8
+
9
+ // src/revalidate.ts
10
+ import { revalidatePath, revalidateTag } from "next/cache";
11
+ import { parseWebhookRequest, PlenaryWebhookError } from "@plenary/client";
12
+ function createRevalidateHandler(options) {
13
+ return async (request) => {
14
+ let payload;
15
+ try {
16
+ payload = await parseWebhookRequest(request, options);
17
+ } catch (err) {
18
+ if (err instanceof PlenaryWebhookError) {
19
+ return Response.json({ message: err.message }, { status: err.status });
20
+ }
21
+ throw err;
22
+ }
23
+ const plan = planRevalidation(payload, options);
24
+ if (!plan.relevant) {
25
+ return Response.json({ revalidated: false, ignored: payload.event });
26
+ }
27
+ if (plan.test) {
28
+ return Response.json({ test: true, paths: plan.paths, tags: plan.tags });
29
+ }
30
+ for (const path of plan.paths) revalidatePath(path);
31
+ for (const tag of plan.tags) revalidateTag(tag);
32
+ return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });
33
+ };
34
+ }
35
+ export {
36
+ createRevalidateHandler,
37
+ createTaggedFetch,
38
+ entryTag,
39
+ entryUrl,
40
+ modelTag,
41
+ planRevalidation
42
+ };
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/revalidate.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;AAAA,SAAS,gBAAgB,qBAAqB;AAC9C,SAAS,qBAAqB,2BAA2B;AAqBlD,SAAS,wBACd,SACyC;AACzC,SAAO,OAAO,YAAY;AACxB,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,oBAAoB,SAAS,OAAO;AAAA,IACtD,SAAS,KAAK;AACZ,UAAI,eAAe,qBAAqB;AACtC,eAAO,SAAS,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,MACvE;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,SAAS,KAAK,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM,CAAC;AAAA,IACrE;AACA,QAAI,KAAK,MAAM;AACb,aAAO,SAAS,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,IACzE;AACA,eAAW,QAAQ,KAAK,MAAO,gBAAe,IAAI;AAClD,eAAW,OAAO,KAAK,KAAM,eAAc,GAAG;AAC9C,WAAO,SAAS,KAAK,EAAE,aAAa,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,EAChF;AACF;","names":[]}
package/dist/pages.cjs ADDED
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/pages.ts
21
+ var pages_exports = {};
22
+ __export(pages_exports, {
23
+ createPagesRevalidateHandler: () => createPagesRevalidateHandler
24
+ });
25
+ module.exports = __toCommonJS(pages_exports);
26
+ var import_client2 = require("@plenary/client");
27
+
28
+ // src/resolve.ts
29
+ var import_client = require("@plenary/client");
30
+
31
+ // src/tags.ts
32
+ function modelTag(modelKey) {
33
+ return `plenary:model:${modelKey}`;
34
+ }
35
+ function entryTag(slugOrId) {
36
+ return `plenary:entry:${slugOrId}`;
37
+ }
38
+
39
+ // src/resolve.ts
40
+ function entryUrl(payload) {
41
+ const { slug, path } = payload.entry;
42
+ if (!slug) return null;
43
+ const dir = path && path !== "/" ? path : "";
44
+ return `${dir}/${slug}`;
45
+ }
46
+ function planRevalidation(payload, options) {
47
+ const none = { relevant: false, test: false, paths: [], tags: [] };
48
+ if (!(0, import_client.isEntryWebhookPayload)(payload)) return none;
49
+ const test = payload.event === import_client.WEBHOOK_TEST_EVENT;
50
+ if (!test && options.events && !options.events.includes(payload.event)) {
51
+ return none;
52
+ }
53
+ const modelKey = payload.model.key;
54
+ const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
55
+ let paths;
56
+ if (mapped === void 0) {
57
+ paths = (options.entryUrl ?? entryUrl)(payload);
58
+ } else {
59
+ paths = typeof mapped === "function" ? mapped(payload) : mapped;
60
+ }
61
+ const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
62
+ const tags = [];
63
+ if (options.tags) {
64
+ if (modelKey) tags.push(modelTag(modelKey));
65
+ tags.push(entryTag(payload.entry.id));
66
+ if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
67
+ }
68
+ return { relevant: true, test, paths: [...new Set(pathList)], tags };
69
+ }
70
+
71
+ // src/pages.ts
72
+ function createPagesRevalidateHandler(options) {
73
+ const header = options.secretHeader ?? "x-plenary-secret";
74
+ const query = options.secretQuery ?? "secret";
75
+ return async (req, res) => {
76
+ if (req.method !== "POST") {
77
+ res.status(405).json({ message: "Method not allowed" });
78
+ return;
79
+ }
80
+ const fromHeader = req.headers[header];
81
+ const fromQuery = req.query[query];
82
+ const provided = (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ?? (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);
83
+ if (!(0, import_client2.verifyWebhookSecret)(provided, options.secret)) {
84
+ res.status(401).json({ message: "Invalid webhook secret" });
85
+ return;
86
+ }
87
+ let payload;
88
+ try {
89
+ payload = (0, import_client2.parseWebhookPayload)(req.body);
90
+ } catch (err) {
91
+ if (err instanceof import_client2.PlenaryWebhookError) {
92
+ res.status(err.status).json({ message: err.message });
93
+ return;
94
+ }
95
+ throw err;
96
+ }
97
+ const plan = planRevalidation(payload, options);
98
+ if (!plan.relevant) {
99
+ res.json({ revalidated: false, ignored: payload.event });
100
+ return;
101
+ }
102
+ if (plan.test) {
103
+ res.json({ test: true, paths: plan.paths });
104
+ return;
105
+ }
106
+ await Promise.all(plan.paths.map((p) => res.revalidate(p)));
107
+ res.json({ revalidated: true, paths: plan.paths });
108
+ };
109
+ }
110
+ // Annotate the CommonJS export names for ESM import in node:
111
+ 0 && (module.exports = {
112
+ createPagesRevalidateHandler
113
+ });
114
+ //# sourceMappingURL=pages.cjs.map
@@ -0,0 +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 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, 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`). 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 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) tags.push(modelTag(modelKey));\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 every read of a model's content. */\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/** 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/**\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 * GET requests to `/api/v1/content/{model}` are tagged with the model, and\n * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything\n * 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 tags = [modelTag(decodeURIComponent(match[1]))];\n if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));\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"]}
@@ -0,0 +1,20 @@
1
+ import { NextApiRequest, NextApiResponse } from 'next';
2
+ import { R as RevalidateOptions } from './resolve-DzNy1GAi.cjs';
3
+ import '@plenary/client';
4
+
5
+ /**
6
+ * The Pages Router counterpart of `createRevalidateHandler`, built on
7
+ * `res.revalidate`.
8
+ *
9
+ * ```ts
10
+ * // pages/api/revalidate.ts
11
+ * export default createPagesRevalidateHandler({
12
+ * secret: process.env.PLENARY_WEBHOOK_SECRET!,
13
+ * });
14
+ * ```
15
+ *
16
+ * Cache tags are an App Router feature; the `tags` option is ignored here.
17
+ */
18
+ declare function createPagesRevalidateHandler(options: RevalidateOptions): (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
19
+
20
+ export { createPagesRevalidateHandler };
@@ -0,0 +1,20 @@
1
+ import { NextApiRequest, NextApiResponse } from 'next';
2
+ import { R as RevalidateOptions } from './resolve-DzNy1GAi.js';
3
+ import '@plenary/client';
4
+
5
+ /**
6
+ * The Pages Router counterpart of `createRevalidateHandler`, built on
7
+ * `res.revalidate`.
8
+ *
9
+ * ```ts
10
+ * // pages/api/revalidate.ts
11
+ * export default createPagesRevalidateHandler({
12
+ * secret: process.env.PLENARY_WEBHOOK_SECRET!,
13
+ * });
14
+ * ```
15
+ *
16
+ * Cache tags are an App Router feature; the `tags` option is ignored here.
17
+ */
18
+ declare function createPagesRevalidateHandler(options: RevalidateOptions): (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
19
+
20
+ export { createPagesRevalidateHandler };
package/dist/pages.js ADDED
@@ -0,0 +1,52 @@
1
+ import {
2
+ planRevalidation
3
+ } from "./chunk-BMLKEIKX.js";
4
+
5
+ // src/pages.ts
6
+ import {
7
+ parseWebhookPayload,
8
+ verifyWebhookSecret,
9
+ PlenaryWebhookError
10
+ } from "@plenary/client";
11
+ function createPagesRevalidateHandler(options) {
12
+ const header = options.secretHeader ?? "x-plenary-secret";
13
+ const query = options.secretQuery ?? "secret";
14
+ return async (req, res) => {
15
+ if (req.method !== "POST") {
16
+ res.status(405).json({ message: "Method not allowed" });
17
+ return;
18
+ }
19
+ const fromHeader = req.headers[header];
20
+ const fromQuery = req.query[query];
21
+ const provided = (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ?? (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);
22
+ if (!verifyWebhookSecret(provided, options.secret)) {
23
+ res.status(401).json({ message: "Invalid webhook secret" });
24
+ return;
25
+ }
26
+ let payload;
27
+ try {
28
+ payload = parseWebhookPayload(req.body);
29
+ } catch (err) {
30
+ if (err instanceof PlenaryWebhookError) {
31
+ res.status(err.status).json({ message: err.message });
32
+ return;
33
+ }
34
+ throw err;
35
+ }
36
+ const plan = planRevalidation(payload, options);
37
+ if (!plan.relevant) {
38
+ res.json({ revalidated: false, ignored: payload.event });
39
+ return;
40
+ }
41
+ if (plan.test) {
42
+ res.json({ test: true, paths: plan.paths });
43
+ return;
44
+ }
45
+ await Promise.all(plan.paths.map((p) => res.revalidate(p)));
46
+ res.json({ revalidated: true, paths: plan.paths });
47
+ };
48
+ }
49
+ export {
50
+ createPagesRevalidateHandler
51
+ };
52
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pages.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 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"],"mappings":";;;;;AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgBA,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,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,CAAC,oBAAoB,UAAU,QAAQ,MAAM,GAAG;AAClD,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,yBAAyB,CAAC;AAC1D;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAU,oBAAoB,IAAI,IAAI;AAAA,IACxC,SAAS,KAAK;AACZ,UAAI,eAAe,qBAAqB;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":[]}
@@ -0,0 +1,43 @@
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`). Entries without a slug (singletons) get no default. */
31
+ declare function entryUrl(payload: EntryWebhookPayload): string | null;
32
+ interface RevalidationPlan {
33
+ /** Whether this payload is one the handler acts on. */
34
+ relevant: boolean;
35
+ /** True for the console's Test button — report, don't revalidate. */
36
+ test: boolean;
37
+ paths: string[];
38
+ tags: string[];
39
+ }
40
+ /** Decides what one payload revalidates. */
41
+ declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
42
+
43
+ export { type RevalidateOptions as R, type RevalidatePaths as a, type RevalidationPlan as b, entryUrl as e, planRevalidation as p };
@@ -0,0 +1,43 @@
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`). Entries without a slug (singletons) get no default. */
31
+ declare function entryUrl(payload: EntryWebhookPayload): string | null;
32
+ interface RevalidationPlan {
33
+ /** Whether this payload is one the handler acts on. */
34
+ relevant: boolean;
35
+ /** True for the console's Test button — report, don't revalidate. */
36
+ test: boolean;
37
+ paths: string[];
38
+ tags: string[];
39
+ }
40
+ /** Decides what one payload revalidates. */
41
+ declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
42
+
43
+ export { type RevalidateOptions as R, type RevalidatePaths as a, type RevalidationPlan as b, entryUrl as e, planRevalidation as p };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@plenary/next",
3
+ "version": "0.1.0",
4
+ "description": "Plenary Next.js integration: webhook revalidation handlers and cache-tagged fetch.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./pages": {
17
+ "types": "./dist/pages.d.ts",
18
+ "import": "./dist/pages.js",
19
+ "require": "./dist/pages.cjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "dependencies": {
26
+ "@plenary/client": "0.1.0"
27
+ },
28
+ "peerDependencies": {
29
+ "next": "^14 || ^15 || ^16"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^26.1.2",
33
+ "next": "^15.5.12",
34
+ "react": "^19.2.7",
35
+ "react-dom": "^19.2.7"
36
+ },
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "dev": "tsup --watch",
41
+ "test": "vitest run",
42
+ "typecheck": "tsc --noEmit"
43
+ }
44
+ }