@backmatter/next 0.0.36 → 0.0.37
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/dist/config.cjs +8 -8
- package/dist/loader.cjs +8 -8
- package/package.json +2 -2
- package/src/cached-doc.ts +8 -22
- package/src/preview-content.ts +4 -4
- package/src/preview-live.tsx +6 -9
- package/src/preview-refresh.tsx +8 -17
- package/src/preview-route.ts +1 -2
- package/src/render-preview.ts +0 -2
- package/src/routes.ts +22 -80
- package/src/site-doc-bridge.tsx +1 -2
- package/src/site-doc.ts +1 -10
- package/src/site-editor.tsx +1 -1
- package/src/source-data.ts +1 -1
package/dist/config.cjs
CHANGED
|
@@ -194,27 +194,27 @@ function renderCachedCmsClient() {
|
|
|
194
194
|
return [
|
|
195
195
|
"const raw = createBackmatter({ ...config, models, sync: false });",
|
|
196
196
|
"",
|
|
197
|
-
"async function cachedGetDoc(
|
|
197
|
+
"async function cachedGetDoc(type: string, slug: string) {",
|
|
198
198
|
' "use cache";',
|
|
199
199
|
' cacheLife("hours");',
|
|
200
|
-
" cacheTag(...contentCacheTags(
|
|
201
|
-
" return raw.getDoc(
|
|
200
|
+
" cacheTag(...contentCacheTags(type, slug));",
|
|
201
|
+
" return raw.getDoc(type, slug);",
|
|
202
202
|
"}",
|
|
203
203
|
"",
|
|
204
|
-
"async function cachedListDocs(filter?: { type?: string
|
|
204
|
+
"async function cachedListDocs(filter?: { type?: string }) {",
|
|
205
205
|
' "use cache";',
|
|
206
206
|
' cacheLife("hours");',
|
|
207
207
|
" cacheTag(...listCacheTags(filter?.type));",
|
|
208
208
|
" return raw.listDocs(filter);",
|
|
209
209
|
"}",
|
|
210
210
|
"",
|
|
211
|
-
"async function getDoc(
|
|
211
|
+
"async function getDoc(type: string, slug: string) {",
|
|
212
212
|
" const preview = currentPreviewContent();",
|
|
213
|
-
" if (preview) return preview.getDoc(
|
|
214
|
-
" return cachedGetDoc(
|
|
213
|
+
" if (preview) return preview.getDoc(type, slug);",
|
|
214
|
+
" return cachedGetDoc(type, slug);",
|
|
215
215
|
"}",
|
|
216
216
|
"",
|
|
217
|
-
"async function listDocs(filter?: { type?: string
|
|
217
|
+
"async function listDocs(filter?: { type?: string }) {",
|
|
218
218
|
" const preview = currentPreviewContent();",
|
|
219
219
|
" if (preview) return preview.listDocs(filter);",
|
|
220
220
|
" return cachedListDocs(filter);",
|
package/dist/loader.cjs
CHANGED
|
@@ -187,27 +187,27 @@ function renderCachedCmsClient() {
|
|
|
187
187
|
return [
|
|
188
188
|
"const raw = createBackmatter({ ...config, models, sync: false });",
|
|
189
189
|
"",
|
|
190
|
-
"async function cachedGetDoc(
|
|
190
|
+
"async function cachedGetDoc(type: string, slug: string) {",
|
|
191
191
|
' "use cache";',
|
|
192
192
|
' cacheLife("hours");',
|
|
193
|
-
" cacheTag(...contentCacheTags(
|
|
194
|
-
" return raw.getDoc(
|
|
193
|
+
" cacheTag(...contentCacheTags(type, slug));",
|
|
194
|
+
" return raw.getDoc(type, slug);",
|
|
195
195
|
"}",
|
|
196
196
|
"",
|
|
197
|
-
"async function cachedListDocs(filter?: { type?: string
|
|
197
|
+
"async function cachedListDocs(filter?: { type?: string }) {",
|
|
198
198
|
' "use cache";',
|
|
199
199
|
' cacheLife("hours");',
|
|
200
200
|
" cacheTag(...listCacheTags(filter?.type));",
|
|
201
201
|
" return raw.listDocs(filter);",
|
|
202
202
|
"}",
|
|
203
203
|
"",
|
|
204
|
-
"async function getDoc(
|
|
204
|
+
"async function getDoc(type: string, slug: string) {",
|
|
205
205
|
" const preview = currentPreviewContent();",
|
|
206
|
-
" if (preview) return preview.getDoc(
|
|
207
|
-
" return cachedGetDoc(
|
|
206
|
+
" if (preview) return preview.getDoc(type, slug);",
|
|
207
|
+
" return cachedGetDoc(type, slug);",
|
|
208
208
|
"}",
|
|
209
209
|
"",
|
|
210
|
-
"async function listDocs(filter?: { type?: string
|
|
210
|
+
"async function listDocs(filter?: { type?: string }) {",
|
|
211
211
|
" const preview = currentPreviewContent();",
|
|
212
212
|
" if (preview) return preview.listDocs(filter);",
|
|
213
213
|
" return cachedListDocs(filter);",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"test": "bun test"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@backmatter/core": "^0.0.
|
|
50
|
+
"@backmatter/core": "^0.0.28",
|
|
51
51
|
"@mdx-js/mdx": "^3.1.1",
|
|
52
52
|
"remark-gfm": "^4.0.1",
|
|
53
53
|
"@backmatter/studio": "^0.0.13"
|
package/src/cached-doc.ts
CHANGED
|
@@ -7,22 +7,11 @@ export type CacheFns = {
|
|
|
7
7
|
cacheTag: (...tags: string[]) => void;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export type GetDocFn = (
|
|
11
|
-
locale: string,
|
|
12
|
-
type: string,
|
|
13
|
-
slug: string,
|
|
14
|
-
) => Promise<BackmatterDoc | null>;
|
|
10
|
+
export type GetDocFn = (type: string, slug: string) => Promise<BackmatterDoc | null>;
|
|
15
11
|
|
|
16
|
-
export type ListDocsFn = (filter?: {
|
|
17
|
-
type?: string;
|
|
18
|
-
locale?: string;
|
|
19
|
-
}) => Promise<BackmatterDocSummary[]>;
|
|
12
|
+
export type ListDocsFn = (filter?: { type?: string }) => Promise<BackmatterDocSummary[]>;
|
|
20
13
|
|
|
21
|
-
export type PublishedRender = (
|
|
22
|
-
locale: string,
|
|
23
|
-
type: string,
|
|
24
|
-
slug: string,
|
|
25
|
-
) => Promise<ReactNode>;
|
|
14
|
+
export type PublishedRender = (type: string, slug: string) => Promise<ReactNode>;
|
|
26
15
|
|
|
27
16
|
let registeredGetDoc: GetDocFn | null = null;
|
|
28
17
|
let registeredListDocs: ListDocsFn | null = null;
|
|
@@ -61,7 +50,6 @@ async function loadCacheFns(): Promise<CacheFns> {
|
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
export async function getCachedDoc(
|
|
64
|
-
locale: string,
|
|
65
53
|
type: string,
|
|
66
54
|
slug: string,
|
|
67
55
|
cache?: CacheFns,
|
|
@@ -69,17 +57,16 @@ export async function getCachedDoc(
|
|
|
69
57
|
"use cache";
|
|
70
58
|
const fns = cache ?? (await loadCacheFns());
|
|
71
59
|
fns.cacheLife("hours");
|
|
72
|
-
fns.cacheTag(...contentCacheTags(
|
|
60
|
+
fns.cacheTag(...contentCacheTags(type, slug));
|
|
73
61
|
const getDoc = registeredGetDoc;
|
|
74
62
|
if (!getDoc) {
|
|
75
63
|
throw new Error("[backmatter] getCachedDoc called before createBackmatter registered getDoc");
|
|
76
64
|
}
|
|
77
|
-
return getDoc(
|
|
65
|
+
return getDoc(type, slug);
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
export async function getCachedDocs(
|
|
81
69
|
type?: string,
|
|
82
|
-
locale?: string,
|
|
83
70
|
cache?: CacheFns,
|
|
84
71
|
): Promise<BackmatterDocSummary[]> {
|
|
85
72
|
"use cache";
|
|
@@ -90,7 +77,7 @@ export async function getCachedDocs(
|
|
|
90
77
|
if (!listDocs) {
|
|
91
78
|
throw new Error("[backmatter] getCachedDocs called before createBackmatter registered listDocs");
|
|
92
79
|
}
|
|
93
|
-
return listDocs({ type
|
|
80
|
+
return listDocs({ type });
|
|
94
81
|
}
|
|
95
82
|
|
|
96
83
|
/**
|
|
@@ -98,7 +85,6 @@ export async function getCachedDocs(
|
|
|
98
85
|
* The cache entry is what a static prerender and a normal link prefetch store.
|
|
99
86
|
*/
|
|
100
87
|
export async function getCachedPublished(
|
|
101
|
-
locale: string,
|
|
102
88
|
type: string,
|
|
103
89
|
slug: string,
|
|
104
90
|
cache?: CacheFns,
|
|
@@ -106,12 +92,12 @@ export async function getCachedPublished(
|
|
|
106
92
|
"use cache";
|
|
107
93
|
const fns = cache ?? (await loadCacheFns());
|
|
108
94
|
fns.cacheLife("hours");
|
|
109
|
-
fns.cacheTag(...contentCacheTags(
|
|
95
|
+
fns.cacheTag(...contentCacheTags(type, slug));
|
|
110
96
|
const render = registeredPublishedRender;
|
|
111
97
|
if (!render) {
|
|
112
98
|
throw new Error("[backmatter] getCachedPublished called before a published render was registered");
|
|
113
99
|
}
|
|
114
|
-
return render(
|
|
100
|
+
return render(type, slug);
|
|
115
101
|
}
|
|
116
102
|
|
|
117
103
|
/** Unwrap Next `params` inside `"use cache"` so Cache Components can prerender. */
|
package/src/preview-content.ts
CHANGED
|
@@ -14,8 +14,8 @@ export type PreviewContentConfig = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export type PreviewContent = {
|
|
17
|
-
getDoc(
|
|
18
|
-
listDocs(filter?: { type?: string
|
|
17
|
+
getDoc(type: string, slug: string): Promise<BackmatterDoc | null>;
|
|
18
|
+
listDocs(filter?: { type?: string }): Promise<BackmatterDocSummary[]>;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
const current = new AsyncLocalStorage<PreviewContent>();
|
|
@@ -30,8 +30,8 @@ export function previewContentForCheckout(
|
|
|
30
30
|
previewToken: config.previewToken,
|
|
31
31
|
};
|
|
32
32
|
return {
|
|
33
|
-
getDoc(
|
|
34
|
-
return fetchPreviewDoc(options, {
|
|
33
|
+
getDoc(type, slug) {
|
|
34
|
+
return fetchPreviewDoc(options, { type, slug, ...checkout });
|
|
35
35
|
},
|
|
36
36
|
listDocs(filter) {
|
|
37
37
|
return fetchPreviewDocs(options, { ...checkout, ...filter });
|
package/src/preview-live.tsx
CHANGED
|
@@ -15,10 +15,9 @@ export function PreviewUnavailable({ message }: { message?: string }) {
|
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function streamUrl(ref: {
|
|
18
|
+
function streamUrl(ref: { type: string; slug: string }) {
|
|
19
19
|
const params = new URLSearchParams({
|
|
20
20
|
stream: "1",
|
|
21
|
-
locale: ref.locale,
|
|
22
21
|
type: ref.type,
|
|
23
22
|
slug: ref.slug,
|
|
24
23
|
});
|
|
@@ -28,7 +27,7 @@ function streamUrl(ref: { locale: string; type: string; slug: string }) {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export function listenForPreviewChanges(
|
|
31
|
-
ref: {
|
|
30
|
+
ref: { type: string; slug: string },
|
|
32
31
|
onChange: () => void,
|
|
33
32
|
options?: { debounceMs?: number },
|
|
34
33
|
): () => void {
|
|
@@ -52,7 +51,7 @@ export function PreviewBody({
|
|
|
52
51
|
components,
|
|
53
52
|
children,
|
|
54
53
|
}: {
|
|
55
|
-
doc: Pick<BackmatterDoc, "
|
|
54
|
+
doc: Pick<BackmatterDoc, "type" | "slug" | "frontmatter" | "body">;
|
|
56
55
|
components?: MdxComponents;
|
|
57
56
|
/** RSC first paint. Replaced in place when SSE sends a new body. */
|
|
58
57
|
children?: ReactNode;
|
|
@@ -76,19 +75,17 @@ export function PreviewBody({
|
|
|
76
75
|
source.removeEventListener("change", onMessage as EventListener);
|
|
77
76
|
source.close();
|
|
78
77
|
};
|
|
79
|
-
}, [doc.
|
|
78
|
+
}, [doc.type, doc.slug]);
|
|
80
79
|
|
|
81
80
|
if (body === null) return children ?? <MdxBody source={doc.body} components={components} />;
|
|
82
81
|
return <MdxBody source={body} components={components} />;
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
export function PreviewPageReloader({
|
|
86
|
-
locale,
|
|
87
85
|
type,
|
|
88
86
|
slug,
|
|
89
87
|
refresh,
|
|
90
88
|
}: {
|
|
91
|
-
locale: string;
|
|
92
89
|
type: string;
|
|
93
90
|
slug: string;
|
|
94
91
|
/** Hosts omit this; the Next wrapper passes `router.refresh` in a transition. */
|
|
@@ -96,11 +93,11 @@ export function PreviewPageReloader({
|
|
|
96
93
|
}) {
|
|
97
94
|
useEffect(
|
|
98
95
|
() =>
|
|
99
|
-
listenForPreviewChanges({
|
|
96
|
+
listenForPreviewChanges({ type, slug }, () => {
|
|
100
97
|
if (refresh) refresh();
|
|
101
98
|
else window.location.reload();
|
|
102
99
|
}),
|
|
103
|
-
[
|
|
100
|
+
[type, slug, refresh],
|
|
104
101
|
);
|
|
105
102
|
return null;
|
|
106
103
|
}
|
package/src/preview-refresh.tsx
CHANGED
|
@@ -20,7 +20,7 @@ import { fanInPreviewTransports, sitePageTransport } from "./site-channel";
|
|
|
20
20
|
import { PreviewPageReloader as PreviewStreamReloader, PreviewRemoved } from "./preview-live";
|
|
21
21
|
|
|
22
22
|
/** Keeps the current page visible while preview content is refetched. */
|
|
23
|
-
export function PreviewPageReloader(props: {
|
|
23
|
+
export function PreviewPageReloader(props: { type: string; slug: string }) {
|
|
24
24
|
const router = useRouter();
|
|
25
25
|
const refresh = useCallback(() => {
|
|
26
26
|
startTransition(() => {
|
|
@@ -30,8 +30,8 @@ export function PreviewPageReloader(props: { locale: string; type: string; slug:
|
|
|
30
30
|
// HEAD and other documents in this checkout. This document's own saves
|
|
31
31
|
// arrive on the doc stream below and must not refresh the page twice.
|
|
32
32
|
useEffect(
|
|
33
|
-
() => subscribeTreeStream({
|
|
34
|
-
[props.
|
|
33
|
+
() => subscribeTreeStream({ type: props.type, slug: props.slug }, refresh),
|
|
34
|
+
[props.type, props.slug, refresh],
|
|
35
35
|
);
|
|
36
36
|
return <PreviewStreamReloader {...props} refresh={refresh} />;
|
|
37
37
|
}
|
|
@@ -51,12 +51,11 @@ function referrerOrigin(): string {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function subscribeDocStream(
|
|
54
|
-
ref: {
|
|
54
|
+
ref: { type: string; slug: string },
|
|
55
55
|
onEvent: (event: { raw?: string; body?: string; frontmatter?: Record<string, unknown> }) => void,
|
|
56
56
|
): () => void {
|
|
57
57
|
const params = new URLSearchParams({
|
|
58
58
|
stream: "1",
|
|
59
|
-
locale: ref.locale,
|
|
60
59
|
type: ref.type,
|
|
61
60
|
slug: ref.slug,
|
|
62
61
|
});
|
|
@@ -90,7 +89,7 @@ function subscribeDocStream(
|
|
|
90
89
|
|
|
91
90
|
/** Refresh when some other document in this checkout changes, or HEAD moves. */
|
|
92
91
|
function subscribeTreeStream(
|
|
93
|
-
ref: {
|
|
92
|
+
ref: { type: string; slug: string },
|
|
94
93
|
refresh: () => void,
|
|
95
94
|
): () => void {
|
|
96
95
|
const params = new URLSearchParams({ stream: "tree" });
|
|
@@ -107,14 +106,9 @@ function subscribeTreeStream(
|
|
|
107
106
|
};
|
|
108
107
|
const onChange = (ev: MessageEvent) => {
|
|
109
108
|
try {
|
|
110
|
-
const data = JSON.parse(String(ev.data)) as { ref?: {
|
|
109
|
+
const data = JSON.parse(String(ev.data)) as { ref?: { type?: string; slug?: string } };
|
|
111
110
|
const changed = data.ref;
|
|
112
|
-
if (
|
|
113
|
-
changed &&
|
|
114
|
-
changed.locale === ref.locale &&
|
|
115
|
-
changed.type === ref.type &&
|
|
116
|
-
changed.slug === ref.slug
|
|
117
|
-
) {
|
|
111
|
+
if (changed && changed.type === ref.type && changed.slug === ref.slug) {
|
|
118
112
|
return;
|
|
119
113
|
}
|
|
120
114
|
} catch {
|
|
@@ -137,7 +131,6 @@ function subscribeTreeStream(
|
|
|
137
131
|
* and swaps the body. First paint is `children`.
|
|
138
132
|
*/
|
|
139
133
|
export function PreviewClient({
|
|
140
|
-
locale,
|
|
141
134
|
type,
|
|
142
135
|
slug,
|
|
143
136
|
branch,
|
|
@@ -146,7 +139,6 @@ export function PreviewClient({
|
|
|
146
139
|
renderPreview,
|
|
147
140
|
children,
|
|
148
141
|
}: {
|
|
149
|
-
locale: string;
|
|
150
142
|
type: string;
|
|
151
143
|
slug: string;
|
|
152
144
|
branch?: string;
|
|
@@ -163,7 +155,6 @@ export function PreviewClient({
|
|
|
163
155
|
|
|
164
156
|
useEffect(() => {
|
|
165
157
|
const ref: PreviewChannelRef = {
|
|
166
|
-
locale,
|
|
167
158
|
type,
|
|
168
159
|
slug,
|
|
169
160
|
...(branch ? { branch } : {}),
|
|
@@ -266,7 +257,7 @@ export function PreviewClient({
|
|
|
266
257
|
session.close();
|
|
267
258
|
windowTransport?.close();
|
|
268
259
|
};
|
|
269
|
-
}, [
|
|
260
|
+
}, [type, slug, branch, tree, renderPreview, router, originsKey]);
|
|
270
261
|
|
|
271
262
|
if (removed) return <PreviewRemoved />;
|
|
272
263
|
return (
|
package/src/preview-route.ts
CHANGED
|
@@ -277,13 +277,12 @@ export function createPreviewRoute(
|
|
|
277
277
|
}
|
|
278
278
|
const checkout = await checkoutFrom(req, secret, url);
|
|
279
279
|
if (!checkout) return new Response("preview session missing", { status: 401 });
|
|
280
|
-
const locale = url.searchParams.get("locale") ?? cms.config.defaultLocale;
|
|
281
280
|
const type = url.searchParams.get("type") ?? "";
|
|
282
281
|
const slug = url.searchParams.get("slug") ?? "";
|
|
283
282
|
if (!type || !slug) return new Response("missing type or slug", { status: 400 });
|
|
284
283
|
const upstream = previewStreamUrl(
|
|
285
284
|
{ apiUrl: cms.config.apiUrl, workspaceId: cms.config.workspaceId, previewToken: secret },
|
|
286
|
-
{
|
|
285
|
+
{ type, slug, ...checkout },
|
|
287
286
|
);
|
|
288
287
|
const res = await fetch(upstream);
|
|
289
288
|
return new Response(res.body, {
|
package/src/render-preview.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { requireSource, resolveSourceData } from "./source-data";
|
|
|
17
17
|
export const RENDER_PREVIEW_PER_SECOND = 20;
|
|
18
18
|
|
|
19
19
|
export type RenderPreviewRef = {
|
|
20
|
-
locale: string;
|
|
21
20
|
type: string;
|
|
22
21
|
slug: string;
|
|
23
22
|
branch?: string;
|
|
@@ -165,7 +164,6 @@ export function createRenderPreview(
|
|
|
165
164
|
const node = await runWithPreviewContent(content, () =>
|
|
166
165
|
renderLayoutDoc(
|
|
167
166
|
{
|
|
168
|
-
locale: ref.locale,
|
|
169
167
|
type: ref.type,
|
|
170
168
|
slug: ref.slug,
|
|
171
169
|
source: "tree",
|
package/src/routes.ts
CHANGED
|
@@ -6,19 +6,17 @@
|
|
|
6
6
|
import { Fragment, Suspense, createElement, type ReactNode } from "react";
|
|
7
7
|
import { HeldPreview, HoldPreview } from "./preview-hold";
|
|
8
8
|
import {
|
|
9
|
-
decodeDocLocale,
|
|
10
9
|
DEFAULT_TEMPLATE_SLUG,
|
|
11
10
|
isRoutable,
|
|
12
11
|
isTemplateSlug,
|
|
13
12
|
matchRoute,
|
|
14
13
|
PREVIEW_CHECKOUT_QUERY,
|
|
15
14
|
PREVIEW_COOKIE,
|
|
16
|
-
PREVIEW_LOCALE_QUERY,
|
|
17
15
|
readPreviewCookie,
|
|
18
16
|
routeSegments,
|
|
19
|
-
routeTemplate,
|
|
20
17
|
type BackmatterDoc,
|
|
21
18
|
type ModelDef,
|
|
19
|
+
type RouteMatch,
|
|
22
20
|
type SourceDef,
|
|
23
21
|
} from "@backmatter/core";
|
|
24
22
|
import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
|
|
@@ -53,10 +51,6 @@ export type CatchAllOptions = {
|
|
|
53
51
|
* third argument. A missing record is a 404.
|
|
54
52
|
*/
|
|
55
53
|
sources?: Record<string, SourceDef>;
|
|
56
|
-
/** Overrides `cms.config.locales`. */
|
|
57
|
-
locales?: readonly string[];
|
|
58
|
-
/** Overrides `cms.config.defaultLocale`. */
|
|
59
|
-
defaultLocale?: string;
|
|
60
54
|
/**
|
|
61
55
|
* Wrap the rendered body (layout, title, metadata). Default: the body only.
|
|
62
56
|
*/
|
|
@@ -69,7 +63,6 @@ export type CatchAllOptions = {
|
|
|
69
63
|
*/
|
|
70
64
|
renderPreview?: (
|
|
71
65
|
ref: {
|
|
72
|
-
locale: string;
|
|
73
66
|
type: string;
|
|
74
67
|
slug: string;
|
|
75
68
|
branch?: string;
|
|
@@ -89,7 +82,6 @@ export type CatchAllOptions = {
|
|
|
89
82
|
checkout?: { branch?: string; tree?: string };
|
|
90
83
|
error?: string;
|
|
91
84
|
getDoc?: (ref: {
|
|
92
|
-
locale: string;
|
|
93
85
|
type: string;
|
|
94
86
|
slug: string;
|
|
95
87
|
branch?: string;
|
|
@@ -138,19 +130,7 @@ export function previewReplacesPublished(
|
|
|
138
130
|
return preview.enabled && preview.error !== "invalid_session";
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
type RouteHit =
|
|
142
|
-
|
|
143
|
-
async function previewLocaleQuery(
|
|
144
|
-
searchParams: Promise<CatchAllSearchParams> | CatchAllSearchParams | undefined,
|
|
145
|
-
): Promise<string | undefined> {
|
|
146
|
-
if (!searchParams) return undefined;
|
|
147
|
-
const query = await searchParams;
|
|
148
|
-
const raw = query[PREVIEW_LOCALE_QUERY];
|
|
149
|
-
if (raw === undefined) return undefined;
|
|
150
|
-
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
151
|
-
if (value === undefined) return undefined;
|
|
152
|
-
return decodeDocLocale(value);
|
|
153
|
-
}
|
|
133
|
+
type RouteHit = RouteMatch;
|
|
154
134
|
|
|
155
135
|
async function checkoutQueryToken(
|
|
156
136
|
searchParams: Promise<CatchAllSearchParams> | CatchAllSearchParams | undefined,
|
|
@@ -202,23 +182,18 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
202
182
|
): CatchAllRoute {
|
|
203
183
|
const models = cms.models as readonly ModelDef[];
|
|
204
184
|
const byType = new Map(models.map((m) => [m.type, m]));
|
|
205
|
-
const matchOptions = {
|
|
206
|
-
defaultLocale: options.defaultLocale ?? cms.config.defaultLocale,
|
|
207
|
-
locales: options.locales ?? cms.config.locales,
|
|
208
|
-
};
|
|
209
185
|
|
|
210
186
|
const resolve: CatchAllRoute["resolve"] = (segments) => {
|
|
211
187
|
const pathname = `/${(segments ?? []).join("/")}`;
|
|
212
|
-
return matchRoute(models, pathname
|
|
188
|
+
return matchRoute(models, pathname);
|
|
213
189
|
};
|
|
214
190
|
|
|
215
191
|
const generateStaticParams: CatchAllRoute["generateStaticParams"] = async () => {
|
|
216
192
|
const docs = await cms.listDocs();
|
|
217
193
|
const params: CatchAllParams[] = [];
|
|
218
194
|
const seen = new Set<string>();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const segments = routeSegments(model, locale, slug);
|
|
195
|
+
const push = (model: ModelDef, slug: string) => {
|
|
196
|
+
const segments = routeSegments(model, slug);
|
|
222
197
|
const key = segments.join("/");
|
|
223
198
|
if (seen.has(key)) return;
|
|
224
199
|
seen.add(key);
|
|
@@ -227,32 +202,22 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
227
202
|
for (const doc of docs) {
|
|
228
203
|
const model = byType.get(doc.type);
|
|
229
204
|
if (!model || !isRoutable(model)) continue;
|
|
230
|
-
// The template document has no page of its own.
|
|
231
205
|
if (isTemplateSlug(model, doc.slug)) continue;
|
|
232
|
-
push(model, doc.
|
|
206
|
+
push(model, doc.slug);
|
|
233
207
|
}
|
|
234
|
-
// Catalog records that never got a CMS file still prerender through the template.
|
|
235
|
-
const locales = matchOptions.locales ?? [matchOptions.defaultLocale];
|
|
236
208
|
for (const model of models) {
|
|
237
209
|
if (!isRoutable(model)) continue;
|
|
238
210
|
const list = options.sources?.[model.type]?.list;
|
|
239
211
|
if (!list) continue;
|
|
240
|
-
for (const
|
|
241
|
-
for (const slug of await list(locale)) push(model, locale, slug);
|
|
242
|
-
}
|
|
212
|
+
for (const slug of await list()) push(model, slug);
|
|
243
213
|
}
|
|
244
214
|
return params;
|
|
245
215
|
};
|
|
246
216
|
|
|
247
217
|
const failFor = options.notFound ?? nextNotFound;
|
|
248
218
|
|
|
249
|
-
type GetDocFn = (
|
|
219
|
+
type GetDocFn = (type: string, slug: string) => Promise<BackmatterDoc | null>;
|
|
250
220
|
|
|
251
|
-
/**
|
|
252
|
-
* Document plus external record for a route hit, or null for a 404.
|
|
253
|
-
* External types: no record → null; no per-slug doc → the template doc.
|
|
254
|
-
* The template slug itself only renders when `allowTemplate` (preview).
|
|
255
|
-
*/
|
|
256
221
|
async function loadRouteDoc(
|
|
257
222
|
hit: RouteHit,
|
|
258
223
|
getDoc: GetDocFn,
|
|
@@ -265,13 +230,13 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
265
230
|
if (onTemplate && !allowTemplate) return null;
|
|
266
231
|
const resolved = await resolveSourceData(source, model, hit);
|
|
267
232
|
if (!resolved.ok) return null;
|
|
268
|
-
let doc = await getDoc(hit.
|
|
233
|
+
let doc = await getDoc(hit.type, hit.slug);
|
|
269
234
|
if (!doc && model.external && !onTemplate) {
|
|
270
235
|
const template = model.template ?? DEFAULT_TEMPLATE_SLUG;
|
|
271
|
-
doc = await getDoc(hit.
|
|
236
|
+
doc = await getDoc(hit.type, template);
|
|
272
237
|
if (!doc) {
|
|
273
238
|
console.warn(
|
|
274
|
-
`[backmatter] ${hit.type}/${hit.slug} has a record but no document; add content/${hit.
|
|
239
|
+
`[backmatter] ${hit.type}/${hit.slug} has a record but no document; add content/${hit.type}/${template} to render every ${hit.type}`,
|
|
275
240
|
);
|
|
276
241
|
}
|
|
277
242
|
}
|
|
@@ -313,7 +278,6 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
313
278
|
return createElement(
|
|
314
279
|
PreviewClient,
|
|
315
280
|
{
|
|
316
|
-
locale: doc.locale,
|
|
317
281
|
type: doc.type,
|
|
318
282
|
slug: doc.slug,
|
|
319
283
|
branch: preview.branch,
|
|
@@ -324,13 +288,11 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
324
288
|
inner,
|
|
325
289
|
);
|
|
326
290
|
}
|
|
327
|
-
// Hosts that have not generated `preview-action` keep the refresh fallback.
|
|
328
291
|
const { PreviewPageReloader } = await import("./preview-refresh");
|
|
329
292
|
return createElement(
|
|
330
293
|
"div",
|
|
331
294
|
null,
|
|
332
295
|
createElement(PreviewPageReloader, {
|
|
333
|
-
locale: doc.locale,
|
|
334
296
|
type: doc.type,
|
|
335
297
|
slug: doc.slug,
|
|
336
298
|
}),
|
|
@@ -357,7 +319,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
357
319
|
));
|
|
358
320
|
const loaded = await loadRouteDoc(
|
|
359
321
|
hit,
|
|
360
|
-
(
|
|
322
|
+
(type, slug) => getDoc({ type, slug, ...preview.checkout }),
|
|
361
323
|
true,
|
|
362
324
|
);
|
|
363
325
|
if (!loaded) return previewUnavailable("Document not found");
|
|
@@ -367,8 +329,8 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
367
329
|
}
|
|
368
330
|
}
|
|
369
331
|
|
|
370
|
-
async function renderPublished(
|
|
371
|
-
const loaded = await loadRouteDoc({
|
|
332
|
+
async function renderPublished(type: string, slug: string): Promise<ReactNode> {
|
|
333
|
+
const loaded = await loadRouteDoc({ type, slug }, cms.getDoc, false);
|
|
372
334
|
if (!loaded) return null;
|
|
373
335
|
return createElement(
|
|
374
336
|
"div",
|
|
@@ -377,12 +339,10 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
377
339
|
);
|
|
378
340
|
}
|
|
379
341
|
|
|
380
|
-
async function publishedBody(
|
|
342
|
+
async function publishedBody(type: string, slug: string): Promise<ReactNode> {
|
|
381
343
|
registerPublishedRender(renderPublished);
|
|
382
|
-
const body = await getCachedPublished(
|
|
344
|
+
const body = await getCachedPublished(type, slug);
|
|
383
345
|
if (body != null) return body;
|
|
384
|
-
// A tree-only document has no published body. 404 only when preview is off,
|
|
385
|
-
// so the preview branch can still render it. The dynamic read is on the miss path.
|
|
386
346
|
if (options.preview) {
|
|
387
347
|
const state: PreviewState = options.preview.enabled
|
|
388
348
|
? {
|
|
@@ -405,29 +365,13 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
405
365
|
return failFor();
|
|
406
366
|
}
|
|
407
367
|
|
|
408
|
-
/**
|
|
409
|
-
* Routes without `{locale}` always match as the default locale. The editor
|
|
410
|
-
* puts the file's locale on the preview URL so a conflicted or translated
|
|
411
|
-
* copy is the document that gets fetched.
|
|
412
|
-
*/
|
|
413
|
-
async function previewHit(
|
|
414
|
-
hit: RouteHit,
|
|
415
|
-
searchParams: Promise<CatchAllSearchParams> | CatchAllSearchParams | undefined,
|
|
416
|
-
): Promise<RouteHit> {
|
|
417
|
-
const model = byType.get(hit.type);
|
|
418
|
-
if (!model || routeTemplate(model).includes("{locale}")) return hit;
|
|
419
|
-
const locale = await previewLocaleQuery(searchParams);
|
|
420
|
-
if (locale === undefined) return hit;
|
|
421
|
-
return { ...hit, locale };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
368
|
const CmsRoute: CatchAllRoute["CmsRoute"] = async ({ params, searchParams }) => {
|
|
425
369
|
const resolved = await cachedRouteParams(params);
|
|
426
370
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
427
371
|
if (!hit) return failFor();
|
|
428
372
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
429
|
-
if (previewReplacesPublished(preview)) return loadPreviewDoc(
|
|
430
|
-
return publishedBody(hit.
|
|
373
|
+
if (previewReplacesPublished(preview)) return loadPreviewDoc(hit, preview);
|
|
374
|
+
return publishedBody(hit.type, hit.slug);
|
|
431
375
|
};
|
|
432
376
|
|
|
433
377
|
const PublishedRoute = async ({
|
|
@@ -439,7 +383,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
439
383
|
const resolved = await params;
|
|
440
384
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
441
385
|
if (!hit) return failFor();
|
|
442
|
-
const body = await publishedBody(hit.
|
|
386
|
+
const body = await publishedBody(hit.type, hit.slug);
|
|
443
387
|
if (body == null) return body;
|
|
444
388
|
return createElement(Fragment, null, docBridge(hit), body);
|
|
445
389
|
};
|
|
@@ -456,7 +400,6 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
456
400
|
if (!hit) return null;
|
|
457
401
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
458
402
|
if (!previewReplacesPublished(preview)) return null;
|
|
459
|
-
const docHit = await previewHit(hit, searchParams);
|
|
460
403
|
return createElement(
|
|
461
404
|
HoldPreview,
|
|
462
405
|
null,
|
|
@@ -468,17 +411,16 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
468
411
|
__html: "[data-backmatter-published]{display:none!important}",
|
|
469
412
|
},
|
|
470
413
|
}),
|
|
471
|
-
docBridge(
|
|
472
|
-
await loadPreviewDoc(
|
|
414
|
+
docBridge(hit),
|
|
415
|
+
await loadPreviewDoc(hit, preview),
|
|
473
416
|
),
|
|
474
417
|
);
|
|
475
418
|
};
|
|
476
419
|
|
|
477
|
-
function docBridge(hit:
|
|
420
|
+
function docBridge(hit: RouteHit) {
|
|
478
421
|
if (!cms.config.workspaceId) return null;
|
|
479
422
|
return createElement(SiteDocBridge, {
|
|
480
423
|
doc: {
|
|
481
|
-
locale: hit.locale,
|
|
482
424
|
type: hit.type,
|
|
483
425
|
slug: hit.slug,
|
|
484
426
|
workspaceId: cms.config.workspaceId,
|
package/src/site-doc-bridge.tsx
CHANGED
|
@@ -35,13 +35,12 @@ export function subscribeSiteDoc(listener: (doc: SiteDocRef) => void): () => voi
|
|
|
35
35
|
export function SiteDocBridge({ doc }: { doc: SiteDocRef }) {
|
|
36
36
|
useEffect(() => {
|
|
37
37
|
publishSiteDoc(doc);
|
|
38
|
-
}, [doc.
|
|
38
|
+
}, [doc.type, doc.slug, doc.workspaceId]);
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
41
|
<span
|
|
42
42
|
hidden
|
|
43
43
|
data-backmatter-doc=""
|
|
44
|
-
data-locale={doc.locale}
|
|
45
44
|
data-type={doc.type}
|
|
46
45
|
data-slug={doc.slug}
|
|
47
46
|
data-workspace={doc.workspaceId}
|
package/src/site-doc.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// packages/next/src/site-doc.ts
|
|
2
2
|
export type SiteDocRef = {
|
|
3
|
-
locale: string;
|
|
4
3
|
type: string;
|
|
5
4
|
slug: string;
|
|
6
5
|
workspaceId: string;
|
|
@@ -11,7 +10,6 @@ export function parseSiteDoc(content: string | null | undefined): SiteDocRef | n
|
|
|
11
10
|
try {
|
|
12
11
|
const value = JSON.parse(content) as Record<string, unknown>;
|
|
13
12
|
if (
|
|
14
|
-
(value.locale !== undefined && typeof value.locale !== "string") ||
|
|
15
13
|
typeof value.type !== "string" ||
|
|
16
14
|
typeof value.slug !== "string" ||
|
|
17
15
|
typeof value.workspaceId !== "string" ||
|
|
@@ -22,7 +20,6 @@ export function parseSiteDoc(content: string | null | undefined): SiteDocRef | n
|
|
|
22
20
|
return null;
|
|
23
21
|
}
|
|
24
22
|
return {
|
|
25
|
-
locale: value.locale ?? "",
|
|
26
23
|
type: value.type,
|
|
27
24
|
slug: value.slug,
|
|
28
25
|
workspaceId: value.workspaceId,
|
|
@@ -37,7 +34,6 @@ export function readSiteDoc(): SiteDocRef | null {
|
|
|
37
34
|
const marked = document.querySelector("[data-backmatter-doc]");
|
|
38
35
|
if (marked) {
|
|
39
36
|
const doc = {
|
|
40
|
-
locale: marked.getAttribute("data-locale") ?? "",
|
|
41
37
|
type: marked.getAttribute("data-type") ?? "",
|
|
42
38
|
slug: marked.getAttribute("data-slug") ?? "",
|
|
43
39
|
workspaceId: marked.getAttribute("data-workspace") ?? "",
|
|
@@ -50,12 +46,7 @@ export function readSiteDoc(): SiteDocRef | null {
|
|
|
50
46
|
export function sameSiteDoc(a: SiteDocRef | null, b: SiteDocRef | null): boolean {
|
|
51
47
|
if (a === b) return true;
|
|
52
48
|
if (!a || !b) return false;
|
|
53
|
-
return
|
|
54
|
-
a.locale === b.locale &&
|
|
55
|
-
a.type === b.type &&
|
|
56
|
-
a.slug === b.slug &&
|
|
57
|
-
a.workspaceId === b.workspaceId
|
|
58
|
-
);
|
|
49
|
+
return a.type === b.type && a.slug === b.slug && a.workspaceId === b.workspaceId;
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
export type SiteBlock = {
|
package/src/site-editor.tsx
CHANGED
|
@@ -45,7 +45,7 @@ function useSiteDoc(): SiteDocRef | null {
|
|
|
45
45
|
subtree: true,
|
|
46
46
|
childList: true,
|
|
47
47
|
attributes: true,
|
|
48
|
-
attributeFilter: ["data-
|
|
48
|
+
attributeFilter: ["data-type", "data-slug", "data-workspace"],
|
|
49
49
|
});
|
|
50
50
|
return () => {
|
|
51
51
|
stop();
|
package/src/source-data.ts
CHANGED
|
@@ -33,7 +33,7 @@ export async function resolveSourceData(
|
|
|
33
33
|
): Promise<SourceResult> {
|
|
34
34
|
if (!source) return { ok: true, data: undefined };
|
|
35
35
|
if (isTemplateSlug(model, ref.slug)) {
|
|
36
|
-
const sample = source.sample ? await source.sample(
|
|
36
|
+
const sample = source.sample ? await source.sample() : null;
|
|
37
37
|
return { ok: true, data: sample };
|
|
38
38
|
}
|
|
39
39
|
const record = await source.load(ref);
|