@tetherto/dev-websites-core 0.3.4 → 0.3.6

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/README.md CHANGED
@@ -2,22 +2,24 @@
2
2
 
3
3
  Shared CMS, SEO, i18n, utils, UI, and analytics primitives for WDK / QVAC / PEAR / MDK websites.
4
4
 
5
- | Import | Contents |
6
- | --- | --- |
7
- | `@tetherto/dev-websites-core/cms` | Payload `buildCmsConfig`, stock collections, access, Lexical, media I/O |
8
- | `@tetherto/dev-websites-core/cms/google-oauth` | Google admin login handlers (`google-auth-library` peer) |
9
- | `@tetherto/dev-websites-core/seo` | `createMetadataFactory`, `getSiteUrl`, `getOgBackgroundDataUrl` |
10
- | `@tetherto/dev-websites-core/i18n` | `Languages` / `LanguageLabel` |
11
- | `@tetherto/dev-websites-core/utils` | `createLogger`, `toPlainValue`, `stripShikiPreBackground` |
12
- | `@tetherto/dev-websites-core/analytics` | `GtmLoader`, `createTrackedLink` (optional `@next/third-parties` peer) |
13
- | `@tetherto/dev-websites-core/ui` | UI primitives (`Button`, `Dialog`, `Pagination`, `Tooltip`, …) + `cn` |
14
- | `@tetherto/dev-websites-core/ui/google-oauth` | Payload `GoogleOAuthButton` (Tailwind classes; source `dist` from Payload CSS) |
15
- | `@tetherto/dev-websites-core/ui/a11y` | `focusRingClassName`, `VisuallyHidden` |
16
- | `@tetherto/dev-websites-core/ui/carousel` | `Carousel` (+ optional `embla-carousel-react` peer) |
17
- | `@tetherto/dev-websites-core/ui/theme.css` | One-shot: tokens + Tailwind v4 `@theme` preset |
18
- | `@tetherto/dev-websites-core/ui/theme/tokens.css` | CSS variables (HSL channels) |
19
- | `@tetherto/dev-websites-core/ui/theme/preset.css` | Tailwind v4 `@theme` map |
20
- | `@tetherto/dev-websites-core/ui/theme/preset` | JS Tailwind preset for `tailwind.config.ts` |
5
+ | Import | Contents |
6
+ | ------------------------------------------------- | ------------------------------------------------------------------------------ |
7
+ | `@tetherto/dev-websites-core/cms` | Payload `buildCmsConfig`, stock collections, access, Lexical, media I/O |
8
+ | `@tetherto/dev-websites-core/cache` | Mongo `cacheHandler` factory plus Payload revalidation helpers |
9
+ | `@tetherto/dev-websites-core/cache/handler` | Default Next `cacheHandler` (reads `MONGODB_URI`) |
10
+ | `@tetherto/dev-websites-core/cms/google-oauth` | Google admin login handlers (`google-auth-library` peer) |
11
+ | `@tetherto/dev-websites-core/seo` | `createMetadataFactory`, `getSiteUrl`, `getOgBackgroundDataUrl` |
12
+ | `@tetherto/dev-websites-core/i18n` | `Languages` / `LanguageLabel` |
13
+ | `@tetherto/dev-websites-core/utils` | `createLogger`, `toPlainValue`, `stripShikiPreBackground` |
14
+ | `@tetherto/dev-websites-core/analytics` | `GtmLoader`, `createTrackedLink` (optional `@next/third-parties` peer) |
15
+ | `@tetherto/dev-websites-core/ui` | UI primitives (`Button`, `Dialog`, `Pagination`, `Tooltip`, …) + `cn` |
16
+ | `@tetherto/dev-websites-core/ui/google-oauth` | Payload `GoogleOAuthButton` (Tailwind classes; source `dist` from Payload CSS) |
17
+ | `@tetherto/dev-websites-core/ui/a11y` | `focusRingClassName`, `VisuallyHidden` |
18
+ | `@tetherto/dev-websites-core/ui/carousel` | `Carousel` (+ optional `embla-carousel-react` peer) |
19
+ | `@tetherto/dev-websites-core/ui/theme.css` | One-shot: tokens + Tailwind v4 `@theme` preset |
20
+ | `@tetherto/dev-websites-core/ui/theme/tokens.css` | CSS variables (HSL channels) |
21
+ | `@tetherto/dev-websites-core/ui/theme/preset.css` | Tailwind v4 `@theme` map |
22
+ | `@tetherto/dev-websites-core/ui/theme/preset` | JS Tailwind preset for `tailwind.config.ts` |
21
23
 
22
24
  ### Install
23
25
 
