@backmatter/next 0.0.18 → 0.0.19

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
@@ -73,8 +73,8 @@ __export(exports_config, {
73
73
  withBackmatter: () => withBackmatter
74
74
  });
75
75
  module.exports = __toCommonJS(exports_config);
76
- var import_node_fs3 = require("node:fs");
77
- var import_node_path3 = __toESM(require("node:path"));
76
+ var import_node_fs4 = require("node:fs");
77
+ var import_node_path4 = __toESM(require("node:path"));
78
78
 
79
79
  // ../core/src/node/generate.ts
80
80
  var import_node_fs2 = require("node:fs");
@@ -398,16 +398,295 @@ function generate(options) {
398
398
  };
399
399
  }
400
400
 
401
+ // ../core/src/node/registry-import.ts
402
+ var import_node_fs3 = require("node:fs");
403
+ var import_node_path3 = __toESM(require("node:path"));
404
+ var REGISTRY_SPECIFIER = "backmatter";
405
+ function registryAliases() {
406
+ return REGISTRY_FILES.map((file) => {
407
+ const stem = file.slice(0, -".ts".length);
408
+ return {
409
+ specifier: stem === "index" ? REGISTRY_SPECIFIER : `${REGISTRY_SPECIFIER}/${stem}`,
410
+ file
411
+ };
412
+ });
413
+ }
414
+ function registryTurbopackAlias(appRoot, outDir = DEFAULT_OUT_DIR) {
415
+ const alias = {};
416
+ const base = import_node_path3.default.resolve(appRoot);
417
+ for (const { specifier, file } of registryAliases()) {
418
+ const abs = import_node_path3.default.resolve(appRoot, outDir, file);
419
+ alias[specifier] = asRelative(import_node_path3.default.relative(base, abs).split(import_node_path3.default.sep).join("/"));
420
+ }
421
+ return alias;
422
+ }
423
+ function registryWebpackAlias(root, outDir = DEFAULT_OUT_DIR) {
424
+ const alias = {};
425
+ for (const { specifier, file } of registryAliases()) {
426
+ alias[`${specifier}$`] = import_node_path3.default.resolve(root, outDir, file);
427
+ }
428
+ return alias;
429
+ }
430
+ function registryImportPaths(outDir = DEFAULT_OUT_DIR, baseUrl) {
431
+ const base = !baseUrl || baseUrl === "." ? "." : baseUrl;
432
+ const from = import_node_path3.default.resolve("/registry-root", base);
433
+ const to = import_node_path3.default.resolve("/registry-root", outDir);
434
+ const folder = asRelative(import_node_path3.default.relative(from, to).split(import_node_path3.default.sep).join("/"));
435
+ const paths = {};
436
+ for (const { specifier, file } of registryAliases()) {
437
+ paths[specifier] = [`${folder}/${file}`];
438
+ }
439
+ return paths;
440
+ }
441
+ function addRegistryPaths(source, outDir = DEFAULT_OUT_DIR) {
442
+ let parsed;
443
+ try {
444
+ parsed = looseParse(source);
445
+ } catch {
446
+ return source;
447
+ }
448
+ if (!isRecord(parsed))
449
+ return source;
450
+ const options = parsed.compilerOptions;
451
+ if (!isRecord(options))
452
+ return source;
453
+ const baseUrl = typeof options.baseUrl === "string" ? options.baseUrl : undefined;
454
+ const mappings = registryImportPaths(outDir, baseUrl);
455
+ const existing = isRecord(options.paths) ? options.paths : undefined;
456
+ if (options.paths !== undefined && !existing)
457
+ return source;
458
+ const missing = Object.fromEntries(Object.entries(mappings).filter(([key]) => !existing || existing[key] === undefined));
459
+ if (Object.keys(missing).length === 0)
460
+ return source;
461
+ const compiler = findKeyBrace(source, "compilerOptions", 0, source.length);
462
+ if (!compiler)
463
+ return source;
464
+ const compilerEnd = matchBrace(source, compiler.brace);
465
+ if (compilerEnd < 0)
466
+ return source;
467
+ const nl = source.includes(`\r
468
+ `) ? `\r
469
+ ` : `
470
+ `;
471
+ if (existing) {
472
+ const pathsBrace = findKeyBrace(source, "paths", compiler.brace + 1, compilerEnd);
473
+ if (!pathsBrace || pathsBrace.brace > compilerEnd)
474
+ return source;
475
+ const indent2 = innerIndent(source, pathsBrace.brace);
476
+ const entries2 = formatEntries(missing, indent2, nl);
477
+ return insertEntries(source, pathsBrace.brace, entries2, indent2, nl);
478
+ }
479
+ const indent = innerIndent(source, compiler.brace);
480
+ const child = `${indent} `;
481
+ const entries = formatEntries(missing, child, nl);
482
+ const block = `${indent}"paths": {${nl}${entries},${nl}${indent}}`;
483
+ return `${source.slice(0, compiler.brace + 1)}${nl}${block},${source.slice(compiler.brace + 1)}`;
484
+ }
485
+ function ensureRegistryTsconfig(root, outDir = DEFAULT_OUT_DIR) {
486
+ const tsconfig = import_node_path3.default.join(root, "tsconfig.json");
487
+ const jsconfig = import_node_path3.default.join(root, "jsconfig.json");
488
+ const file = import_node_fs3.existsSync(tsconfig) ? tsconfig : import_node_fs3.existsSync(jsconfig) ? jsconfig : null;
489
+ if (!file)
490
+ return null;
491
+ const before = import_node_fs3.readFileSync(file, "utf-8");
492
+ const after = addRegistryPaths(before, outDir);
493
+ if (after === before)
494
+ return null;
495
+ import_node_fs3.writeFileSync(file, after);
496
+ return `updated ${import_node_path3.default.basename(file)}`;
497
+ }
498
+ function asRelative(rel) {
499
+ if (rel.startsWith("./") || rel.startsWith("../"))
500
+ return rel;
501
+ return `./${rel}`;
502
+ }
503
+ function insertEntries(source, open, entries, indent, nl) {
504
+ let rest = source.slice(open + 1);
505
+ if (!/^\r?\n/.test(rest))
506
+ rest = `${nl}${indent}${rest.trimStart()}`;
507
+ return `${source.slice(0, open + 1)}${nl}${entries},${rest}`;
508
+ }
509
+ function formatEntries(mappings, indent, nl) {
510
+ return Object.entries(mappings).map(([key, value]) => `${indent}"${key}": [${value.map((entry) => JSON.stringify(entry)).join(", ")}]`).join(`,${nl}`);
511
+ }
512
+ function innerIndent(source, open) {
513
+ const rest = source.slice(open + 1);
514
+ const nl = rest.search(/\r?\n/);
515
+ if (nl !== -1) {
516
+ const after = rest.slice(nl).replace(/^\r?\n/, "");
517
+ const indent = after.match(/^[ \t]*/)?.[0] ?? "";
518
+ const next = after[indent.length];
519
+ if (indent.length > 0 && next && next !== "}")
520
+ return indent;
521
+ }
522
+ const lineStart = source.lastIndexOf(`
523
+ `, open) + 1;
524
+ const base = source.slice(lineStart, open).match(/^[ \t]*/)?.[0] ?? "";
525
+ return `${base} `;
526
+ }
527
+ function looseParse(source) {
528
+ const stripped = stripJsonComments(source).replace(/,\s*([}\]])/g, "$1");
529
+ return JSON.parse(stripped);
530
+ }
531
+ function stripJsonComments(input) {
532
+ let out = "";
533
+ let i = 0;
534
+ let inString = false;
535
+ let escaped = false;
536
+ while (i < input.length) {
537
+ const c = input[i];
538
+ const next = input[i + 1];
539
+ if (inString) {
540
+ out += c;
541
+ if (escaped)
542
+ escaped = false;
543
+ else if (c === "\\")
544
+ escaped = true;
545
+ else if (c === '"')
546
+ inString = false;
547
+ i++;
548
+ continue;
549
+ }
550
+ if (c === '"') {
551
+ inString = true;
552
+ out += c;
553
+ i++;
554
+ continue;
555
+ }
556
+ if (c === "/" && next === "/") {
557
+ i += 2;
558
+ while (i < input.length && input[i] !== `
559
+ `)
560
+ i++;
561
+ continue;
562
+ }
563
+ if (c === "/" && next === "*") {
564
+ i += 2;
565
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/"))
566
+ i++;
567
+ i += 2;
568
+ continue;
569
+ }
570
+ out += c;
571
+ i++;
572
+ }
573
+ return out;
574
+ }
575
+ function isRecord(value) {
576
+ return typeof value === "object" && value !== null && !Array.isArray(value);
577
+ }
578
+ function findKeyBrace(source, key, start, end) {
579
+ const needle = `"${key}"`;
580
+ let i = start;
581
+ let inString = false;
582
+ let escaped = false;
583
+ while (i < end) {
584
+ const c = source[i];
585
+ const next = source[i + 1];
586
+ if (inString) {
587
+ if (escaped)
588
+ escaped = false;
589
+ else if (c === "\\")
590
+ escaped = true;
591
+ else if (c === '"')
592
+ inString = false;
593
+ i++;
594
+ continue;
595
+ }
596
+ if (c === '"') {
597
+ if (source.startsWith(needle, i)) {
598
+ let j = i + needle.length;
599
+ while (j < end && /\s/.test(source[j]))
600
+ j++;
601
+ if (source[j] === ":") {
602
+ j++;
603
+ while (j < end && /\s/.test(source[j]))
604
+ j++;
605
+ if (source[j] === "{")
606
+ return { brace: j };
607
+ }
608
+ }
609
+ inString = true;
610
+ i++;
611
+ continue;
612
+ }
613
+ if (c === "/" && next === "/") {
614
+ i += 2;
615
+ while (i < end && source[i] !== `
616
+ `)
617
+ i++;
618
+ continue;
619
+ }
620
+ if (c === "/" && next === "*") {
621
+ i += 2;
622
+ while (i < end && !(source[i] === "*" && source[i + 1] === "/"))
623
+ i++;
624
+ i += 2;
625
+ continue;
626
+ }
627
+ i++;
628
+ }
629
+ return null;
630
+ }
631
+ function matchBrace(source, open) {
632
+ let depth = 0;
633
+ let i = open;
634
+ let inString = false;
635
+ let escaped = false;
636
+ while (i < source.length) {
637
+ const c = source[i];
638
+ const next = source[i + 1];
639
+ if (inString) {
640
+ if (escaped)
641
+ escaped = false;
642
+ else if (c === "\\")
643
+ escaped = true;
644
+ else if (c === '"')
645
+ inString = false;
646
+ i++;
647
+ continue;
648
+ }
649
+ if (c === '"') {
650
+ inString = true;
651
+ i++;
652
+ continue;
653
+ }
654
+ if (c === "/" && next === "/") {
655
+ i += 2;
656
+ while (i < source.length && source[i] !== `
657
+ `)
658
+ i++;
659
+ continue;
660
+ }
661
+ if (c === "/" && next === "*") {
662
+ i += 2;
663
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/"))
664
+ i++;
665
+ i += 2;
666
+ continue;
667
+ }
668
+ if (c === "{")
669
+ depth++;
670
+ else if (c === "}") {
671
+ depth--;
672
+ if (depth === 0)
673
+ return i;
674
+ }
675
+ i++;
676
+ }
677
+ return -1;
678
+ }
679
+
401
680
  // src/config.ts
