@backmatter/next 0.0.27 → 0.0.28
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/package.json +3 -3
- package/src/devbar-client.tsx +62 -21
- package/src/devbar.tsx +10 -2
- package/src/preview-route.ts +54 -2
- package/src/site-editor.tsx +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"test": "bun test"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@backmatter/core": "^0.0.
|
|
50
|
+
"@backmatter/core": "^0.0.22",
|
|
51
51
|
"@mdx-js/mdx": "^3.1.1",
|
|
52
52
|
"remark-gfm": "^4.0.1",
|
|
53
|
-
"@backmatter/studio": "^0.0.
|
|
53
|
+
"@backmatter/studio": "^0.0.6"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": ">=16.3.0",
|
package/src/devbar-client.tsx
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
clearHostPreviewSession,
|
|
7
7
|
fetchCmsSession,
|
|
8
8
|
fetchHostPreviewSession,
|
|
9
|
+
setHostSiteLive,
|
|
10
|
+
setHostSitePreview,
|
|
9
11
|
resolveDevMode,
|
|
10
12
|
type CmsSessionUser,
|
|
11
13
|
type HostSessionReason,
|
|
@@ -47,6 +49,7 @@ export type SessionGateAction = "show" | "bounce" | "quiet" | "wait";
|
|
|
47
49
|
*/
|
|
48
50
|
export function sessionGateAction(input: {
|
|
49
51
|
explicit: boolean;
|
|
52
|
+
/** Host session verified (preview or signed-in live). */
|
|
50
53
|
enabled: boolean;
|
|
51
54
|
reason: HostSessionReason;
|
|
52
55
|
recentAttempt: boolean;
|
|
@@ -59,9 +62,15 @@ export function sessionGateAction(input: {
|
|
|
59
62
|
|
|
60
63
|
export function devModeState(
|
|
61
64
|
user: CmsSessionUser | null,
|
|
62
|
-
session: {
|
|
65
|
+
session: {
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
signedIn?: boolean;
|
|
68
|
+
branch?: string;
|
|
69
|
+
tree?: string;
|
|
70
|
+
reason?: HostSessionReason;
|
|
71
|
+
},
|
|
63
72
|
): DevModeState {
|
|
64
|
-
if (!user && !session.enabled) {
|
|
73
|
+
if (!user && !session.enabled && !session.signedIn) {
|
|
65
74
|
return session.reason ? { status: "hidden", reason: session.reason } : { status: "hidden" };
|
|
66
75
|
}
|
|
67
76
|
const resolved = resolveDevMode(session);
|
|
@@ -339,11 +348,11 @@ function useSiteSession(gate: Gate, previewEntryPath: string): DevModeState {
|
|
|
339
348
|
let cancelled = false;
|
|
340
349
|
void loadDevModeState("").then((next) => {
|
|
341
350
|
if (cancelled) return;
|
|
342
|
-
const
|
|
351
|
+
const showBar = next.status === "ready";
|
|
343
352
|
const reason = next.status === "hidden" ? (next.reason ?? "anonymous") : "anonymous";
|
|
344
353
|
const action = sessionGateAction({
|
|
345
354
|
explicit: gate === "enter",
|
|
346
|
-
enabled,
|
|
355
|
+
enabled: showBar,
|
|
347
356
|
reason,
|
|
348
357
|
recentAttempt: recentEnterAttempt(sessionStorage.getItem(ENTER_ATTEMPT_KEY), Date.now()),
|
|
349
358
|
});
|
|
@@ -376,38 +385,48 @@ function useSiteSession(gate: Gate, previewEntryPath: string): DevModeState {
|
|
|
376
385
|
export function DevBarClient({
|
|
377
386
|
studioPath,
|
|
378
387
|
previewEntryPath = DEFAULT_PREVIEW_ENTRY_PATH,
|
|
388
|
+
previewFrame,
|
|
379
389
|
}: {
|
|
380
390
|
/** Unused since the bar stopped calling the CMS origin; kept for host layouts that still pass it. */
|
|
381
391
|
apiUrl?: string;
|
|
382
392
|
studioPath?: string;
|
|
383
393
|
previewEntryPath?: string;
|
|
394
|
+
previewFrame?: boolean;
|
|
384
395
|
}) {
|
|
385
396
|
const gate = useGate();
|
|
386
397
|
const state = useSiteSession(gate, previewEntryPath);
|
|
387
398
|
const [exiting, setExiting] = useState(false);
|
|
388
|
-
const
|
|
399
|
+
const [entering, setEntering] = useState(false);
|
|
400
|
+
const goLive = () => {
|
|
389
401
|
if (exiting) return;
|
|
390
402
|
setExiting(true);
|
|
391
|
-
void
|
|
403
|
+
void goHostSiteLive().then((ok) => {
|
|
392
404
|
if (!ok) setExiting(false);
|
|
393
405
|
});
|
|
394
406
|
};
|
|
407
|
+
const goPreview = () => {
|
|
408
|
+
if (entering) return;
|
|
409
|
+
setEntering(true);
|
|
410
|
+
void enterHostPreview().then((ok) => {
|
|
411
|
+
if (!ok) setEntering(false);
|
|
412
|
+
});
|
|
413
|
+
};
|
|
395
414
|
if (state.status !== "ready") return null;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
415
|
+
return (
|
|
416
|
+
<Suspense fallback={null}>
|
|
417
|
+
<SiteChrome
|
|
418
|
+
branch={state.branch}
|
|
419
|
+
tree={state.tree}
|
|
420
|
+
siteMode={state.mode}
|
|
421
|
+
exiting={exiting || entering}
|
|
422
|
+
onExit={goLive}
|
|
423
|
+
onEnterPreview={goPreview}
|
|
424
|
+
studioPath={studioPath}
|
|
425
|
+
previewEntryPath={previewEntryPath}
|
|
426
|
+
previewFrame={state.mode === "preview" ? previewFrame : false}
|
|
427
|
+
/>
|
|
428
|
+
</Suspense>
|
|
429
|
+
);
|
|
411
430
|
}
|
|
412
431
|
|
|
413
432
|
export async function exitHostPreview(
|
|
@@ -420,3 +439,25 @@ export async function exitHostPreview(
|
|
|
420
439
|
if (ok) reload();
|
|
421
440
|
return ok;
|
|
422
441
|
}
|
|
442
|
+
|
|
443
|
+
export async function goHostSiteLive(
|
|
444
|
+
live: (origin?: string) => Promise<boolean> = setHostSiteLive,
|
|
445
|
+
reload: () => void = () => {
|
|
446
|
+
location.reload();
|
|
447
|
+
},
|
|
448
|
+
): Promise<boolean> {
|
|
449
|
+
const ok = await live("");
|
|
450
|
+
if (ok) reload();
|
|
451
|
+
return ok;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function enterHostPreview(
|
|
455
|
+
preview: (origin?: string) => Promise<boolean> = setHostSitePreview,
|
|
456
|
+
reload: () => void = () => {
|
|
457
|
+
location.reload();
|
|
458
|
+
},
|
|
459
|
+
): Promise<boolean> {
|
|
460
|
+
const ok = await preview("");
|
|
461
|
+
if (ok) reload();
|
|
462
|
+
return ok;
|
|
463
|
+
}
|
package/src/devbar.tsx
CHANGED
|
@@ -18,23 +18,31 @@ export {
|
|
|
18
18
|
/**
|
|
19
19
|
* Host layout: `<DevBar />`. Public visitors get a gate of a few hundred
|
|
20
20
|
* bytes that makes no request. The bar appears only when the site session
|
|
21
|
-
* verifies. `?backmatter=1` or ⌘⇧E with no session sends the whole window to
|
|
21
|
+
* verifies (preview or signed-in live). `?backmatter=1` or ⌘⇧E with no session sends the whole window to
|
|
22
22
|
* the dashboard; a leftover `bm_ui` marker stays on the public page.
|
|
23
23
|
* `studioPath` is where `@backmatter/studio` is mounted on this site
|
|
24
24
|
* (default `/backmatter`); the bar's panels are frames of that route.
|
|
25
25
|
* `previewEntryPath` is the ticket route (default `/api/backmatter/preview`).
|
|
26
|
+
* `previewFrame` draws a viewport ring and top pill while preview is active.
|
|
26
27
|
* `apiUrl` is accepted for older layouts and no longer used.
|
|
27
28
|
*/
|
|
28
29
|
export function DevBar({
|
|
29
30
|
apiUrl,
|
|
30
31
|
studioPath,
|
|
31
32
|
previewEntryPath,
|
|
32
|
-
|
|
33
|
+
previewFrame,
|
|
34
|
+
}: {
|
|
35
|
+
apiUrl?: string;
|
|
36
|
+
studioPath?: string;
|
|
37
|
+
previewEntryPath?: string;
|
|
38
|
+
previewFrame?: boolean;
|
|
39
|
+
} = {}) {
|
|
33
40
|
return (
|
|
34
41
|
<DevBarClient
|
|
35
42
|
apiUrl={resolveDevBarApiUrl(apiUrl)}
|
|
36
43
|
studioPath={studioPath}
|
|
37
44
|
previewEntryPath={previewEntryPath}
|
|
45
|
+
previewFrame={previewFrame}
|
|
38
46
|
/>
|
|
39
47
|
);
|
|
40
48
|
}
|
package/src/preview-route.ts
CHANGED
|
@@ -179,8 +179,25 @@ export function createPreviewRoute(
|
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
if (verdict !== "ok") return clearLogin();
|
|
182
|
+
const draftOn = await (deps.isDraftEnabled ?? nextDraftEnabled)();
|
|
183
|
+
const checkout = secret ? await checkoutFrom(req, secret, url) : null;
|
|
184
|
+
if (draftOn && checkout) {
|
|
185
|
+
return Response.json(
|
|
186
|
+
{
|
|
187
|
+
mode: "preview",
|
|
188
|
+
branch: checkout.branch ?? claims.branch,
|
|
189
|
+
tree: checkout.tree ?? claims.tree,
|
|
190
|
+
},
|
|
191
|
+
{ headers: { "Cache-Control": "no-store" } },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
182
194
|
return Response.json(
|
|
183
|
-
{
|
|
195
|
+
{
|
|
196
|
+
mode: "live",
|
|
197
|
+
signedIn: true,
|
|
198
|
+
branch: claims.branch,
|
|
199
|
+
tree: claims.tree,
|
|
200
|
+
},
|
|
184
201
|
{ headers: { "Cache-Control": "no-store" } },
|
|
185
202
|
);
|
|
186
203
|
}
|
|
@@ -310,8 +327,43 @@ export function createPreviewRoute(
|
|
|
310
327
|
return new Response(null, { status: 204, headers });
|
|
311
328
|
}
|
|
312
329
|
|
|
330
|
+
const secure = url.protocol === "https:";
|
|
331
|
+
|
|
332
|
+
if (url.searchParams.get("live") === "1") {
|
|
333
|
+
await (deps.disableDraft ?? (() => nextDraft(false)))();
|
|
334
|
+
const headers = new Headers();
|
|
335
|
+
headers.append("Set-Cookie", cookieBase(PREVIEW_COOKIE, secure, 0, ""));
|
|
336
|
+
return new Response(null, { status: 204, headers });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (url.searchParams.get("preview") === "1") {
|
|
340
|
+
const token = cookieFrom(req, PREVIEW_SESSION_COOKIE);
|
|
341
|
+
const claims = secret && token ? await readPreviewSession(secret, token) : null;
|
|
342
|
+
const workspaceOk = !cms.config.workspaceId || claims?.workspaceId === cms.config.workspaceId;
|
|
343
|
+
if (!claims || !token || !workspaceOk) {
|
|
344
|
+
return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
345
|
+
}
|
|
346
|
+
const confirm = deps.confirmSession ?? ((current: string) => confirmSessionOnApi(cms.config.apiUrl, current));
|
|
347
|
+
const verdict = await confirm(token);
|
|
348
|
+
if (verdict !== "ok") {
|
|
349
|
+
return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
350
|
+
}
|
|
351
|
+
const value = await writePreviewCookie(secret, {
|
|
352
|
+
branch: claims.branch,
|
|
353
|
+
tree: claims.tree,
|
|
354
|
+
exp: now() + PREVIEW_COOKIE_TTL_MS,
|
|
355
|
+
});
|
|
356
|
+
await (deps.enableDraft ?? (() => nextDraft(true)))();
|
|
357
|
+
const headers = new Headers();
|
|
358
|
+
headers.append(
|
|
359
|
+
"Set-Cookie",
|
|
360
|
+
cookieBase(PREVIEW_COOKIE, secure, Math.floor(PREVIEW_COOKIE_TTL_MS / 1000), value),
|
|
361
|
+
);
|
|
362
|
+
headers.append("Set-Cookie", markerCookieHeader(secure, true));
|
|
363
|
+
return new Response(null, { status: 204, headers });
|
|
364
|
+
}
|
|
365
|
+
|
|
313
366
|
await (deps.disableDraft ?? (() => nextDraft(false)))();
|
|
314
|
-
const secure = new URL(req.url).protocol === "https:";
|
|
315
367
|
const headers = new Headers();
|
|
316
368
|
headers.append("Set-Cookie", cookieBase(PREVIEW_COOKIE, secure, 0, ""));
|
|
317
369
|
headers.append("Set-Cookie", cookieBase(PREVIEW_SESSION_COOKIE, secure, 0, ""));
|
package/src/site-editor.tsx
CHANGED
|
@@ -62,18 +62,24 @@ export const DEFAULT_STUDIO_PATH = "/backmatter";
|
|
|
62
62
|
export function SiteChrome({
|
|
63
63
|
branch,
|
|
64
64
|
tree,
|
|
65
|
+
siteMode = "preview",
|
|
65
66
|
exiting,
|
|
66
67
|
onExit,
|
|
68
|
+
onEnterPreview,
|
|
67
69
|
previewEntryPath = DEFAULT_PREVIEW_ENTRY_PATH,
|
|
70
|
+
previewFrame,
|
|
68
71
|
}: {
|
|
69
72
|
branch?: string;
|
|
70
73
|
tree?: string;
|
|
74
|
+
siteMode?: "preview" | "live";
|
|
71
75
|
exiting?: boolean;
|
|
72
76
|
onExit: () => void;
|
|
77
|
+
onEnterPreview?: () => void;
|
|
73
78
|
/** Kept so existing hosts can still pass it. The bar does not open the document editor. */
|
|
74
79
|
studioPath?: string;
|
|
75
80
|
/** Ticket route. `POST ?renew=1` slides the site session. A 401 drops the bar. */
|
|
76
81
|
previewEntryPath?: string;
|
|
82
|
+
previewFrame?: boolean;
|
|
77
83
|
}) {
|
|
78
84
|
const router = useRouter();
|
|
79
85
|
const doc = useSiteDoc();
|
|
@@ -125,10 +131,14 @@ export function SiteChrome({
|
|
|
125
131
|
doc={doc}
|
|
126
132
|
branch={branch}
|
|
127
133
|
tree={tree}
|
|
134
|
+
siteMode={siteMode}
|
|
128
135
|
exiting={exiting}
|
|
129
136
|
onExit={onExit}
|
|
137
|
+
onEnterPreview={onEnterPreview}
|
|
130
138
|
onDocChanged={() => startTransition(() => router.refresh())}
|
|
131
139
|
onSessionExpired={leave}
|
|
140
|
+
previewEntryPath={previewEntryPath}
|
|
141
|
+
previewFrame={previewFrame}
|
|
132
142
|
version={studioPkg.version}
|
|
133
143
|
/>
|
|
134
144
|
);
|