@@ -160,7 +162,7 @@ export default {
160
162
  theme: {
161
163
  extend: {
162
164
  colors: {
163
- accent: { DEFAULT: 'hsl(var(--color-text-action) / <alpha-value>)', /* … */ },
165
+ accent: { DEFAULT: 'hsl(var(--color-text-action) / <alpha-value>)' /* … */ },
164
166
  // …
165
167
  },
166
168
  },
@@ -206,7 +208,7 @@ Registered by `buildCmsConfig` in this order. Disable or extend any of them via
206
208
 
207
209
  Changelog stock ships shared fields only; site-specific body / GitHub /
208
210
  `relatedBlock` fields go in `overrides.fields`. Revalidation hooks go in
209
- `overrides.hooks` (site-owned, e.g. `src/lib/cache`) — hook arrays are **appended**
211
+ `overrides.hooks` (from `@tetherto/dev-websites-core/cache`) — hook arrays are **appended**
210
212
  to stock hooks, not replaced.
211
213
 
212
214
  Draft collections enable Payload `schedulePublish`. `buildCmsConfig` starts
@@ -214,6 +216,52 @@ Draft collections enable Payload `schedulePublish`. `buildCmsConfig` starts
214
216
  process (`next start` / Docker). Override or disable via `payload.jobs`. This
215
217
  is not for serverless hosts.
216
218
 
219
+ ### Cache handler
220
+
221
+ `@tetherto/dev-websites-core/cache/handler` backs Next's ISR / route / fetch
222
+ cache with Mongo so every pod of a deployment shares one cache instead of a
223
+ per-instance filesystem. It reads `MONGODB_URI`, keeps one client (reused
224
+ across dev reloads), and exports the class Next constructs with `new Handler(ctx)`.
225
+
226
+ ```ts
227
+ // next.config.ts
228
+ cacheHandler: require.resolve('@tetherto/dev-websites-core/cache/handler'),
229
+ cacheMaxMemorySize: 0,
230
+ outputFileTracingIncludes: {
231
+ '/*': ['./node_modules/@tetherto/dev-websites-core/dist/cache/**/*'],
232
+ },
233
+ ```
234
+
235
+ `createMongoCacheHandler({ clientPromise })` is the same class for a site that
236
+ already owns a client. Any object matching `MongoCacheClient` works (a real
237
+ `MongoClient` is assignable).
238
+
239
+ `/cache` also exports the Payload revalidation helpers
240
+ (`safeRevalidatePath`, `safeRevalidateTag`, `shouldSkipRevalidate`,
241
+ `createPublishedSlugRevalidateHooks`, `createAlwaysRevalidateHooks`,
242
+ `createBlogRevalidators`, `createChangelogRevalidators`). Import those from
243
+ `@tetherto/dev-websites-core/cache` — not from the handler entry, which
244
+ connects to Mongo on load.
245
+
246
+ Defaults: database `next_cache`, collection `fetch_cache`, 7-day TTL refreshed
247
+ on every write, page keys scoped by `.next/BUILD_ID`. Override with `dbName`,
248
+ `collectionName`, `entryTtlSeconds`, `buildId`.
249
+
250
+ Behaviour worth knowing:
251
+
252
+ - `get()` returns rows of any age so Next can serve stale while it revalidates.
253
+ Expiry is Next's decision, not the handler's.
254
+ - Page and route keys are prefixed with the build id, because rendered HTML
255
+ embeds chunk filenames that 404 after a deploy. Fetch keys are content
256
+ hashes and stay shared across builds.
257
+ - Non-2xx page/route responses are never written, and poisoned rows from
258
+ earlier writes are deleted on read (`shouldSkipCacheWrite` is exported).
259
+ Without this, a scanner hitting unknown ISR slugs rewrites a 404 row on
260
+ every request.
261
+ - Page tags fall back to the `x-next-cache-tags` header, since Next only passes
262
+ `ctx.tags` for fetch entries — without it `revalidateTag` cannot purge
263
+ rendered HTML.
264
+
217
265
  ### Peer dependencies
218
266
 
219
267
  CMS / Next (required for `/cms` consumers):
@@ -230,17 +278,18 @@ UI (required for `/ui` consumers):
230
278
  Read from `process.env` at runtime by the package — **set them in the consuming
231
279
  site's environment** (`.env` / hosting / CI); `@tetherto/dev-websites-core` never defines them.
232
280
 
233
- | Variable | Used by | Notes |
234
- | --------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
235
- | `AWS_S3_BUCKET` | `media`, `createS3MediaPlugin`, `readMediaBuffer` | When set, media reads/writes go to S3; otherwise disk (`<cwd>/media`). |
236
- | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | S3 access | Optional; falls back to the default AWS credential chain. |
237
- | `AWS_S3_REGION` | S3 access | Defaults to `us-east-1`. |
281
+ | Variable | Used by | Notes |
282
+ | --------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
283
+ | `AWS_S3_BUCKET` | `media`, `createS3MediaPlugin`, `readMediaBuffer` | When set, media reads/writes go to S3; otherwise disk (`<cwd>/media`). |
284
+ | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | S3 access | Optional; falls back to the default AWS credential chain. |
285
+ | `AWS_S3_REGION` | S3 access | Defaults to `us-east-1`. |
238
286
  | `AWS_S3_ENDPOINT` | S3 access | Optional custom endpoint (S3-compatible stores on QVAC / PEAR). Omit on WDK for default AWS S3. |
239
- | `AWS_S3_FORCE_PATH_STYLE` | S3 access | Set to `true` when the custom endpoint requires path-style addressing. |
240
- | `NEXT_PUBLIC_SITE_URL` | `getSiteUrl` | Public origin; trailing slash trimmed. Warns in production if this and `VERCEL_URL` are unset. |
241
- | `VERCEL_URL` | `getSiteUrl` | Fallback origin on Vercel when `NEXT_PUBLIC_SITE_URL` is unset. |
242
- | `LOG_LEVEL` | `createLogger` | `debug` \| `info` \| `warn` \| `error`. Defaults to `info` in production, `debug` otherwise. |
243
- | `NEXT_PHASE` | `requireEnvAtRuntime` | Build phases skip the required-env check (no DB connection). |
287
+ | `AWS_S3_FORCE_PATH_STYLE` | S3 access | Set to `true` when the custom endpoint requires path-style addressing. |
288
+ | `NEXT_PUBLIC_SITE_URL` | `getSiteUrl` | Public origin; trailing slash trimmed. Warns in production if this and `VERCEL_URL` are unset. |
289
+ | `VERCEL_URL` | `getSiteUrl` | Fallback origin on Vercel when `NEXT_PUBLIC_SITE_URL` is unset. |
290
+ | `LOG_LEVEL` | `createLogger` | `debug` \| `info` \| `warn` \| `error`. Defaults to `info` in production, `debug` otherwise. |
291
+ | `MONGODB_URI` | `cache/handler` | Required when the default cache handler is loaded. |
292
+ | `NEXT_PHASE` | `requireEnvAtRuntime` | Build phases skip the required-env check (no DB connection). |
244
293
 
245
294
  Hard-required vars (e.g. `PAYLOAD_SECRET`, `MONGODB_URI`) should be read via
246
295
  `requireEnvAtRuntime(name)` — it logs and throws at runtime when the var is
@@ -0,0 +1,51 @@
1
+ //#region src/cache/types.d.ts
2
+ /**
3
+ * A cache entry as Next hands it to `set()`. Only the fields this handler reads
4
+ * are named; `kind` is `'APP_PAGE'`, `'APP_ROUTE'`, `'PAGES'`, `'FETCH'`, or
5
+ * `'IMAGE'`, and the rest of the payload is stored verbatim.
6
+ */
7
+ type CachedValue = {
8
+ kind?: string;
9
+ status?: number;
10
+ html?: string;
11
+ headers?: Record<string, string | undefined>;
12
+ /** Stored as a plain object; Next iterates it as a Map, so `get()` rebuilds one. */
13
+ segmentData?: Record<string, unknown> | Map<string, unknown>;
14
+ [key: string]: unknown;
15
+ };
16
+ type CachedEntry = {
17
+ lastModified?: number;
18
+ value: CachedValue | null;
19
+ };
20
+ /** The `ctx` Next passes to `set()`; only `tags` is meaningful here. */
21
+ type CacheSetContext = {
22
+ tags?: string[];
23
+ };
24
+ //#endregion
25
+ //#region src/cache/createMongoCacheHandler.d.ts
26
+ type MongoCacheHandler = {
27
+ get(key: string): Promise<CachedEntry | null>;
28
+ set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void>;
29
+ delete(key: string): Promise<void>;
30
+ resetRequestCache(): void;
31
+ revalidateTag(tags: string | string[], durations?: unknown): Promise<void>;
32
+ };
33
+ /**
34
+ * Next instantiates the configured handler with `new Handler(ctx)`, so the
35
+ * factory returns a class rather than an instance.
36
+ */
37
+ type MongoCacheHandlerConstructor = new (ctx?: unknown) => MongoCacheHandler;
38
+ //#endregion
39
+ //#region src/cache/handler.d.ts
40
+ /**
41
+ * Default Next.js `cacheHandler`. Point `cacheHandler` in `next.config` at
42
+ * `@tetherto/dev-websites-core/cache/handler`. Reads `MONGODB_URI` itself so
43
+ * sites do not ship their own handler or Mongo client.
44
+ *
45
+ * Kept off the `/cache` barrel: importing the revalidation helpers must not
46
+ * open a database connection.
47
+ */
48
+ declare const _default: MongoCacheHandlerConstructor;
49
+ //#endregion
50
+ export { _default as default };
51
+ //# sourceMappingURL=handler.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.mts","names":[],"sources":["../../src/cache/types.ts","../../src/cache/createMongoCacheHandler.ts","../../src/cache/handler.ts"],"mappings":";;;;;;KAgCY;EACV;EACA;EACA;EACA,UAAU;;EAEV,cAAc,0BAA0B;GACvC;;KAWS;EACV;EACA,OAAO;;;KAIG;EACV;;;;KCtBU;EACV,IAAI,cAAc,QAAQ;EAC1B,IAAI,aAAa,OAAO,oBAAoB,MAAM,kBAAkB;EACpE,OAAO,cAAc;EACrB;EACA,cAAc,yBAAyB,sBAAsB;;;;;;KAOnD,oCAAoC,kBAAkB"}
@@ -0,0 +1,178 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { MongoClient } from "mongodb";
4
+ //#region src/cache/shouldSkipCacheWrite.ts
5
+ /**
6
+ * ISR pages with `dynamicParams = true` still render for unknown paths, call
7
+ * `notFound()`, and Next then asks the handler to persist the 404. Scanners
8
+ * hitting junk URLs rewrite that row on every request and bloat the cache
9
+ * collection and the Mongo oplog, so non-success page/route responses are
10
+ * never stored.
11
+ *
12
+ * `FETCH` entries are unaffected: Next nests their upstream status under
13
+ * `value.data.status`, not the top-level `status` read here.
14
+ */
15
+ function shouldSkipCacheWrite(value) {
16
+ if (!value || typeof value !== "object") return false;
17
+ const status = value.status;
18
+ if (typeof status === "number" && (status < 200 || status >= 400)) return true;
19
+ if (typeof value.html === "string" && value.html.includes("id=\"__next_error__\"")) return true;
20
+ return false;
21
+ }
22
+ //#endregion
23
+ //#region src/cache/createMongoCacheHandler.ts
24
+ const DEFAULT_DB_NAME = "next_cache";
25
+ const DEFAULT_COLLECTION_NAME = "fetch_cache";
26
+ /**
27
+ * `get()` deliberately returns rows of any age so Next can serve them stale
28
+ * while it revalidates, which means nothing expires on its own. Mongo drops a
29
+ * row this long after its last write; anything still in use is rewritten by
30
+ * revalidation far sooner, so only paths nobody requests anymore fall out.
31
+ */
32
+ const DEFAULT_ENTRY_TTL_SECONDS = 604800;
33
+ function readBuildId() {
34
+ try {
35
+ return readFileSync(path.join(process.cwd(), ".next", "BUILD_ID"), "utf8").trim();
36
+ } catch {
37
+ return "development";
38
+ }
39
+ }
40
+ /**
41
+ * Next only passes `ctx.tags` for fetch entries. Page and route entries carry
42
+ * theirs — including the `_N_T_<path>` tag behind `revalidatePath` — in the
43
+ * response headers, so without this fallback `revalidateTag` can never purge
44
+ * rendered HTML and CMS edits wait out the route's full revalidate window.
45
+ */
46
+ function resolveTags(value, ctx) {
47
+ if (ctx?.tags?.length) return ctx.tags;
48
+ const header = value?.headers?.["x-next-cache-tags"];
49
+ return typeof header === "string" ? header.split(",").filter(Boolean) : [];
50
+ }
51
+ /**
52
+ * Mongo-backed `cacheHandler` for Next's ISR / route / fetch cache, so every
53
+ * instance of a deployment shares one cache instead of a per-pod filesystem.
54
+ *
55
+ * Sites normally point `cacheHandler` at `@tetherto/dev-websites-core/cache/handler`,
56
+ * which builds the client from `MONGODB_URI`. Use this factory when the site
57
+ * already owns a client and wants to pass it in.
58
+ *
59
+ * Wire it with `cacheMaxMemorySize: 0`, and include
60
+ * `node_modules/@tetherto/dev-websites-core/dist/cache` in
61
+ * `outputFileTracingIncludes` so the standalone server can load the handler.
62
+ */
63
+ function createMongoCacheHandler(options) {
64
+ const { clientPromise, dbName = DEFAULT_DB_NAME, collectionName = DEFAULT_COLLECTION_NAME, entryTtlSeconds = DEFAULT_ENTRY_TTL_SECONDS } = options;
65
+ /**
66
+ * Rendered HTML embeds the chunk filenames of the build that produced it, so
67
+ * an entry left by a previous build loads but never hydrates — its chunks are
68
+ * gone (404). Page keys are therefore scoped per build, while instances of
69
+ * the same build keep sharing them.
70
+ *
71
+ * During `next build` `.next/BUILD_ID` does not exist yet, which is safe
72
+ * because Next writes prerenders to disk and never calls `set()` for page
73
+ * entries during the build.
74
+ */
75
+ const buildId = options.buildId ?? readBuildId();
76
+ /**
77
+ * Page and route entries are keyed by pathname; fetch entries are keyed by a
78
+ * content hash and stay shared across builds, so upstream data survives a
79
+ * deploy.
80
+ */
81
+ const scopeKey = (key) => key.startsWith("/") ? `${buildId}${key}` : key;
82
+ let indexPromise;
83
+ const getCollection = async () => {
84
+ const collection = (await clientPromise).db(dbName).collection(collectionName);
85
+ indexPromise ??= Promise.all([
86
+ collection.createIndex({ key: 1 }, { unique: true }),
87
+ collection.createIndex({ tags: 1 }),
88
+ collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
89
+ ]).catch(() => {});
90
+ await indexPromise;
91
+ return collection;
92
+ };
93
+ return class MongoCacheHandlerImpl {
94
+ /**
95
+ * Returns the entry whatever its age. Next compares `lastModified` against
96
+ * the route's own revalidate window and decides itself whether to serve it
97
+ * as fresh, serve it stale while refreshing in the background, or
98
+ * re-render. Dropping stale rows here turns every expiry into a blocking
99
+ * render.
100
+ */
101
+ async get(key) {
102
+ const collection = await getCollection();
103
+ const scoped = scopeKey(key);
104
+ const entry = await collection.findOne({ key: scoped });
105
+ if (!entry) return null;
106
+ if (shouldSkipCacheWrite(entry.value)) {
107
+ await collection.deleteOne({ key: scoped });
108
+ return null;
109
+ }
110
+ const value = entry.value ?? null;
111
+ const segmentData = value?.segmentData;
112
+ return {
113
+ lastModified: entry.lastModified,
114
+ value: segmentData && !(segmentData instanceof Map) ? {
115
+ ...value,
116
+ segmentData: new Map(Object.entries(segmentData))
117
+ } : value
118
+ };
119
+ }
120
+ async set(key, value, ctx) {
121
+ if (shouldSkipCacheWrite(value)) return;
122
+ const collection = await getCollection();
123
+ const scoped = scopeKey(key);
124
+ const now = Date.now();
125
+ await collection.updateOne({ key: scoped }, { $set: {
126
+ key: scoped,
127
+ value,
128
+ lastModified: now,
129
+ expiresAt: new Date(now + entryTtlSeconds * 1e3),
130
+ tags: resolveTags(value, ctx)
131
+ } }, { upsert: true });
132
+ }
133
+ async delete(key) {
134
+ await (await getCollection()).deleteOne({ key: scopeKey(key) });
135
+ }
136
+ resetRequestCache() {}
137
+ async revalidateTag(tags, _durations) {
138
+ const list = typeof tags === "string" ? [tags] : tags;
139
+ if (!list?.length) return;
140
+ await (await getCollection()).deleteMany({ tags: { $in: list } });
141
+ }
142
+ };
143
+ }
144
+ //#endregion
145
+ //#region src/cache/createMongoClient.ts
146
+ const options = { promoteBuffers: true };
147
+ /**
148
+ * One Mongo client for the cache handler. In development the promise lives on
149
+ * `globalThis` so hot reload does not open a new connection on every compile.
150
+ *
151
+ * `promoteBuffers` makes BSON Binary come back as Node `Buffer`, which is what
152
+ * Next stores for route and image cache entries.
153
+ */
154
+ function createMongoClientPromise() {
155
+ const uri = process.env.MONGODB_URI;
156
+ if (!uri) throw new Error("Missing MONGODB_URI");
157
+ if (process.env.NODE_ENV === "development") {
158
+ const globalForMongo = globalThis;
159
+ globalForMongo._mongoClientPromise ??= new MongoClient(uri, options).connect();
160
+ return globalForMongo._mongoClientPromise;
161
+ }
162
+ return new MongoClient(uri, options).connect();
163
+ }
164
+ //#endregion
165
+ //#region src/cache/handler.ts
166
+ /**
167
+ * Default Next.js `cacheHandler`. Point `cacheHandler` in `next.config` at
168
+ * `@tetherto/dev-websites-core/cache/handler`. Reads `MONGODB_URI` itself so
169
+ * sites do not ship their own handler or Mongo client.
170
+ *
171
+ * Kept off the `/cache` barrel: importing the revalidation helpers must not
172
+ * open a database connection.
173
+ */
174
+ var handler_default = createMongoCacheHandler({ clientPromise: createMongoClientPromise() });
175
+ //#endregion
176
+ export { handler_default as default };
177
+
178
+ //# sourceMappingURL=handler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.mjs","names":[],"sources":["../../src/cache/shouldSkipCacheWrite.ts","../../src/cache/createMongoCacheHandler.ts","../../src/cache/createMongoClient.ts","../../src/cache/handler.ts"],"sourcesContent":["import type { CachedValue } from './types.js'\n\n/**\n * ISR pages with `dynamicParams = true` still render for unknown paths, call\n * `notFound()`, and Next then asks the handler to persist the 404. Scanners\n * hitting junk URLs rewrite that row on every request and bloat the cache\n * collection and the Mongo oplog, so non-success page/route responses are\n * never stored.\n *\n * `FETCH` entries are unaffected: Next nests their upstream status under\n * `value.data.status`, not the top-level `status` read here.\n */\nexport function shouldSkipCacheWrite(value: CachedValue | null | undefined): boolean {\n if (!value || typeof value !== 'object') return false\n\n const status = value.status\n if (typeof status === 'number' && (status < 200 || status >= 400)) return true\n\n if (typeof value.html === 'string' && value.html.includes('id=\"__next_error__\"')) {\n return true\n }\n\n return false\n}\n","import { readFileSync } from 'node:fs'\nimport path from 'node:path'\n\nimport { shouldSkipCacheWrite } from './shouldSkipCacheWrite.js'\n\nimport type {\n CacheSetContext,\n CachedEntry,\n CachedValue,\n MongoCacheClient,\n MongoCacheCollection,\n} from './types.js'\n\nconst DEFAULT_DB_NAME = 'next_cache'\nconst DEFAULT_COLLECTION_NAME = 'fetch_cache'\n\n/**\n * `get()` deliberately returns rows of any age so Next can serve them stale\n * while it revalidates, which means nothing expires on its own. Mongo drops a\n * row this long after its last write; anything still in use is rewritten by\n * revalidation far sooner, so only paths nobody requests anymore fall out.\n */\nconst DEFAULT_ENTRY_TTL_SECONDS = 60 * 60 * 24 * 7\n\nexport type MongoCacheHandlerOptions = {\n /** Connected client, owned by the consuming app so this package needs no driver. */\n clientPromise: Promise<MongoCacheClient>\n dbName?: string\n collectionName?: string\n /** TTL applied on every write. Defaults to 7 days. */\n entryTtlSeconds?: number\n /** Overrides the build id read from `.next/BUILD_ID`. */\n buildId?: string\n}\n\nexport type MongoCacheHandler = {\n get(key: string): Promise<CachedEntry | null>\n set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void>\n delete(key: string): Promise<void>\n resetRequestCache(): void\n revalidateTag(tags: string | string[], durations?: unknown): Promise<void>\n}\n\n/**\n * Next instantiates the configured handler with `new Handler(ctx)`, so the\n * factory returns a class rather than an instance.\n */\nexport type MongoCacheHandlerConstructor = new (ctx?: unknown) => MongoCacheHandler\n\nfunction readBuildId(): string {\n try {\n return readFileSync(path.join(process.cwd(), '.next', 'BUILD_ID'), 'utf8').trim()\n } catch {\n return 'development'\n }\n}\n\n/**\n * Next only passes `ctx.tags` for fetch entries. Page and route entries carry\n * theirs — including the `_N_T_<path>` tag behind `revalidatePath` — in the\n * response headers, so without this fallback `revalidateTag` can never purge\n * rendered HTML and CMS edits wait out the route's full revalidate window.\n */\nfunction resolveTags(value: CachedValue | null | undefined, ctx?: CacheSetContext): string[] {\n if (ctx?.tags?.length) return ctx.tags\n\n const header = value?.headers?.['x-next-cache-tags']\n return typeof header === 'string' ? header.split(',').filter(Boolean) : []\n}\n\n/**\n * Mongo-backed `cacheHandler` for Next's ISR / route / fetch cache, so every\n * instance of a deployment shares one cache instead of a per-pod filesystem.\n *\n * Sites normally point `cacheHandler` at `@tetherto/dev-websites-core/cache/handler`,\n * which builds the client from `MONGODB_URI`. Use this factory when the site\n * already owns a client and wants to pass it in.\n *\n * Wire it with `cacheMaxMemorySize: 0`, and include\n * `node_modules/@tetherto/dev-websites-core/dist/cache` in\n * `outputFileTracingIncludes` so the standalone server can load the handler.\n */\nexport function createMongoCacheHandler(\n options: MongoCacheHandlerOptions\n): MongoCacheHandlerConstructor {\n const {\n clientPromise,\n dbName = DEFAULT_DB_NAME,\n collectionName = DEFAULT_COLLECTION_NAME,\n entryTtlSeconds = DEFAULT_ENTRY_TTL_SECONDS,\n } = options\n\n /**\n * Rendered HTML embeds the chunk filenames of the build that produced it, so\n * an entry left by a previous build loads but never hydrates — its chunks are\n * gone (404). Page keys are therefore scoped per build, while instances of\n * the same build keep sharing them.\n *\n * During `next build` `.next/BUILD_ID` does not exist yet, which is safe\n * because Next writes prerenders to disk and never calls `set()` for page\n * entries during the build.\n */\n const buildId = options.buildId ?? readBuildId()\n\n /**\n * Page and route entries are keyed by pathname; fetch entries are keyed by a\n * content hash and stay shared across builds, so upstream data survives a\n * deploy.\n */\n const scopeKey = (key: string): string => (key.startsWith('/') ? `${buildId}${key}` : key)\n\n // Next builds a fresh handler per request, so this has to live outside the\n // class to stay a one-off instead of an extra round-trip on every read.\n let indexPromise: Promise<unknown> | undefined\n\n const getCollection = async (): Promise<MongoCacheCollection> => {\n const client = await clientPromise\n const collection = client.db(dbName).collection(collectionName)\n\n indexPromise ??= Promise.all([\n collection.createIndex({ key: 1 }, { unique: true }),\n // `revalidateTag` deletes by `{ tags: { $in } }`; without this it COLLSCANs.\n collection.createIndex({ tags: 1 }),\n collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }),\n ]).catch(() => {})\n await indexPromise\n\n return collection\n }\n\n return class MongoCacheHandlerImpl implements MongoCacheHandler {\n /**\n * Returns the entry whatever its age. Next compares `lastModified` against\n * the route's own revalidate window and decides itself whether to serve it\n * as fresh, serve it stale while refreshing in the background, or\n * re-render. Dropping stale rows here turns every expiry into a blocking\n * render.\n */\n async get(key: string): Promise<CachedEntry | null> {\n const collection = await getCollection()\n const scoped = scopeKey(key)\n const entry = await collection.findOne({ key: scoped })\n if (!entry) return null\n\n // Heals rows written before error responses were skipped.\n if (shouldSkipCacheWrite(entry.value)) {\n await collection.deleteOne({ key: scoped })\n return null\n }\n\n const value = entry.value ?? null\n const segmentData = value?.segmentData\n\n return {\n lastModified: entry.lastModified,\n value:\n segmentData && !(segmentData instanceof Map)\n ? { ...value, segmentData: new Map(Object.entries(segmentData)) }\n : value,\n }\n }\n\n async set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void> {\n if (shouldSkipCacheWrite(value)) return\n\n const collection = await getCollection()\n const scoped = scopeKey(key)\n const now = Date.now()\n\n await collection.updateOne(\n { key: scoped },\n {\n $set: {\n key: scoped,\n value,\n lastModified: now,\n expiresAt: new Date(now + entryTtlSeconds * 1000),\n tags: resolveTags(value, ctx),\n },\n },\n { upsert: true }\n )\n }\n\n async delete(key: string): Promise<void> {\n const collection = await getCollection()\n await collection.deleteOne({ key: scopeKey(key) })\n }\n\n resetRequestCache(): void {}\n\n async revalidateTag(tags: string | string[], _durations?: unknown): Promise<void> {\n const list = typeof tags === 'string' ? [tags] : tags\n if (!list?.length) return\n\n const collection = await getCollection()\n await collection.deleteMany({ tags: { $in: list } })\n }\n }\n}\n","import { MongoClient, type MongoClientOptions } from 'mongodb'\n\nconst options: MongoClientOptions = { promoteBuffers: true }\n\ntype GlobalWithMongo = typeof globalThis & {\n _mongoClientPromise?: Promise<MongoClient>\n}\n\n/**\n * One Mongo client for the cache handler. In development the promise lives on\n * `globalThis` so hot reload does not open a new connection on every compile.\n *\n * `promoteBuffers` makes BSON Binary come back as Node `Buffer`, which is what\n * Next stores for route and image cache entries.\n */\nexport function createMongoClientPromise(): Promise<MongoClient> {\n const uri = process.env.MONGODB_URI\n if (!uri) {\n throw new Error('Missing MONGODB_URI')\n }\n\n if (process.env.NODE_ENV === 'development') {\n const globalForMongo = globalThis as GlobalWithMongo\n globalForMongo._mongoClientPromise ??= new MongoClient(uri, options).connect()\n return globalForMongo._mongoClientPromise\n }\n\n return new MongoClient(uri, options).connect()\n}\n","import { createMongoCacheHandler } from './createMongoCacheHandler.js'\nimport { createMongoClientPromise } from './createMongoClient.js'\n\n/**\n * Default Next.js `cacheHandler`. Point `cacheHandler` in `next.config` at\n * `@tetherto/dev-websites-core/cache/handler`. Reads `MONGODB_URI` itself so\n * sites do not ship their own handler or Mongo client.\n *\n * Kept off the `/cache` barrel: importing the revalidation helpers must not\n * open a database connection.\n */\nexport default createMongoCacheHandler({ clientPromise: createMongoClientPromise() })\n"],"mappings":"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"}
@@ -0,0 +1,170 @@
1
+ import { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from "payload";
2
+ //#region src/cache/types.d.ts
3
+ /**
4
+ * Structural subset of the `mongodb` driver that the cache handler needs.
5
+ *
6
+ * Declaring it here instead of importing from `mongodb` keeps the driver out of
7
+ * the `/cache` entry, so revalidation helpers can be imported without loading it.
8
+ * A real `MongoClient` is assignable to `MongoCacheClient`.
9
+ */
10
+ type MongoCacheClient = {
11
+ db(name: string): {
12
+ collection(name: string): MongoCacheCollection;
13
+ };
14
+ };
15
+ type MongoCacheFilter = Record<string, unknown>;
16
+ type MongoCacheCollection = {
17
+ findOne(filter: MongoCacheFilter): Promise<MongoCacheRow | null>;
18
+ updateOne(filter: MongoCacheFilter, update: MongoCacheFilter, options: {
19
+ upsert: boolean;
20
+ }): Promise<unknown>;
21
+ deleteOne(filter: MongoCacheFilter): Promise<unknown>;
22
+ deleteMany(filter: MongoCacheFilter): Promise<unknown>;
23
+ createIndex(spec: Record<string, number>, options?: Record<string, unknown>): Promise<unknown>;
24
+ };
25
+ /**
26
+ * A cache entry as Next hands it to `set()`. Only the fields this handler reads
27
+ * are named; `kind` is `'APP_PAGE'`, `'APP_ROUTE'`, `'PAGES'`, `'FETCH'`, or
28
+ * `'IMAGE'`, and the rest of the payload is stored verbatim.
29
+ */
30
+ type CachedValue = {
31
+ kind?: string;
32
+ status?: number;
33
+ html?: string;
34
+ headers?: Record<string, string | undefined>;
35
+ /** Stored as a plain object; Next iterates it as a Map, so `get()` rebuilds one. */
36
+ segmentData?: Record<string, unknown> | Map<string, unknown>;
37
+ [key: string]: unknown;
38
+ };
39
+ type MongoCacheRow = {
40
+ key?: string;
41
+ lastModified?: number;
42
+ tags?: string[];
43
+ value?: CachedValue | null;
44
+ [key: string]: unknown;
45
+ };
46
+ type CachedEntry = {
47
+ lastModified?: number;
48
+ value: CachedValue | null;
49
+ };
50
+ /** The `ctx` Next passes to `set()`; only `tags` is meaningful here. */
51
+ type CacheSetContext = {
52
+ tags?: string[];
53
+ };
54
+ //#endregion
55
+ //#region src/cache/createMongoCacheHandler.d.ts
56
+ type MongoCacheHandlerOptions = {
57
+ /** Connected client, owned by the consuming app so this package needs no driver. */
58
+ clientPromise: Promise<MongoCacheClient>;
59
+ dbName?: string;
60
+ collectionName?: string;
61
+ /** TTL applied on every write. Defaults to 7 days. */
62
+ entryTtlSeconds?: number;
63
+ /** Overrides the build id read from `.next/BUILD_ID`. */
64
+ buildId?: string;
65
+ };
66
+ type MongoCacheHandler = {
67
+ get(key: string): Promise<CachedEntry | null>;
68
+ set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void>;
69
+ delete(key: string): Promise<void>;
70
+ resetRequestCache(): void;
71
+ revalidateTag(tags: string | string[], durations?: unknown): Promise<void>;
72
+ };
73
+ /**
74
+ * Next instantiates the configured handler with `new Handler(ctx)`, so the
75
+ * factory returns a class rather than an instance.
76
+ */
77
+ type MongoCacheHandlerConstructor = new (ctx?: unknown) => MongoCacheHandler;
78
+ /**
79
+ * Mongo-backed `cacheHandler` for Next's ISR / route / fetch cache, so every
80
+ * instance of a deployment shares one cache instead of a per-pod filesystem.
81
+ *
82
+ * Sites normally point `cacheHandler` at `@tetherto/dev-websites-core/cache/handler`,
83
+ * which builds the client from `MONGODB_URI`. Use this factory when the site
84
+ * already owns a client and wants to pass it in.
85
+ *
86
+ * Wire it with `cacheMaxMemorySize: 0`, and include
87
+ * `node_modules/@tetherto/dev-websites-core/dist/cache` in
88
+ * `outputFileTracingIncludes` so the standalone server can load the handler.
89
+ */
90
+ declare function createMongoCacheHandler(options: MongoCacheHandlerOptions): MongoCacheHandlerConstructor;
91
+ //#endregion
92
+ //#region src/cache/shouldSkipCacheWrite.d.ts
93
+ /**
94
+ * ISR pages with `dynamicParams = true` still render for unknown paths, call
95
+ * `notFound()`, and Next then asks the handler to persist the 404. Scanners
96
+ * hitting junk URLs rewrite that row on every request and bloat the cache
97
+ * collection and the Mongo oplog, so non-success page/route responses are
98
+ * never stored.
99
+ *
100
+ * `FETCH` entries are unaffected: Next nests their upstream status under
101
+ * `value.data.status`, not the top-level `status` read here.
102
+ */
103
+ declare function shouldSkipCacheWrite(value: CachedValue | null | undefined): boolean;
104
+ //#endregion
105
+ //#region src/cache/shouldSkipRevalidate.d.ts
106
+ /**
107
+ * Standalone scripts (e.g. seed) pass `context: { disableRevalidate: true }`
108
+ * through the Payload Local API so collection hooks can skip cache invalidation —
109
+ * `revalidatePath` throws outside an active Next.js request context.
110
+ */
111
+ declare function shouldSkipRevalidate(req: PayloadRequest | undefined): boolean;
112
+ //#endregion
113
+ //#region src/cache/safeRevalidatePath.d.ts
114
+ declare function safeRevalidatePath(path: string, type?: 'layout' | 'page'): void;
115
+ declare function safeRevalidateTag(tag: string, profile: 'max'): void;
116
+ //#endregion
117
+ //#region src/cache/createPublishedSlugRevalidateHooks.d.ts
118
+ type PublishedSlugRevalidateHandlers = {
119
+ onPublishedSlugs: (slugs: Iterable<string>) => void;
120
+ onPublishedList?: () => void;
121
+ };
122
+ /**
123
+ * Payload hooks that revalidate when a draft/published doc with a `slug` changes.
124
+ * Skips when `req.context.disableRevalidate` is set (seeds).
125
+ */
126
+ declare function createPublishedSlugRevalidateHooks(handlers: PublishedSlugRevalidateHandlers): {
127
+ afterChange: CollectionAfterChangeHook;
128
+ afterDelete: CollectionAfterDeleteHook;
129
+ };
130
+ /** Hooks that always call `onChange` (e.g. categories / tags that affect listings). */
131
+ declare function createAlwaysRevalidateHooks(onChange: () => void): {
132
+ afterChange: CollectionAfterChangeHook;
133
+ afterDelete: CollectionAfterDeleteHook;
134
+ };
135
+ //#endregion
136
+ //#region src/cache/createPathRevalidators.d.ts
137
+ type BlogRevalidatorPaths = {
138
+ /** e.g. `(slug, locale) => \`/${locale}/blog/${slug}\`` — concrete URL, no `type` */
139
+ post: (slug: string, locale: string) => string;
140
+ /** Route pattern including route groups, e.g. `/[locale]/(marketing)/blog/[slug]` */
141
+ postDynamic: string;
142
+ list: string;
143
+ serverSitemap?: string;
144
+ sitemap?: string;
145
+ llms?: string;
146
+ };
147
+ type BlogRevalidators = {
148
+ revalidatePost: (slugs: Iterable<string>) => void;
149
+ revalidateList: () => void;
150
+ revalidateAllSurfaces: () => void;
151
+ };
152
+ declare function createBlogRevalidators(locales: readonly string[], paths: BlogRevalidatorPaths): BlogRevalidators;
153
+ type ChangelogRevalidatorPaths = {
154
+ detail: (slug: string, locale: string) => string;
155
+ detailDynamic: string;
156
+ list: string;
157
+ rss?: string;
158
+ serverSitemap?: string;
159
+ sitemap?: string;
160
+ llms?: string;
161
+ };
162
+ type ChangelogRevalidators = {
163
+ revalidateEntry: (slugs: Iterable<string>) => void;
164
+ revalidateList: () => void;
165
+ revalidateAllSurfaces: () => void;
166
+ };
167
+ declare function createChangelogRevalidators(locales: readonly string[], paths: ChangelogRevalidatorPaths): ChangelogRevalidators;
168
+ //#endregion
169
+ export { type BlogRevalidatorPaths, type BlogRevalidators, type CacheSetContext, type CachedEntry, type CachedValue, type ChangelogRevalidatorPaths, type ChangelogRevalidators, type MongoCacheClient, type MongoCacheCollection, type MongoCacheFilter, type MongoCacheHandler, type MongoCacheHandlerConstructor, type MongoCacheHandlerOptions, type MongoCacheRow, type PublishedSlugRevalidateHandlers, createAlwaysRevalidateHooks, createBlogRevalidators, createChangelogRevalidators, createMongoCacheHandler, createPublishedSlugRevalidateHooks, safeRevalidatePath, safeRevalidateTag, shouldSkipCacheWrite, shouldSkipRevalidate };
170
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cache/types.ts","../../src/cache/createMongoCacheHandler.ts","../../src/cache/shouldSkipCacheWrite.ts","../../src/cache/shouldSkipRevalidate.ts","../../src/cache/safeRevalidatePath.ts","../../src/cache/createPublishedSlugRevalidateHooks.ts","../../src/cache/createPathRevalidators.ts"],"mappings":";;;;;;;;;KAOY;EACV,GAAG;IACD,WAAW,eAAe;;;KAIlB,mBAAmB;KAEnB;EACV,QAAQ,QAAQ,mBAAmB,QAAQ;EAC3C,UACE,QAAQ,kBACR,QAAQ,kBACR;IAAW;MACV;EACH,UAAU,QAAQ,mBAAmB;EACrC,WAAW,QAAQ,mBAAmB;EACtC,YAAY,MAAM,wBAAwB,UAAU,0BAA0B;;;;;;;KAQpE;EACV;EACA;EACA;EACA,UAAU;;EAEV,cAAc,0BAA0B;GACvC;;KAGS;EACV;EACA;EACA;EACA,QAAQ;GACP;;KAGS;EACV;EACA,OAAO;;;KAIG;EACV;;;;KCjCU;;EAEV,eAAe,QAAQ;EACvB;EACA;;EAEA;;EAEA;;KAGU;EACV,IAAI,cAAc,QAAQ;EAC1B,IAAI,aAAa,OAAO,oBAAoB,MAAM,kBAAkB;EACpE,OAAO,cAAc;EACrB;EACA,cAAc,yBAAyB,sBAAsB;;;;;;KAOnD,oCAAoC,kBAAkB;;;;;;;;;;;;;iBAmClD,wBACd,SAAS,2BACR;;;;;;;;;;;;;iBCxEa,qBAAqB,OAAO;;;;;;;;iBCL5B,qBAAqB,KAAK;;;iBCsB1B,mBAAmB,cAAc;iBAUjC,kBAAkB,aAAa;;;KCnCnC;EACV,mBAAmB,OAAO;EAC1B;;;;;;iBAOc,mCAAmC,UAAU;EAC3D,aAAa;EACb,aAAa;;;iBA8BC,4BAA4B;EAC1C,aAAa;EACb,aAAa;;;;KC7CH;;EAEV,OAAO,cAAc;;EAErB;EACA;EACA;EACA;EACA;;KAGU;EACV,iBAAiB,OAAO;EACxB;EACA;;iBAGc,uBACd,4BACA,OAAO,uBACN;KA4BS;EACV,SAAS,cAAc;EACvB;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV,kBAAkB,OAAO;EACzB;EACA;;iBAGc,4BACd,4BACA,OAAO,4BACN"}