@backmatter/next 0.0.12 → 0.0.13

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 CHANGED
@@ -180,8 +180,40 @@ function detectSdk(root) {
180
180
  } catch {}
181
181
  return "@backmatter/core/node";
182
182
  }
183
+ function renderCachedCmsClient() {
184
+ return [
185
+ "const raw = createBackmatter({ ...config, models });",
186
+ "",
187
+ "async function getDoc(locale: string, type: string, slug: string) {",
188
+ ' "use cache";',
189
+ ' cacheLife("hours");',
190
+ " cacheTag(...contentCacheTags(locale, type, slug));",
191
+ " return raw.getDoc(locale, type, slug);",
192
+ "}",
193
+ "",
194
+ "async function listDocs(filter?: { type?: string; locale?: string }) {",
195
+ ' "use cache";',
196
+ ' cacheLife("hours");',
197
+ " cacheTag(...listCacheTags(filter?.type));",
198
+ " return raw.listDocs(filter);",
199
+ "}",
200
+ "",
201
+ "/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */",
202
+ "export const cms = {",
203
+ " ...raw,",
204
+ " getDoc,",
205
+ " listDocs,",
206
+ ` query: createQuery(models, { getDoc, listDocs }),`,
207
+ "};"
208
+ ];
209
+ }
183
210
  function renderModelsBarrel(input) {
211
+ const nextHost = input.sdk === "@backmatter/next";
184
212
  const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
213
+ if (nextHost) {
214
+ lines.push('import { contentCacheTags, createQuery, listCacheTags } from "@backmatter/core";');
215
+ lines.push('import { cacheLife, cacheTag } from "next/cache";');
216
+ }
185
217
  if (input.configFile) {
186
218
  lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
187
219
  } else {
@@ -194,8 +226,12 @@ function renderModelsBarrel(input) {
194
226
  lines.push(`export const models = [${input.models.map((m) => m.importName).join(", ")}] as const;`);
195
227
  lines.push("export type Models = typeof models;");
196
228
  lines.push("");
197
- lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
198
- lines.push("export const cms = createBackmatter({ ...config, models });");
229
+ if (nextHost) {
230
+ lines.push(...renderCachedCmsClient());
231
+ } else {
232
+ lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
233
+ lines.push("export const cms = createBackmatter({ ...config, models });");
234
+ }
199
235
  return `${lines.join(`
200
236
  `)}
201
237
  `;
@@ -395,10 +431,14 @@ function withBackmatter(nextConfig = {}, options = {}) {
395
431
  return cfg.webpack ? cfg.webpack(config, context) : config;
396
432
  };
397
433
  const cacheComponents = cfg.cacheComponents ?? true;
434
+ const transpilePackages = [
435
+ ...new Set([...cfg.transpilePackages ?? [], "@backmatter/next", "@backmatter/core"])
436
+ ];
398
437
  return {
399
438
  ...nextConfig,
400
439
  cacheComponents,
401
440
  partialPrefetching: cfg.partialPrefetching ?? cacheComponents,
441
+ transpilePackages,
402
442
  turbopack,
403
443
  webpack
404
444
  };
package/dist/loader.cjs CHANGED
@@ -174,8 +174,40 @@ function detectSdk(root) {
174
174
  } catch {}
175
175
  return "@backmatter/core/node";
176
176
  }
177
+ function renderCachedCmsClient() {
178
+ return [
179
+ "const raw = createBackmatter({ ...config, models });",
180
+ "",
181
+ "async function getDoc(locale: string, type: string, slug: string) {",
182
+ ' "use cache";',
183
+ ' cacheLife("hours");',
184
+ " cacheTag(...contentCacheTags(locale, type, slug));",
185
+ " return raw.getDoc(locale, type, slug);",
186
+ "}",
187
+ "",
188
+ "async function listDocs(filter?: { type?: string; locale?: string }) {",
189
+ ' "use cache";',
190
+ ' cacheLife("hours");',
191
+ " cacheTag(...listCacheTags(filter?.type));",
192
+ " return raw.listDocs(filter);",
193
+ "}",
194
+ "",
195
+ "/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */",
196
+ "export const cms = {",
197
+ " ...raw,",
198
+ " getDoc,",
199
+ " listDocs,",
200
+ ` query: createQuery(models, { getDoc, listDocs }),`,
201
+ "};"
202
+ ];
203
+ }
177
204
  function renderModelsBarrel(input) {
205
+ const nextHost = input.sdk === "@backmatter/next";
178
206
  const lines = [GENERATED_HEADER, `import { createBackmatter } from "${input.sdk}";`];
207
+ if (nextHost) {
208
+ lines.push('import { contentCacheTags, createQuery, listCacheTags } from "@backmatter/core";');
209
+ lines.push('import { cacheLife, cacheTag } from "next/cache";');
210
+ }
179
211
  if (input.configFile) {
180
212
  lines.push(`import config from "${importSpecifier(input.outDir, input.configFile)}";`);
181
213
  } else {
@@ -188,8 +220,12 @@ function renderModelsBarrel(input) {
188
220
  lines.push(`export const models = [${input.models.map((m) => m.importName).join(", ")}] as const;`);
189
221
  lines.push("export type Models = typeof models;");
190
222
  lines.push("");
191
- lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
192
- lines.push("export const cms = createBackmatter({ ...config, models });");
223
+ if (nextHost) {
224
+ lines.push(...renderCachedCmsClient());
225
+ } else {
226
+ lines.push("/** Typed content client: cms.query.{type}, cms.getDoc, cms.listDocs. */");
227
+ lines.push("export const cms = createBackmatter({ ...config, models });");
228
+ }
193
229
  return `${lines.join(`
194
230
  `)}
195
231
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/next",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -38,7 +38,7 @@
38
38
  "test": "bun test"
39
39
  },
40
40
  "dependencies": {
41
- "@backmatter/core": "^0.0.11",
41
+ "@backmatter/core": "^0.0.12",
42
42
  "@mdx-js/mdx": "^3.1.1",
43
43
  "remark-gfm": "^4.0.1"
44
44
  },
@@ -16,7 +16,7 @@ export type DevModeState =
16
16
  | { status: "hidden" }
17
17
  | {
18
18
  status: "ready";
19
- user: CmsSessionUser;
19
+ user?: CmsSessionUser;
20
20
  mode: "preview" | "live";
21
21
  branch?: string;
22
22
  tree?: string;
@@ -26,28 +26,37 @@ export function devModeState(
26
26
  user: CmsSessionUser | null,
27
27
  session: { enabled: boolean; branch?: string; tree?: string },
28
28
  ): DevModeState {
29
- if (!user) return { status: "hidden" };
29
+ if (!user && !session.enabled) return { status: "hidden" };
30
30
  const resolved = resolveDevMode(session);
31
31
  return {
32
32
  status: "ready",
33
- user,
33
+ ...(user ? { user } : {}),
34
34
  ...resolved,
35
35
  };
36
36
  }
37
37
 
38
+ export async function loadDevModeState(
39
+ apiUrl: string,
40
+ fetchUser: typeof fetchCmsSession = fetchCmsSession,
41
+ fetchSession: typeof fetchHostPreviewSession = fetchHostPreviewSession,
42
+ ): Promise<DevModeState> {
43
+ const trimmed = apiUrl.trim();
44
+ const [user, session] = await Promise.all([
45
+ trimmed ? fetchUser(trimmed) : Promise.resolve(null),
46
+ fetchSession(""),
47
+ ]);
48
+ return devModeState(user, session);
49
+ }
50
+
38
51
  export function useDevMode(options: { apiUrl: string }): DevModeState {
39
52
  const [state, setState] = useState<DevModeState>({ status: "hidden" });
40
53
 
41
54
  useEffect(() => {
42
- const apiUrl = options.apiUrl.trim();
43
- if (!apiUrl) return;
44
55
  let cancelled = false;
45
- void Promise.all([fetchCmsSession(apiUrl), fetchHostPreviewSession("")]).then(
46
- ([user, session]) => {
47
- if (cancelled) return;
48
- setState(devModeState(user, session));
49
- },
50
- );
56
+ void loadDevModeState(options.apiUrl).then((next) => {
57
+ if (cancelled) return;
58
+ setState(next);
59
+ });
51
60
  return () => {
52
61
  cancelled = true;
53
62
  };
package/src/devbar.tsx CHANGED
@@ -7,6 +7,7 @@ export {
7
7
  DevBarView,
8
8
  devModeState,
9
9
  exitHostPreview,
10
+ loadDevModeState,
10
11
  useDevMode,
11
12
  type CmsSessionUser,
12
13
  type DevModeState,
@@ -15,6 +15,7 @@ import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
15
15
  export type PreviewRouteDeps = {
16
16
  enableDraft?: () => void | Promise<void>;
17
17
  disableDraft?: () => void | Promise<void>;
18
+ isDraftEnabled?: () => boolean | Promise<boolean>;
18
19
  now?: () => number;
19
20
  };
20
21
 
@@ -45,6 +46,13 @@ async function nextDraft(enable: boolean): Promise<void> {
45
46
  else draft.disable();
46
47
  }
47
48
 
49
+ async function nextDraftEnabled(): Promise<boolean> {
50
+ const mod = (await import("next/headers" as string)) as {
51
+ draftMode: () => Promise<{ isEnabled: boolean }>;
52
+ };
53
+ return (await mod.draftMode()).isEnabled;
54
+ }
55
+
48
56
  function cookieFrom(req: Request): string | undefined {
49
57
  const header = req.headers.get("cookie");
50
58
  if (!header) return undefined;
@@ -75,11 +83,14 @@ export function createPreviewRoute(
75
83
 
76
84
  if (url.searchParams.get("session") === "1") {
77
85
  const checkout = secret ? await checkoutFrom(req, secret, url) : null;
78
- if (!checkout) return Response.json({ mode: "live" });
79
- return Response.json({
80
- mode: "preview",
81
- ...checkout,
82
- });
86
+ if (checkout) {
87
+ return Response.json({
88
+ mode: "preview",
89
+ ...checkout,
90
+ });
91
+ }
92
+ const draftOn = await (deps.isDraftEnabled ?? nextDraftEnabled)();
93
+ return Response.json({ mode: draftOn ? "preview" : "live" });
83
94
  }
84
95
 
85
96
  if (url.searchParams.get("stream") === "1") {
package/src/routes.ts CHANGED
@@ -44,6 +44,7 @@ export type CatchAllOptions = {
44
44
  preview?: {
45
45
  enabled: boolean;
46
46
  checkout?: { branch?: string; tree?: string };
47
+ error?: string;
47
48
  getDoc?: (ref: {
48
49
  locale: string;
49
50
  type: string;
@@ -87,6 +88,13 @@ type PreviewState =
87
88
  | { enabled: false }
88
89
  | { enabled: true; checkout: { branch?: string; tree?: string }; error?: string };
89
90
 
91
+ /** Draft without a readable checkout stays on the published page. */
92
+ export function previewReplacesPublished(
93
+ preview: PreviewState,
94
+ ): preview is Extract<PreviewState, { enabled: true }> {
95
+ return preview.enabled && preview.error !== "invalid_session";
96
+ }
97
+
90
98
  type RouteHit = { locale: string; type: string; slug: string };
91
99
 
92
100
  async function checkoutQueryToken(
@@ -107,7 +115,11 @@ async function resolvePreviewState(
107
115
  ): Promise<PreviewState> {
108
116
  if (injected) {
109
117
  if (!injected.enabled) return { enabled: false };
110
- return { enabled: true, checkout: injected.checkout ?? {} };
118
+ return {
119
+ enabled: true,
120
+ checkout: injected.checkout ?? {},
121
+ ...(injected.error ? { error: injected.error } : {}),
122
+ };
111
123
  }
112
124
  try {
113
125
  const mod = (await import("next/headers" as string)) as {
@@ -224,7 +236,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
224
236
  const hit = resolve(resolved[CATCH_ALL_PARAM]);
225
237
  if (!hit) return failFor();
226
238
  const preview = await resolvePreviewState(cms, options.preview, searchParams);
227
- if (preview.enabled) return loadPreviewDoc(hit, preview);
239
+ if (previewReplacesPublished(preview)) return loadPreviewDoc(hit, preview);
228
240
  const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
229
241
  if (!doc) return failFor();
230
242
  return layoutDoc(doc, false);
@@ -259,7 +271,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
259
271
  const hit = resolve(resolved[CATCH_ALL_PARAM]);
260
272
  if (!hit) return null;
261
273
  const preview = await resolvePreviewState(cms, options.preview, searchParams);
262
- if (!preview.enabled) return null;
274
+ if (!previewReplacesPublished(preview)) return null;
263
275
  return createElement(
264
276
  Fragment,
265
277
  null,