@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 +21 -0
- package/dist/cache-handler.d.ts +34 -0
- package/dist/cache-handler.d.ts.map +1 -0
- package/dist/cache-handler.js +90 -0
- package/dist/cache-handler.js.map +1 -0
- package/dist/cache-value.d.ts +23 -0
- package/dist/cache-value.d.ts.map +1 -0
- package/dist/cache-value.js +121 -0
- package/dist/cache-value.js.map +1 -0
- package/dist/content-loader.d.ts +18 -0
- package/dist/content-loader.d.ts.map +1 -0
- package/dist/content-loader.js +85 -0
- package/dist/content-loader.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/page-cache.d.ts +9 -0
- package/dist/page-cache.d.ts.map +1 -0
- package/dist/page-cache.js +17 -0
- package/dist/page-cache.js.map +1 -0
- package/dist/page-refresh.d.ts +16 -0
- package/dist/page-refresh.d.ts.map +1 -0
- package/dist/page-refresh.js +61 -0
- package/dist/page-refresh.js.map +1 -0
- package/dist/site-preview.d.ts +24 -0
- package/dist/site-preview.d.ts.map +1 -0
- package/dist/site-preview.js +70 -0
- package/dist/site-preview.js.map +1 -0
- package/package.json +49 -3
- package/README.md +0 -3
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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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