402
681
  var LOADER = "@backmatter/next/loader";
403
682
  var SDK = "@backmatter/next";
404
683
  function warnMissingInstrumentation(root) {
405
684
  if (detectSdk(root) !== "@backmatter/next")
406
685
  return;
407
- const hasRoot = import_node_fs3.existsSync(import_node_path3.default.join(root, "instrumentation.ts"));
408
- const hasSrc = import_node_fs3.existsSync(import_node_path3.default.join(root, "src", "instrumentation.ts"));
686
+ const hasRoot = import_node_fs4.existsSync(import_node_path4.default.join(root, "instrumentation.ts"));
687
+ const hasSrc = import_node_fs4.existsSync(import_node_path4.default.join(root, "src", "instrumentation.ts"));
409
688
  if (!hasRoot && !hasSrc) {
410
- console.warn("backmatter: add instrumentation.ts that imports .backmatter/sync — block schema sync will not run without it");
689
+ console.warn("backmatter: add instrumentation.ts that imports backmatter/sync — block schema sync will not run without it");
411
690
  }
412
691
  }
413
692
  function defaultLog(result, root) {
@@ -423,7 +702,7 @@ function defaultLog(result, root) {
423
702
  }
424
703
  function resolveLoaderOptions(options = {}) {
425
704
  return {
426
- root: import_node_path3.default.resolve(options.root ?? process.cwd()),
705
+ root: import_node_path4.default.resolve(options.root ?? process.cwd()),
427
706
  outDir: options.outDir ?? DEFAULT_OUT_DIR,
428
707
  modelsDir: options.modelsDir ?? DEFAULT_DIRS.models,
429
708
  blocksDir: options.blocksDir ?? DEFAULT_DIRS.blocks,
@@ -442,7 +721,7 @@ function generateRegistry(options = {}) {
442
721
  return result;
443
722
  }
444
723
  function registryMatchers(loaderOptions) {
445
- const rel = import_node_path3.default.relative(loaderOptions.root, import_node_path3.default.resolve(loaderOptions.root, loaderOptions.outDir)).split(import_node_path3.default.sep).join("/");
724
+ const rel = import_node_path4.default.relative(loaderOptions.root, import_node_path4.default.resolve(loaderOptions.root, loaderOptions.outDir)).split(import_node_path4.default.sep).join("/");
446
725
  const escaped = rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
447
726
  return {
448
727
  glob: `**/${rel}/*.ts`,
@@ -457,17 +736,30 @@ function withBackmatter(nextConfig = {}, options = {}) {
457
736
  const cfg = nextConfig;
458
737
  const existingTurbo = cfg.turbopack ?? {};
459
738
  const existingRules = existingTurbo.rules ?? {};
739
+ const existingAlias = existingTurbo.resolveAlias ?? {};
460
740
  const turbopackRule = { loaders: [use] };
461
741
  const turbopack = {
462
742
  ...existingTurbo,
743
+ resolveAlias: {
744
+ ...registryTurbopackAlias(loaderOptions.root, loaderOptions.outDir),
745
+ ...existingAlias
746
+ },
463
747
  rules: { ...existingRules, [glob]: turbopackRule }
464
748
  };
465
749
  const webpack = (config, context) => {
466
750
  config.module ??= {};
467
751
  config.module.rules ??= [];
468
752
  config.module.rules.push({ test, enforce: "pre", use: [use] });
753
+ config.resolve ??= {};
754
+ const prevAlias = config.resolve.alias;
755
+ const prev = prevAlias && typeof prevAlias === "object" && !Array.isArray(prevAlias) ? prevAlias : {};
756
+ config.resolve.alias = {
757
+ ...registryWebpackAlias(loaderOptions.root, loaderOptions.outDir),
758
+ ...prev
759
+ };
469
760
  return cfg.webpack ? cfg.webpack(config, context) : config;
470
761
  };
762
+ ensureRegistryTsconfig(loaderOptions.root, loaderOptions.outDir);
471
763
  const cacheComponents = cfg.cacheComponents ?? true;
472
764
  const transpilePackages = [
473
765
  ...new Set([...cfg.transpilePackages ?? [], "@backmatter/next", "@backmatter/core"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/next",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Experimental Next.js adapter for Backmatter. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/cached-doc.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // packages/next/src/cached-doc.ts
2
+ import type { ReactNode } from "react";
2
3
  import { contentCacheTags, listCacheTags, type BackmatterDoc, type BackmatterDocSummary } from "@backmatter/core";
3
4
 
4
5
  export type CacheFns = {
@@ -17,8 +18,15 @@ export type ListDocsFn = (filter?: {
17
18
  locale?: string;
18
19
  }) => Promise<BackmatterDocSummary[]>;
19
20
 
21
+ export type PublishedRender = (
22
+ locale: string,
23
+ type: string,
24
+ slug: string,
25
+ ) => Promise<ReactNode>;
26
+
20
27
  let registeredGetDoc: GetDocFn | null = null;
21
28
  let registeredListDocs: ListDocsFn | null = null;
29
+ let registeredPublishedRender: PublishedRender | null = null;
22
30
 
23
31
  /** Called from `createBackmatter` so `"use cache"` only closes over serializable args. */
24
32
  export function registerCachedDocGet(getDoc: GetDocFn): void {
@@ -29,6 +37,11 @@ export function registerCachedListDocs(listDocs: ListDocsFn): void {
29
37
  registeredListDocs = listDocs;
30
38
  }
31
39
 
40
+ /** The catch-all registers the MDX/block render. `"use cache"` only receives ids. */
41
+ export function registerPublishedRender(render: PublishedRender): void {
42
+ registeredPublishedRender = render;
43
+ }
44
+
32
45
  async function loadCacheFns(): Promise<CacheFns> {
33
46
  try {
34
47
  const mod = (await import("next/cache" as string)) as CacheFns;
@@ -80,6 +93,27 @@ export async function getCachedDocs(
80
93
  return listDocs({ type, locale });
81
94
  }
82
95
 
96
+ /**
97
+ * Finished published page (MDX body or composer blocks, plus layout).
98
+ * The cache entry is what a static prerender and a normal link prefetch store.
99
+ */
100
+ export async function getCachedPublished(
101
+ locale: string,
102
+ type: string,
103
+ slug: string,
104
+ cache?: CacheFns,
105
+ ): Promise<ReactNode> {
106
+ "use cache";
107
+ const fns = cache ?? (await loadCacheFns());
108
+ fns.cacheLife("hours");
109
+ fns.cacheTag(...contentCacheTags(locale, type, slug));
110
+ const render = registeredPublishedRender;
111
+ if (!render) {
112
+ throw new Error("[backmatter] getCachedPublished called before a published render was registered");
113
+ }
114
+ return render(locale, type, slug);
115
+ }
116
+
83
117
  /** Unwrap Next `params` inside `"use cache"` so Cache Components can prerender. */
84
118
  export async function cachedRouteParams<T extends object>(
85
119
  params: Promise<T> | T,
package/src/config.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // packages/next/src/config.ts
2
2
  // `withBackmatter(nextConfig)` for next.config.{js,mjs,ts}.
3
3
  //
4
- // 1. Generates `.backmatter/` once when the config loads, so the first compile can
5
- // resolve `import { cms } from ".backmatter"`.
4
+ // 1. Generates `.backmatter/` once when the config loads, and aliases it to the
5
+ // `backmatter` import (`import { cms } from "backmatter"`).
6
6
  // 2. Registers `@backmatter/next/loader` for the registry files with Turbopack
7
7
  // (Next 16 default) and webpack (`next build --webpack`). The loader declares
8
8
  // `models/`, `blocks/`, `richtext/`, and `mdx/` as context dependencies, so the bundler's
@@ -16,6 +16,11 @@ import {
16
16
  generate,
17
17
  type GenerateResult,
18
18
  } from "@backmatter/core/generate";
19
+ import {
20
+ ensureRegistryTsconfig,
21
+ registryTurbopackAlias,
22
+ registryWebpackAlias,
23
+ } from "@backmatter/core/node/registry-import";
19
24
  import type { BackmatterLoaderOptions } from "./loader";
20
25
 
21
26
  export type WithBackmatterOptions = {
@@ -52,6 +57,7 @@ export type NextConfigLike = {
52
57
 
53
58
  export type WebpackConfigLike = {
54
59
  module?: { rules?: unknown[] } & Record<string, unknown>;
60
+ resolve?: { alias?: Record<string, unknown> } & Record<string, unknown>;
55
61
  } & Record<string, unknown>;
56
62
 
57
63
  export function warnMissingInstrumentation(root: string): void {
@@ -60,7 +66,7 @@ export function warnMissingInstrumentation(root: string): void {
60
66
  const hasSrc = existsSync(path.join(root, "src", "instrumentation.ts"));
61
67
  if (!hasRoot && !hasSrc) {
62
68
  console.warn(
63
- "backmatter: add instrumentation.ts that imports .backmatter/sync — block schema sync will not run without it",
69
+ "backmatter: add instrumentation.ts that imports backmatter/sync — block schema sync will not run without it",
64
70
  );
65
71
  }
66
72
  }
@@ -141,9 +147,14 @@ export function withBackmatter<T extends object>(
141
147
  const cfg = nextConfig as T & NextConfigLike;
142
148
  const existingTurbo = (cfg.turbopack ?? {}) as Record<string, unknown>;
143
149
  const existingRules = (existingTurbo.rules ?? {}) as Record<string, unknown>;
150
+ const existingAlias = (existingTurbo.resolveAlias ?? {}) as Record<string, unknown>;
144
151
  const turbopackRule: TurbopackRule = { loaders: [use] };
145
152
  const turbopack = {
146
153
  ...existingTurbo,
154
+ resolveAlias: {
155
+ ...registryTurbopackAlias(loaderOptions.root, loaderOptions.outDir),
156
+ ...existingAlias,
157
+ },
147
158
  rules: { ...existingRules, [glob]: turbopackRule },
148
159
  };
149
160
 
@@ -152,17 +163,30 @@ export function withBackmatter<T extends object>(
152
163
  config.module.rules ??= [];
153
164
  // `enforce: "pre"` runs before Next's SWC loader; we return TypeScript, SWC compiles it.
154
165
  config.module.rules.push({ test, enforce: "pre", use: [use] });
166
+ config.resolve ??= {};
167
+ const prevAlias = config.resolve.alias;
168
+ const prev =
169
+ prevAlias && typeof prevAlias === "object" && !Array.isArray(prevAlias) ? prevAlias : {};
170
+ config.resolve.alias = {
171
+ ...registryWebpackAlias(loaderOptions.root, loaderOptions.outDir),
172
+ ...prev,
173
+ };
155
174
  return cfg.webpack ? cfg.webpack(config, context) : config;
156
175
  };
157
176
 
177
+ ensureRegistryTsconfig(loaderOptions.root, loaderOptions.outDir);
178
+
158
179
  const cacheComponents = cfg.cacheComponents ?? true;
180
+ // Published pages are static files. Partial prefetching would replace those
181
+ // files with one shared shell for every CMS URL, so a normal link would
182
+ // prefetch the shell instead of the finished page. Hosts can still opt in.
159
183
  const transpilePackages = [
160
184
  ...new Set([...(cfg.transpilePackages ?? []), "@backmatter/next", "@backmatter/core"]),
161
185
  ];
162
186
  return {
163
187
  ...nextConfig,
164
188
  cacheComponents,
165
- partialPrefetching: cfg.partialPrefetching ?? cacheComponents,
189
+ partialPrefetching: cfg.partialPrefetching ?? false,
166
190
  transpilePackages,
167
191
  turbopack,
168
192
  webpack,
@@ -10,6 +10,25 @@ import remarkGfm from "remark-gfm";
10
10
 
11
11
  export type MdxComponents = Record<string, ComponentType<any>>;
12
12
 
13
+ function UnknownMdx(props: { children?: ReactNode }) {
14
+ return props.children ?? null;
15
+ }
16
+
17
+ /**
18
+ * MDX copies `components` with object spread, which skips a Proxy.
19
+ * Fill each capitalized tag the host did not provide so one unknown tag
20
+ * does not fail the page render.
21
+ */
22
+ function mdxComponentsWithFallback(source: string, components?: MdxComponents): MdxComponents {
23
+ const provided: MdxComponents = { ...components };
24
+ for (const match of source.matchAll(/<([A-Z][A-Za-z0-9]*)\b/g)) {
25
+ const name = match[1];
26
+ if (!name || provided[name]) continue;
27
+ provided[name] = UnknownMdx;
28
+ }
29
+ return provided;
30
+ }
31
+
13
32
  export async function compileMdx(
14
33
  source: string,
15
34
  components?: MdxComponents,
@@ -22,7 +41,7 @@ export async function compileMdx(
22
41
  remarkPlugins: [remarkGfm],
23
42
  baseUrl: import.meta.url,
24
43
  });
25
- return createElement(Content, { components });
44
+ return createElement(Content, { components: mdxComponentsWithFallback(source, components) });
26
45
  } catch (err) {
27
46
  const message = err instanceof Error ? err.message : "MDX compile failed";
28
47
  return createElement("pre", { role: "alert" }, message);
package/src/routes.ts CHANGED
@@ -8,7 +8,7 @@ import { isRoutable, matchRoute, modelFormat, PREVIEW_CHECKOUT_QUERY, PREVIEW_CO
8
8
  import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
9
9
  import { fetchPreviewDoc } from "@backmatter/core/preview";
10
10
  import { renderDoc, type MdxComponents } from "./mdx-compile";
11
- import { cachedRouteParams } from "./cached-doc";
11
+ import { cachedRouteParams, getCachedPublished, registerPublishedRender } from "./cached-doc";
12
12
  import { renderPage, type BlockComponents, type BlockFallbacks } from "./page-render";
13
13
  import { previewUnavailable } from "./preview-ui";
14
14
 
@@ -229,15 +229,30 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
229
229
  }
230
230
  }
231
231
 
232
+ async function renderPublished(locale: string, type: string, slug: string): Promise<ReactNode> {
233
+ const doc = await cms.getDoc(locale, type, slug);
234
+ if (!doc) return null;
235
+ return createElement(
236
+ "div",
237
+ { "data-backmatter-published": "", style: { display: "contents" } },
238
+ await layoutDoc(doc, false),
239
+ );
240
+ }
241
+
242
+ async function publishedBody(locale: string, type: string, slug: string): Promise<ReactNode> {
243
+ registerPublishedRender(renderPublished);
244
+ const body = await getCachedPublished(locale, type, slug);
245
+ if (body == null) return failFor();
246
+ return body;
247
+ }
248
+
232
249
  const CmsRoute: CatchAllRoute["CmsRoute"] = async ({ params, searchParams }) => {
233
250
  const resolved = await cachedRouteParams(params);
234
251
  const hit = resolve(resolved[CATCH_ALL_PARAM]);
235
252
  if (!hit) return failFor();
236
253
  const preview = await resolvePreviewState(cms, options.preview, searchParams);
237
254
  if (previewReplacesPublished(preview)) return loadPreviewDoc(hit, preview);
238
- const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
239
- if (!doc) return failFor();
240
- return layoutDoc(doc, false);
255
+ return publishedBody(hit.locale, hit.type, hit.slug);
241
256
  };
242
257
 
243
258
  const PublishedRoute = async ({
@@ -249,13 +264,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
249
264
  const resolved = await params;
250
265
  const hit = resolve(resolved[CATCH_ALL_PARAM]);
251
266
  if (!hit) return failFor();
252
- const doc = await cms.getDoc(hit.locale, hit.type, hit.slug);
253
- if (!doc) return failFor();
254
- return createElement(
255
- "div",
256
- { "data-backmatter-published": "", style: { display: "contents" } },
257
- await layoutDoc(doc, false),
258
- );
267
+ return publishedBody(hit.locale, hit.type, hit.slug);
259
268
  };
260
269
 
261
270
  const PreviewBranch = async ({
@@ -286,11 +295,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
286
295
  createElement(
287
296
  Fragment,
288
297
  null,
289
- createElement(
290
- Suspense,
291
- { fallback: createElement("div", { "aria-busy": "true", style: { minHeight: "100vh" } }) },
292
- createElement(PublishedRoute, props),
293
- ),
298
+ createElement(PublishedRoute, props),
294
299
  createElement(Suspense, { fallback: null }, createElement(PreviewBranch, props)),
295
300
  );
296
301