@krantho/server-next 0.0.0-stage → 1.0.1

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 Descoped
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.
@@ -0,0 +1,34 @@
1
+ import type { CacheHandler, CacheHandlerContext, CacheHandlerValue } from 'next/dist/server/lib/incremental-cache';
2
+ import type { GetIncrementalFetchCacheContext, GetIncrementalResponseCacheContext, IncrementalCacheValue, SetIncrementalFetchCacheContext, SetIncrementalResponseCacheContext } from 'next/dist/server/response-cache/types';
3
+ /**
4
+ * Next.js's cache — rendered pages, route responses and data cache entries —
5
+ * kept in the site's own database, so every instance of a multi-instance site
6
+ * serves and invalidates one cache.
7
+ *
8
+ * An entry is not served when one of its tags was expired after it was
9
+ * written, and is served stale while Next re-renders it when a tag was only
10
+ * marked stale. The tags are a fetch entry's own and implicit tags, and the
11
+ * `x-next-cache-tags` header of a page or route.
12
+ *
13
+ * Enable it in `next.config.ts` with `cacheHandler:
14
+ * require.resolve('@krantho/server-next/cache-handler')` and
15
+ * `cacheMaxMemorySize: 0`, so no instance keeps a private copy.
16
+ */
17
+ declare class KranthoCacheHandler implements CacheHandler {
18
+ private readonly revalidatedTags;
19
+ constructor(context: CacheHandlerContext);
20
+ get(key: string, context: GetIncrementalFetchCacheContext | GetIncrementalResponseCacheContext): Promise<CacheHandlerValue | null>;
21
+ set(key: string, data: IncrementalCacheValue | null, context: SetIncrementalFetchCacheContext | SetIncrementalResponseCacheContext): Promise<void>;
22
+ /**
23
+ * No durations, or an expiry of zero, expires the tags now. Any later expiry
24
+ * — the `'max'` profile, a `cacheLife` profile — marks them stale now: the
25
+ * next read serves the entry and re-renders it.
26
+ */
27
+ revalidateTag(tags: string | string[], durations?: {
28
+ expire?: number;
29
+ }): Promise<void>;
30
+ /** Nothing is kept per request */
31
+ resetRequestCache(): void;
32
+ }
33
+ export default KranthoCacheHandler;
34
+ //# sourceMappingURL=cache-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-handler.d.ts","sourceRoot":"","sources":["../src/cache-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,wCAAwC,CAAA;AAC/C,OAAO,KAAK,EACV,+BAA+B,EAC/B,kCAAkC,EAClC,qBAAqB,EACrB,+BAA+B,EAC/B,kCAAkC,EACnC,MAAM,uCAAuC,CAAA;AA2B9C;;;;;;;;;;;;;GAaG;AACH,cAAM,mBAAoB,YAAW,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmB;gBAEvC,OAAO,EAAE,mBAAmB;IAIlC,GAAG,CACP,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,+BAA+B,GAAG,kCAAkC,GAC5E,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAgB9B,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,qBAAqB,GAAG,IAAI,EAClC,OAAO,EAAE,+BAA+B,GAAG,kCAAkC,GAC5E,OAAO,CAAC,IAAI,CAAC;IAMhB;;;;OAIG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5F,kCAAkC;IAClC,iBAAiB,IAAI,IAAI;CAC1B;AAYD,eAAe,mBAAmB,CAAA"}
@@ -0,0 +1,90 @@
1
+ import { resolvePersistence } from '@krantho/config';
2
+ import { openPageCacheStore } from '@krantho/server';
3
+ import { FETCH_KIND, fromStoredValue, responseTags, toStoredValue } from './cache-value.js';
4
+ /**
5
+ * `lastModified` of an entry whose tags were marked stale. Next reads it as
6
+ * `isStale: -1` and renders the page again before it responds, so a visitor
7
+ * never gets a page older than its tags. A fetch entry's age then exceeds any
8
+ * revalidate period, so Next re-fetches it. Next does not read `cacheState` from
9
+ * a custom handler.
10
+ */
11
+ const STALE_LAST_MODIFIED = -1;
12
+ let opening;
13
+ /**
14
+ * The store is opened once per process. Next loads this module by path with no
15
+ * access to `krantho.config.ts`, so the database comes from the environment.
16
+ * A failed open is not kept: the next call tries again.
17
+ */
18
+ function pageCacheStore() {
19
+ opening ??= openPageCacheStore(resolvePersistence(process.env)).catch((error) => {
20
+ opening = undefined;
21
+ throw error;
22
+ });
23
+ return opening;
24
+ }
25
+ /**
26
+ * Next.js's cache — rendered pages, route responses and data cache entries —
27
+ * kept in the site's own database, so every instance of a multi-instance site
28
+ * serves and invalidates one cache.
29
+ *
30
+ * An entry is not served when one of its tags was expired after it was
31
+ * written, and is served stale while Next re-renders it when a tag was only
32
+ * marked stale. The tags are a fetch entry's own and implicit tags, and the
33
+ * `x-next-cache-tags` header of a page or route.
34
+ *
35
+ * Enable it in `next.config.ts` with `cacheHandler:
36
+ * require.resolve('@krantho/server-next/cache-handler')` and
37
+ * `cacheMaxMemorySize: 0`, so no instance keeps a private copy.
38
+ */
39
+ class KranthoCacheHandler {
40
+ revalidatedTags;
41
+ constructor(context) {
42
+ this.revalidatedTags = context.revalidatedTags;
43
+ }
44
+ async get(key, context) {
45
+ const store = await pageCacheStore();
46
+ const entry = await store.get(key);
47
+ if (!entry)
48
+ return null;
49
+ const value = fromStoredValue(entry.value);
50
+ const tags = [...new Set([...entry.tags, ...requestTags(context), ...responseTags(value)])];
51
+ // In minimal mode Next passes the tags revalidated during this request
52
+ if (context.kind === FETCH_KIND && tags.some((tag) => this.revalidatedTags.includes(tag))) {
53
+ return null;
54
+ }
55
+ const state = tags.length > 0 ? await store.tagsRevalidatedSince(tags, entry.lastModified) : 'fresh';
56
+ if (state === 'expired')
57
+ return null;
58
+ return { lastModified: state === 'stale' ? STALE_LAST_MODIFIED : entry.lastModified, value };
59
+ }
60
+ async set(key, data, context) {
61
+ const store = await pageCacheStore();
62
+ const tags = context.fetchCache ? (context.tags ?? []) : responseTags(data);
63
+ await store.set(key, toStoredValue(data), tags);
64
+ }
65
+ /**
66
+ * No durations, or an expiry of zero, expires the tags now. Any later expiry
67
+ * — the `'max'` profile, a `cacheLife` profile — marks them stale now: the
68
+ * next read serves the entry and re-renders it.
69
+ */
70
+ async revalidateTag(tags, durations) {
71
+ const list = typeof tags === 'string' ? [tags] : tags;
72
+ if (list.length === 0)
73
+ return;
74
+ const expire = durations === undefined || (durations.expire !== undefined && durations.expire <= 0);
75
+ const store = await pageCacheStore();
76
+ await store.markTags(list, { expire });
77
+ }
78
+ /** Nothing is kept per request */
79
+ resetRequestCache() { }
80
+ }
81
+ /** A fetch entry's tags and the implicit tags of the page reading it */
82
+ function requestTags(context) {
83
+ if (context.kind !== FETCH_KIND)
84
+ return [];
85
+ return [...(context.tags ?? []), ...(context.softTags ?? [])];
86
+ }
87
+ // Next loads a cache handler by path and constructs the module's default export,
88
+ // so this entry point is the one module with a default export.
89
+ export default KranthoCacheHandler;
90
+ //# sourceMappingURL=cache-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-handler.js","sourceRoot":"","sources":["../src/cache-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { PageCacheValue } from '@krantho/server';
2
+ import type { IncrementalCacheKind, IncrementalCacheValue } from 'next/dist/server/response-cache/types';
3
+ export declare const FETCH_KIND: IncrementalCacheKind.FETCH;
4
+ /**
5
+ * Convert a value Next caches into one the page cache store keeps.
6
+ *
7
+ * Binary fields (`rscData`, a route's `body`, an image's `buffer`) are Buffers,
8
+ * which the store keeps as bytes. A page's `segmentData` Map becomes a list of
9
+ * `[segmentPath, bytes]` pairs. `pageData` and redirect `props` are JSON, as
10
+ * Next itself writes them to disk.
11
+ */
12
+ export declare function toStoredValue(value: IncrementalCacheValue | null): PageCacheValue;
13
+ /**
14
+ * Restore a stored value into the shape Next reads: bytes back to Buffers and
15
+ * a page's segment pairs back to a Map.
16
+ */
17
+ export declare function fromStoredValue(stored: PageCacheValue): IncrementalCacheValue | null;
18
+ /**
19
+ * The tags Next stamped on a rendered page or route, from its
20
+ * `x-next-cache-tags` header — Next's implicit path tags included.
21
+ */
22
+ export declare function responseTags(value: IncrementalCacheValue | null): string[];
23
+ //# sourceMappingURL=cache-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-value.d.ts","sourceRoot":"","sources":["../src/cache-value.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,KAAK,EAIV,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,uCAAuC,CAAA;AAe9C,eAAO,MAAM,UAAU,EAAc,oBAAoB,CAAC,KAAK,CAAA;AAO/D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,GAAG,cAAc,CAsBjF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,qBAAqB,GAAG,IAAI,CAYpF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,EAAE,CAW1E"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Next's cache kinds, restated. Next declares them as ambient `const enum`s,
3
+ * which isolated modules cannot read as values; the strings are the enums' own.
4
+ */
5
+ const CACHED_KIND = {
6
+ APP_PAGE: 'APP_PAGE',
7
+ APP_ROUTE: 'APP_ROUTE',
8
+ PAGES: 'PAGES',
9
+ FETCH: 'FETCH',
10
+ REDIRECT: 'REDIRECT',
11
+ IMAGE: 'IMAGE',
12
+ };
13
+ export const FETCH_KIND = 'FETCH';
14
+ /** The response header Next records a rendered page's or route's cache tags in */
15
+ const CACHE_TAGS_HEADER = 'x-next-cache-tags';
16
+ /**
17
+ * Convert a value Next caches into one the page cache store keeps.
18
+ *
19
+ * Binary fields (`rscData`, a route's `body`, an image's `buffer`) are Buffers,
20
+ * which the store keeps as bytes. A page's `segmentData` Map becomes a list of
21
+ * `[segmentPath, bytes]` pairs. `pageData` and redirect `props` are JSON, as
22
+ * Next itself writes them to disk.
23
+ */
24
+ export function toStoredValue(value) {
25
+ if (value === null)
26
+ return null;
27
+ switch (value.kind) {
28
+ case CACHED_KIND.APP_PAGE:
29
+ return {
30
+ kind: value.kind,
31
+ html: value.html,
32
+ rscData: value.rscData,
33
+ headers: value.headers,
34
+ postponed: value.postponed,
35
+ status: value.status,
36
+ segmentData: value.segmentData ? [...value.segmentData] : undefined,
37
+ };
38
+ case CACHED_KIND.PAGES:
39
+ return { ...value, pageData: value.pageData };
40
+ case CACHED_KIND.REDIRECT:
41
+ return { ...value, props: value.props };
42
+ case CACHED_KIND.APP_ROUTE:
43
+ case CACHED_KIND.IMAGE:
44
+ case CACHED_KIND.FETCH:
45
+ return { ...value };
46
+ }
47
+ }
48
+ /**
49
+ * Restore a stored value into the shape Next reads: bytes back to Buffers and
50
+ * a page's segment pairs back to a Map.
51
+ */
52
+ export function fromStoredValue(stored) {
53
+ if (!isRecord(stored))
54
+ return null;
55
+ switch (stored.kind) {
56
+ case CACHED_KIND.APP_PAGE:
57
+ return toAppPage(stored);
58
+ case CACHED_KIND.APP_ROUTE:
59
+ return restore({ ...stored, body: toBuffer(stored.body) });
60
+ case CACHED_KIND.IMAGE:
61
+ return restore({ ...stored, buffer: toBuffer(stored.buffer) });
62
+ default:
63
+ return restore(stored);
64
+ }
65
+ }
66
+ /**
67
+ * The tags Next stamped on a rendered page or route, from its
68
+ * `x-next-cache-tags` header — Next's implicit path tags included.
69
+ */
70
+ export function responseTags(value) {
71
+ if (value?.kind !== CACHED_KIND.APP_PAGE &&
72
+ value?.kind !== CACHED_KIND.APP_ROUTE &&
73
+ value?.kind !== CACHED_KIND.PAGES) {
74
+ return [];
75
+ }
76
+ const header = value.headers?.[CACHE_TAGS_HEADER];
77
+ if (typeof header !== 'string')
78
+ return [];
79
+ return header.split(',').filter((tag) => tag.length > 0);
80
+ }
81
+ function toAppPage(stored) {
82
+ return {
83
+ kind: CACHED_KIND.APP_PAGE,
84
+ html: stored.html,
85
+ rscData: stored.rscData === undefined ? undefined : toBuffer(stored.rscData),
86
+ headers: stored.headers,
87
+ postponed: stored.postponed,
88
+ status: stored.status,
89
+ segmentData: toSegmentData(stored.segmentData),
90
+ };
91
+ }
92
+ function toSegmentData(stored) {
93
+ if (!Array.isArray(stored))
94
+ return undefined;
95
+ const segments = new Map();
96
+ for (const pair of stored) {
97
+ segments.set(pair[0], toBuffer(pair[1]));
98
+ }
99
+ return segments;
100
+ }
101
+ /** The store hands bytes back as `Uint8Array`; Next reads Buffers */
102
+ function toBuffer(bytes) {
103
+ if (!(bytes instanceof Uint8Array)) {
104
+ throw new Error('A cached binary field did not come back from the page cache store as bytes');
105
+ }
106
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
107
+ }
108
+ /**
109
+ * A stored record is the value Next handed to `set`, binary fields restored;
110
+ * the store validated nothing of its shape, Next wrote it.
111
+ */
112
+ function restore(record) {
113
+ return record;
114
+ }
115
+ function isRecord(value) {
116
+ return (value !== null &&
117
+ typeof value === 'object' &&
118
+ !Array.isArray(value) &&
119
+ !(value instanceof Uint8Array));
120
+ }
121
+ //# sourceMappingURL=cache-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-value.js","sourceRoot":"","sources":["../src/cache-value.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { IPublishedContentLoader, IPublishedSeoLoader } from '@krantho/core';
2
+ import { createContentLoader as createStoreLoader } from '@krantho/server';
3
+ type LoaderOptions = Parameters<typeof createStoreLoader>[0];
4
+ /**
5
+ * Create the content loader server components read through.
6
+ *
7
+ * It reads the database the API route opens (see `createContentLoader()` in
8
+ * `@krantho/server`). With `cache.pages: 'next-isr'`, every read goes through
9
+ * the Next.js data cache, tagged with what it read — the document, the
10
+ * collection, the SEO configuration — so a page rendered from it stays cached
11
+ * until the API route invalidates one of those tags (in production; `next dev`
12
+ * reads the database every time). This is the one place krantho touches
13
+ * `unstable_cache`. In an editor's preview (draft mode, `isPreview()`), every
14
+ * read comes from the current versions, drafts included, uncached.
15
+ */
16
+ export declare function createContentLoader(options: LoaderOptions): IPublishedContentLoader & IPublishedSeoLoader;
17
+ export {};
18
+ //# sourceMappingURL=content-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-loader.d.ts","sourceRoot":"","sources":["../src/content-loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,uBAAuB,EACvB,mBAAmB,EAGpB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,mBAAmB,IAAI,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAK1E,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAA;AAQ5D;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,GACrB,uBAAuB,GAAG,mBAAmB,CAwD/C"}
@@ -0,0 +1,85 @@
1
+ import { documentReadTags, jsonLdTags, listReadTags, seoReadTags } from '@krantho/core';
2
+ import { createContentLoader as createStoreLoader } from '@krantho/server';
3
+ import { unstable_cache } from 'next/cache.js';
4
+ import { pageCacheSwitch } from './page-cache.js';
5
+ import { isPreview } from './site-preview.js';
6
+ /**
7
+ * Create the content loader server components read through.
8
+ *
9
+ * It reads the database the API route opens (see `createContentLoader()` in
10
+ * `@krantho/server`). With `cache.pages: 'next-isr'`, every read goes through
11
+ * the Next.js data cache, tagged with what it read — the document, the
12
+ * collection, the SEO configuration — so a page rendered from it stays cached
13
+ * until the API route invalidates one of those tags (in production; `next dev`
14
+ * reads the database every time). This is the one place krantho touches
15
+ * `unstable_cache`. In an editor's preview (draft mode, `isPreview()`), every
16
+ * read comes from the current versions, drafts included, uncached.
17
+ */
18
+ export function createContentLoader(options) {
19
+ const store = createStoreLoader(options);
20
+ const cached = pageCacheSwitch(options.config);
21
+ return {
22
+ isPreview,
23
+ getPublished: async (ref) => {
24
+ if (await isPreview())
25
+ return store.preview.getPublished(ref);
26
+ if (!cached())
27
+ return store.getPublished(ref);
28
+ return unstable_cache(async () => toStored(await store.getPublished(ref)), ['krantho', 'doc', ...refKey(ref)], { tags: documentReadTags(ref.collection, ref.slug) })().then(fromStored);
29
+ },
30
+ listPublished: async (collection, locale) => {
31
+ if (await isPreview())
32
+ return store.preview.listPublished(collection, locale);
33
+ if (!cached())
34
+ return store.listPublished(collection, locale);
35
+ return unstable_cache(async () => (await store.listPublished(collection, locale)).map(toStoredDoc), ['krantho', 'list', collection, locale ?? ''], { tags: listReadTags(collection) })().then((docs) => docs.map(fromStoredDoc));
36
+ },
37
+ listPublishedSummaries: async (collection, locale) => {
38
+ if (await isPreview())
39
+ return store.preview.listPublishedSummaries(collection, locale);
40
+ if (!cached())
41
+ return store.listPublishedSummaries(collection, locale);
42
+ return unstable_cache(async () => (await store.listPublishedSummaries(collection, locale)).map(toStoredSummary), ['krantho', 'summaries', collection, locale ?? ''], { tags: listReadTags(collection) })().then((docs) => docs.map(fromStoredSummary));
43
+ },
44
+ seoProperties: async (configId, locale) => {
45
+ if (await isPreview())
46
+ return store.preview.seoProperties(configId, locale);
47
+ if (!cached())
48
+ return store.seoProperties(configId, locale);
49
+ return unstable_cache(() => store.seoProperties(configId, locale), ['krantho', 'seo', configId, locale ?? ''], {
50
+ tags: seoReadTags(configId),
51
+ })();
52
+ },
53
+ // The page's structure is part of what the graph states, so it is part of the cache key
54
+ jsonLd: async (ref, structure) => {
55
+ if (await isPreview())
56
+ return store.preview.jsonLd(ref, structure);
57
+ if (!cached())
58
+ return store.jsonLd(ref, structure);
59
+ const shape = structure ? [JSON.stringify(structure)] : [];
60
+ return unstable_cache(() => store.jsonLd(ref, structure), ['krantho', 'jsonld', ...refKey(ref), ...shape], { tags: jsonLdTags(ref.collection, ref.slug) })();
61
+ },
62
+ };
63
+ }
64
+ function refKey(ref) {
65
+ return [ref.collection, ref.slug, ref.locale ?? ''];
66
+ }
67
+ function toStored(doc) {
68
+ return doc ? toStoredDoc(doc) : null;
69
+ }
70
+ function toStoredDoc(doc) {
71
+ return { ...doc, publishedAt: doc.publishedAt.toISOString() };
72
+ }
73
+ function fromStored(doc) {
74
+ return doc ? fromStoredDoc(doc) : null;
75
+ }
76
+ function fromStoredDoc(doc) {
77
+ return { ...doc, publishedAt: new Date(doc.publishedAt) };
78
+ }
79
+ function toStoredSummary(doc) {
80
+ return { ...doc, publishedAt: doc.publishedAt.toISOString() };
81
+ }
82
+ function fromStoredSummary(doc) {
83
+ return { ...doc, publishedAt: new Date(doc.publishedAt) };
84
+ }
85
+ //# sourceMappingURL=content-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-loader.js","sourceRoot":"","sources":["../src/content-loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { KranthoHandlerOptions } from '@krantho/server';
2
+ export { createContentLoader } from './content-loader.js';
3
+ export { showPreviewBanner } from './site-preview.js';
4
+ export type { KranthoHandlerOptions };
5
+ type RouteHandler = (request: Request) => Promise<Response>;
6
+ /** The second argument Next.js passes a route handler: the matched dynamic segments */
7
+ interface RouteContext {
8
+ params: Promise<Record<string, string | string[] | undefined>>;
9
+ }
10
+ type ApiRouteHandler = (request: Request, context: RouteContext) => Promise<Response>;
11
+ interface ApiRouteHandlers {
12
+ GET: ApiRouteHandler;
13
+ POST: ApiRouteHandler;
14
+ PUT: ApiRouteHandler;
15
+ DELETE: ApiRouteHandler;
16
+ PATCH: ApiRouteHandler;
17
+ }
18
+ /**
19
+ * Create Next.js App Router route handlers from krantho config.
20
+ *
21
+ * Returns { GET, POST, PUT, DELETE, PATCH } matching the export convention
22
+ * for `app/api/[...path]/route.ts`.
23
+ *
24
+ * The handler is initialized lazily on first request and reused across
25
+ * subsequent requests (singleton pattern for serverless). Background jobs run
26
+ * under `after()`, so the platform keeps them alive past the response. With
27
+ * `cache.pages: 'next-isr'`, every committed write invalidates the cached pages
28
+ * that read what it changed, and a published document's page is rendered
29
+ * again right after the request.
30
+ */
31
+ export declare function createApiRouteHandlers(options: KranthoHandlerOptions): ApiRouteHandlers;
32
+ /**
33
+ * Create Next.js App Router route handlers for Studio.
34
+ *
35
+ * Serves the @krantho/studio built SPA from the package's dist directory.
36
+ * Pass the app's configuration to apply `auth.oidc.studioEntry`.
37
+ * Use in `app/studio/[[...slug]]/route.ts`.
38
+ */
39
+ export declare function createStudioRouteHandler(options?: {
40
+ config?: KranthoHandlerOptions['config'];
41
+ }): {
42
+ GET: RouteHandler;
43
+ };
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAO5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,YAAY,EAAE,qBAAqB,EAAE,CAAA;AAErC,KAAK,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE3D,uFAAuF;AACvF,UAAU,YAAY;IACpB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAA;CAC/D;AAED,KAAK,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAErF,UAAU,gBAAgB;IACxB,GAAG,EAAE,eAAe,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,GAAG,EAAE,eAAe,CAAA;IACpB,MAAM,EAAE,eAAe,CAAA;IACvB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,qBAAqB,GAAG,gBAAgB,CA+BvF;AAgBD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAA;CAAO,GACzD;IACD,GAAG,EAAE,YAAY,CAAA;CAClB,CAUA"}
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ import { createLazyHandler, createStudioServer } from '@krantho/server';
2
+ import { after } from 'next/server.js';
3
+ import { pageCacheSwitch } from './page-cache.js';
4
+ import { invalidatePages, withPageRefresh } from './page-refresh.js';
5
+ import { handleSitePreview } from './site-preview.js';
6
+ export { createContentLoader } from './content-loader.js';
7
+ export { showPreviewBanner } from './site-preview.js';
8
+ /**
9
+ * Create Next.js App Router route handlers from krantho config.
10
+ *
11
+ * Returns { GET, POST, PUT, DELETE, PATCH } matching the export convention
12
+ * for `app/api/[...path]/route.ts`.
13
+ *
14
+ * The handler is initialized lazily on first request and reused across
15
+ * subsequent requests (singleton pattern for serverless). Background jobs run
16
+ * under `after()`, so the platform keeps them alive past the response. With
17
+ * `cache.pages: 'next-isr'`, every committed write invalidates the cached pages
18
+ * that read what it changed, and a published document's page is rendered
19
+ * again right after the request.
20
+ */
21
+ export function createApiRouteHandlers(options) {
22
+ const cachesPages = pageCacheSwitch(options.config);
23
+ const getHandler = createLazyHandler({
24
+ ...options,
25
+ onContentChange: async (change) => {
26
+ if (cachesPages())
27
+ invalidatePages(change, options.collections);
28
+ await options.onContentChange?.(change);
29
+ },
30
+ // Called only by a handler, inside its request, where after() is available
31
+ waitUntil: (work) => after(() => work),
32
+ sitePreview: 'draft-mode',
33
+ framework: 'next',
34
+ });
35
+ async function handle(request, context) {
36
+ const handler = await getHandler();
37
+ const apiRequest = await withApiPath(request, context);
38
+ const preview = await handleSitePreview(apiRequest, () => handler.handle(apiRequest));
39
+ if (preview)
40
+ return preview;
41
+ if (!cachesPages())
42
+ return handler.handle(apiRequest);
43
+ return withPageRefresh(request, () => handler.handle(apiRequest));
44
+ }
45
+ return {
46
+ GET: handle,
47
+ POST: handle,
48
+ PUT: handle,
49
+ DELETE: handle,
50
+ PATCH: handle,
51
+ };
52
+ }
53
+ /**
54
+ * A rewrite such as `/robots.txt` → `/api/seo/robots.txt` reaches the catch-all
55
+ * route with the visitor's URL. The catch-all segments carry the path it was
56
+ * rewritten to, so rebuild the `/api` path the router expects from them.
57
+ */
58
+ async function withApiPath(request, context) {
59
+ const url = new URL(request.url);
60
+ if (url.pathname.startsWith('/api/'))
61
+ return request;
62
+ const segments = Object.values(await context.params).find(Array.isArray);
63
+ if (!segments)
64
+ return request;
65
+ url.pathname = `/api/${segments.map(encodeURIComponent).join('/')}`;
66
+ return new Request(url, request);
67
+ }
68
+ /**
69
+ * Create Next.js App Router route handlers for Studio.
70
+ *
71
+ * Serves the @krantho/studio built SPA from the package's dist directory.
72
+ * Pass the app's configuration to apply `auth.oidc.studioEntry`.
73
+ * Use in `app/studio/[[...slug]]/route.ts`.
74
+ */
75
+ export function createStudioRouteHandler(options = {}) {
76
+ const serve = createStudioServer(options.config);
77
+ return {
78
+ async GET(request) {
79
+ return serve({
80
+ url: new URL(request.url),
81
+ cookie: request.headers.get('cookie') ?? undefined,
82
+ });
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { KranthoConfigInput } from '@krantho/config';
2
+ /**
3
+ * Whether Next.js caches this app's pages: `cache.pages: 'next-isr'`, outside
4
+ * `next dev`. The dev server renders every request, and a file edit synced by
5
+ * the watcher arrives outside any request, where no cache tag can be
6
+ * invalidated — so in development every read goes to the database.
7
+ */
8
+ export declare function pageCacheSwitch(config: KranthoConfigInput): () => boolean;
9
+ //# sourceMappingURL=page-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cache.d.ts","sourceRoot":"","sources":["../src/page-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGzD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,OAAO,CAQzE"}
@@ -0,0 +1,17 @@
1
+ import { loadConfig } from '@krantho/config/node';
2
+ /**
3
+ * Whether Next.js caches this app's pages: `cache.pages: 'next-isr'`, outside
4
+ * `next dev`. The dev server renders every request, and a file edit synced by
5
+ * the watcher arrives outside any request, where no cache tag can be
6
+ * invalidated — so in development every read goes to the database.
7
+ */
8
+ export function pageCacheSwitch(config) {
9
+ let enabled;
10
+ return () => {
11
+ enabled ??=
12
+ process.env.NODE_ENV !== 'development' &&
13
+ loadConfig({ code: config, env: process.env }).cache.pages === 'next-isr';
14
+ return enabled;
15
+ };
16
+ }
17
+ //# sourceMappingURL=page-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-cache.js","sourceRoot":"","sources":["../src/page-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAA0B;IACxD,IAAI,OAA4B,CAAA;IAChC,OAAO,GAAG,EAAE;QACV,OAAO;YACL,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa;gBACtC,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,UAAU,CAAA;QAC3E,OAAO,OAAO,CAAA;IAChB,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { ContentChange, PageCollection } from '@krantho/core';
2
+ /**
3
+ * Handle an API request, then render again the pages its writes published, so
4
+ * neither the editor nor the next visitor waits for the render. Next.js applies
5
+ * the request's invalidations before `after()` callbacks run.
6
+ */
7
+ export declare function withPageRefresh(request: Request, handle: () => Promise<Response>): Promise<Response>;
8
+ /**
9
+ * Invalidate the Next.js cache for a change: the edited document expires, so
10
+ * its page is fresh on the next request; broad tags go stale, so their pages
11
+ * are served once more while they render again in the background. A published
12
+ * document's page — at `documentPath()`, from its collection's declared path —
13
+ * is rendered again after the request; a document without a page is not.
14
+ */
15
+ export declare function invalidatePages(change: ContentChange, collections: Readonly<Record<string, PageCollection>>): void;
16
+ //# sourceMappingURL=page-refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-refresh.d.ts","sourceRoot":"","sources":["../src/page-refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAgBlE;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAiBnB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,GACpD,IAAI,CASN"}
@@ -0,0 +1,61 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { documentPath, invalidationsFor, pageCollection } from '@krantho/core';
3
+ import { revalidateTag } from 'next/cache.js';
4
+ import { after } from 'next/server.js';
5
+ const scopes = new AsyncLocalStorage();
6
+ /**
7
+ * Handle an API request, then render again the pages its writes published, so
8
+ * neither the editor nor the next visitor waits for the render. Next.js applies
9
+ * the request's invalidations before `after()` callbacks run.
10
+ */
11
+ export async function withPageRefresh(request, handle) {
12
+ // The origin the request reached: the public host on Vercel, the server itself
13
+ // behind a proxy — either way, the one whose cache holds the page
14
+ const scope = {
15
+ origin: new URL(request.url).origin,
16
+ open: true,
17
+ rerender: new Set(),
18
+ };
19
+ try {
20
+ return await scopes.run(scope, handle);
21
+ }
22
+ finally {
23
+ scope.open = false;
24
+ if (scope.rerender.size > 0) {
25
+ const paths = [...scope.rerender];
26
+ after(() => renderPages(scope.origin, paths));
27
+ }
28
+ }
29
+ }
30
+ /**
31
+ * Invalidate the Next.js cache for a change: the edited document expires, so
32
+ * its page is fresh on the next request; broad tags go stale, so their pages
33
+ * are served once more while they render again in the background. A published
34
+ * document's page — at `documentPath()`, from its collection's declared path —
35
+ * is rendered again after the request; a document without a page is not.
36
+ */
37
+ export function invalidatePages(change, collections) {
38
+ for (const { tags, mode } of invalidationsFor(change)) {
39
+ for (const tag of tags)
40
+ revalidateTag(tag, mode === 'expire' ? { expire: 0 } : 'max');
41
+ }
42
+ const scope = scopes.getStore();
43
+ if (change.kind !== 'document' || !change.live || !scope?.open)
44
+ return;
45
+ const collection = pageCollection(collections, change.collection);
46
+ const path = documentPath(collection, change.slug);
47
+ if (path)
48
+ scope.rerender.add(path);
49
+ }
50
+ async function renderPages(origin, paths) {
51
+ await Promise.all(paths.map(async (path) => {
52
+ try {
53
+ // Read the whole body: the page is stored once its render completes
54
+ await (await fetch(new URL(path, origin))).arrayBuffer();
55
+ }
56
+ catch (err) {
57
+ console.warn(`[krantho] rendering ${path} after a publish failed:`, err);
58
+ }
59
+ }));
60
+ }
61
+ //# sourceMappingURL=page-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-refresh.js","sourceRoot":"","sources":["../src/page-refresh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEpD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAWtC,MAAM,MAAM,GAAG,IAAI,iBAAiB,EAAgB,CAAA;AAEpD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAgB,EAChB,MAA+B;IAE/B,+EAA+E;IAC/E,kEAAkE;IAClE,MAAM,KAAK,GAAiB;QAC1B,MAAM,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM;QACnC,IAAI,EAAE,IAAI;QACV,QAAQ,EAAE,IAAI,GAAG,EAAE;KACpB,CAAA;IACD,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACxC,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,IAAI,GAAG,KAAK,CAAA;QAClB,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAA;YACjC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;QAC/C,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAqB,EACrB,WAAqD;IAErD,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;QACtD,KAAK,MAAM,GAAG,IAAI,IAAI;YAAE,aAAa,CAAC,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IAC/B,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,IAAI;QAAE,OAAM;IACtE,MAAM,UAAU,GAAG,cAAc,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IACjE,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;IAClD,IAAI,IAAI;QAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACpC,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,KAAwB;IACjE,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACvB,IAAI,CAAC;YACH,oEAAoE;YACpE,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QAC1D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,uBAAuB,IAAI,0BAA0B,EAAE,GAAG,CAAC,CAAA;QAC1E,CAAC;IACH,CAAC,CAAC,CACH,CAAA;AACH,CAAC"}
@@ -0,0 +1,24 @@
1
+ /** Where Studio sends an editor to preview a document on the site */
2
+ export declare const PREVIEW_PATH = "/api/preview";
3
+ /** Where a previewing editor leaves draft mode */
4
+ export declare const PREVIEW_EXIT_PATH = "/api/preview/exit";
5
+ /**
6
+ * Whether this render is an editor's preview (Next.js draft mode). Outside a
7
+ * request — a build, a test — it is not.
8
+ */
9
+ export declare function isPreview(): Promise<boolean>;
10
+ /**
11
+ * Whether the site's own "Previewing drafts" notice belongs on this render: it
12
+ * does for an editor's preview, and not when a frame shows the page — Studio's
13
+ * Live mode does, and it has its own way out of the preview. The browser says so
14
+ * itself (`Sec-Fetch-Dest: iframe`), so the site needs no flag of its own.
15
+ */
16
+ export declare function showPreviewBanner(): Promise<boolean>;
17
+ /**
18
+ * Handle the preview routes, or return null for any other request. The API
19
+ * decides who may preview (`GET /api/preview` answers the page's path to an
20
+ * editor); this turns its answer into draft mode and sends the editor to the
21
+ * page, where every read comes from the current version, uncached.
22
+ */
23
+ export declare function handleSitePreview(request: Request, apiResponse: () => Promise<Response>): Promise<Response | null>;
24
+ //# sourceMappingURL=site-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-preview.d.ts","sourceRoot":"","sources":["../src/site-preview.ts"],"names":[],"mappings":"AAGA,qEAAqE;AACrE,eAAO,MAAM,YAAY,iBAAiB,CAAA;AAC1C,kDAAkD;AAClD,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AAEpD;;;GAGG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAMlD;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO1D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GACnC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAe1B"}
@@ -0,0 +1,70 @@
1
+ import { resolveSitePath } from '@krantho/core';
2
+ import { draftMode, headers } from 'next/headers.js';
3
+ /** Where Studio sends an editor to preview a document on the site */
4
+ export const PREVIEW_PATH = '/api/preview';
5
+ /** Where a previewing editor leaves draft mode */
6
+ export const PREVIEW_EXIT_PATH = '/api/preview/exit';
7
+ /**
8
+ * Whether this render is an editor's preview (Next.js draft mode). Outside a
9
+ * request — a build, a test — it is not.
10
+ */
11
+ export async function isPreview() {
12
+ try {
13
+ return (await draftMode()).isEnabled;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ /**
20
+ * Whether the site's own "Previewing drafts" notice belongs on this render: it
21
+ * does for an editor's preview, and not when a frame shows the page — Studio's
22
+ * Live mode does, and it has its own way out of the preview. The browser says so
23
+ * itself (`Sec-Fetch-Dest: iframe`), so the site needs no flag of its own.
24
+ */
25
+ export async function showPreviewBanner() {
26
+ if (!(await isPreview()))
27
+ return false;
28
+ try {
29
+ return (await headers()).get('sec-fetch-dest') !== 'iframe';
30
+ }
31
+ catch {
32
+ return true;
33
+ }
34
+ }
35
+ /**
36
+ * Handle the preview routes, or return null for any other request. The API
37
+ * decides who may preview (`GET /api/preview` answers the page's path to an
38
+ * editor); this turns its answer into draft mode and sends the editor to the
39
+ * page, where every read comes from the current version, uncached.
40
+ */
41
+ export async function handleSitePreview(request, apiResponse) {
42
+ if (request.method !== 'GET')
43
+ return null;
44
+ const url = new URL(request.url);
45
+ if (url.pathname === PREVIEW_EXIT_PATH) {
46
+ ;
47
+ (await draftMode()).disable();
48
+ return redirect(sameSitePath(url.searchParams.get('path'), url), url);
49
+ }
50
+ if (url.pathname !== PREVIEW_PATH)
51
+ return null;
52
+ const response = await apiResponse();
53
+ if (!response.ok)
54
+ return response;
55
+ const { path } = (await response.json());
56
+ (await draftMode()).enable();
57
+ return redirect(sameSitePath(path, url), url);
58
+ }
59
+ /** A path on this site; anything else falls back to the home page */
60
+ function sameSitePath(path, base) {
61
+ const target = path ? resolveSitePath(path, base) : null;
62
+ return target ? `${target.pathname}${target.search}` : '/';
63
+ }
64
+ function redirect(path, base) {
65
+ return new Response(null, {
66
+ status: 307,
67
+ headers: { location: new URL(path, base).toString(), 'cache-control': 'no-store' },
68
+ });
69
+ }
70
+ //# sourceMappingURL=site-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-preview.js","sourceRoot":"","sources":["../src/site-preview.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAEpD,qEAAqE;AACrE,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAA;AAC1C,kDAAkD;AAClD,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAA;AAEpD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IACtC,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,QAAQ,CAAA;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAgB,EAChB,WAAoC;IAEpC,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;QAAE,OAAO,IAAI,CAAA;IACzC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAEhC,IAAI,GAAG,CAAC,QAAQ,KAAK,iBAAiB,EAAE,CAAC;QACvC,CAAC;QAAA,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAA;QAC9B,OAAO,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAA;IACvE,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,YAAY;QAAE,OAAO,IAAI,CAAA;IAE9C,MAAM,QAAQ,GAAG,MAAM,WAAW,EAAE,CAAA;IACpC,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,QAAQ,CAAA;IACjC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAqB,CAC3D;IAAA,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;IAC7B,OAAO,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAA;AAC/C,CAAC;AAED,qEAAqE;AACrE,SAAS,YAAY,CAAC,IAAmB,EAAE,IAAS;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACxD,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAA;AAC5D,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,IAAS;IACvC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE;KACnF,CAAC,CAAA;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@krantho/server-next",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.1",
4
+ "description": "The krantho adapter for the Next.js App Router: API and Studio route handlers, the content loader and page caching.",
5
+ "license": "MIT",
6
+ "homepage": "https://krantho.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/descoped/krantho.git",
10
+ "directory": "packages/server-next"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/descoped/krantho/issues"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./cache-handler": {
22
+ "types": "./dist/cache-handler.d.ts",
23
+ "import": "./dist/cache-handler.js",
24
+ "default": "./dist/cache-handler.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "dependencies": {
31
+ "@krantho/core": "1.0.1",
32
+ "@krantho/server": "1.0.1",
33
+ "@krantho/config": "1.0.1"
34
+ },
35
+ "peerDependencies": {
36
+ "next": ">=16.3.8"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.9.9",
40
+ "next": "~16.4.6",
41
+ "react": "^19.3.1",
42
+ "react-dom": "^19.3.1",
43
+ "typescript": "^6.0.3",
44
+ "zod": "^4.6.7",
45
+ "@krantho/schema": "1.0.1"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc",
49
+ "typecheck": "tsc --noEmit",
50
+ "clean": "rm -rf dist"
51
+ }
6
52
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.