@docubook/flame 2.0.0-alpha.2 → 2.0.0-beta.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.
@@ -1101,15 +1101,12 @@ var frontmatterSchema = z.object({
1101
1101
  description: z.coerce.string().optional(),
1102
1102
  image: z.coerce.string().optional(),
1103
1103
  date: z.coerce.string().optional()
1104
- });
1104
+ }).passthrough();
1105
1105
  function frontmatterField(frontmatter, key) {
1106
1106
  return typeof frontmatter[key] === "string" ? frontmatter[key] : "";
1107
1107
  }
1108
- async function serializeWithDocPlugins(rawMdx, opts = {}) {
1109
- const { strippedContent } = extractFrontmatterWithContent(
1110
- rawMdx,
1111
- opts.frontmatterSchema
1112
- );
1108
+ async function serializeWithDocPlugins(rawMdx, opts = {}, pre) {
1109
+ const { strippedContent, frontmatter } = pre ?? extractFrontmatterWithContent(rawMdx, opts.frontmatterSchema);
1113
1110
  const defaultRemark = createDefaultRemarkPlugins();
1114
1111
  const defaultRehype = createDefaultRehypePlugins();
1115
1112
  const finalRemark = [...defaultRemark, remarkMdxJsxDocsHtmlLinks, ...opts.remarkPlugins ?? []];
@@ -1121,16 +1118,16 @@ async function serializeWithDocPlugins(rawMdx, opts = {}) {
1121
1118
  rehypePlugins: finalRehype,
1122
1119
  remarkPlugins: finalRemark
1123
1120
  }
1124
- });
1121
+ }).then((serialized) => ({ ...serialized, frontmatter, strippedContent }));
1125
1122
  }
1126
- async function compileMdx(rawMdx, filePath, gitDates, remarkPlugins, rehypePlugins, frontmatterSchema2) {
1123
+ async function compileMdx(rawMdx, filePath, gitDates, remarkPlugins, rehypePlugins, frontmatterSchema2, pre) {
1127
1124
  const tocs = extractTocsFromRawMdx(rawMdx);
1128
- const { frontmatter } = extractFrontmatterWithContent(rawMdx, frontmatterSchema2);
1129
- const serialized = await serializeWithDocPlugins(rawMdx, {
1130
- remarkPlugins,
1131
- rehypePlugins,
1132
- frontmatterSchema: frontmatterSchema2
1133
- });
1125
+ const frontmatter = pre?.frontmatter ?? extractFrontmatterWithContent(rawMdx, frontmatterSchema2).frontmatter;
1126
+ const serialized = await serializeWithDocPlugins(
1127
+ rawMdx,
1128
+ { remarkPlugins, rehypePlugins, frontmatterSchema: frontmatterSchema2 },
1129
+ pre
1130
+ );
1134
1131
  const components = createMdxComponents();
1135
1132
  const content = React.createElement(MDXRemote, {
1136
1133
  compiledSource: serialized.compiledSource,
@@ -1146,12 +1143,37 @@ async function compileMdx(rawMdx, filePath, gitDates, remarkPlugins, rehypePlugi
1146
1143
  tocs
1147
1144
  };
1148
1145
  }
1149
- async function compileMdxModule(rawMdx, remarkPlugins, rehypePlugins) {
1146
+ var pageFrontmatter = /* @__PURE__ */ new Map();
1147
+ function registerPageFrontmatter(href, frontmatter) {
1148
+ pageFrontmatter.set(href, frontmatter);
1149
+ }
1150
+ function getPageFrontmatter(href) {
1151
+ return pageFrontmatter.get(href);
1152
+ }
1153
+ var pageStripped = /* @__PURE__ */ new Map();
1154
+ function registerPageStripped(href, stripped) {
1155
+ pageStripped.set(href, stripped);
1156
+ }
1157
+ function getPageStripped(href) {
1158
+ return pageStripped.get(href);
1159
+ }
1160
+ var pageContent = /* @__PURE__ */ new Map();
1161
+ function registerPageContent(href, raw) {
1162
+ pageContent.set(href, raw);
1163
+ }
1164
+ function getPageContent(href) {
1165
+ return pageContent.get(href);
1166
+ }
1167
+ async function compileMdxModule(rawMdx, remarkPlugins, rehypePlugins, href) {
1150
1168
  const serialized = await serializeWithDocPlugins(rawMdx, {
1151
1169
  outputFormat: "program",
1152
1170
  remarkPlugins,
1153
1171
  rehypePlugins
1154
1172
  });
1173
+ if (href) {
1174
+ registerPageFrontmatter(href, serialized.frontmatter);
1175
+ registerPageStripped(href, serialized.strippedContent);
1176
+ }
1155
1177
  return serialized.compiledSource;
1156
1178
  }
1157
1179
 
@@ -1278,7 +1300,7 @@ async function generateSearchIndex(docsDir, outputDir) {
1278
1300
  const mdxFiles = await scanMdxFiles(docs);
1279
1301
  const results = await Promise.all(
1280
1302
  mdxFiles.map(async (file) => {
1281
- const raw = await readFile2(file.absPath, "utf-8");
1303
+ const raw = getPageContent(`/${file.path}`) ?? await readFile2(file.absPath, "utf-8");
1282
1304
  return extractRecords(file.path, raw);
1283
1305
  })
1284
1306
  );
@@ -1364,25 +1386,23 @@ function getRouteMap() {
1364
1386
  routes.forEach((route) => traverse(route));
1365
1387
  return map;
1366
1388
  }
1367
- var descriptionCache = /* @__PURE__ */ new Map();
1368
- function readDescription(href) {
1369
- const cached = descriptionCache.get(href);
1370
- if (cached !== void 0) return cached;
1371
- let description = "";
1389
+ function readPageFrontmatter(href) {
1390
+ const registered = getPageFrontmatter(href);
1391
+ if (registered) return registered;
1392
+ let fm = {};
1372
1393
  const rel = href.replace(/^\/|$/g, "");
1373
1394
  for (const ext of [".mdx", ".md"]) {
1374
1395
  for (const file of [join4(DOCS_DIR, `${rel}${ext}`), join4(DOCS_DIR, `${rel}/index${ext}`)]) {
1375
1396
  try {
1376
- const fm = extractFrontmatter(readFileSync2(file, "utf-8"));
1377
- description = typeof fm.description === "string" ? fm.description : "";
1378
- if (description) break;
1397
+ fm = extractFrontmatter(readFileSync2(file, "utf-8"));
1398
+ break;
1379
1399
  } catch {
1380
1400
  }
1381
1401
  }
1382
- if (description) break;
1402
+ if (Object.keys(fm).length) break;
1383
1403
  }
1384
- descriptionCache.set(href, description);
1385
- return description;
1404
+ registerPageFrontmatter(href, fm);
1405
+ return fm;
1386
1406
  }
1387
1407
  function getPreviousNext(pathname) {
1388
1408
  const normalizedPath = pathname.replace(/^\/|$/g, "");
@@ -1391,12 +1411,13 @@ function getPreviousNext(pathname) {
1391
1411
  const routeMap2 = getRouteMap();
1392
1412
  const first = paths2[0];
1393
1413
  if (!first) return { prev: null, next: null };
1414
+ const fm = readPageFrontmatter(first);
1394
1415
  return {
1395
1416
  prev: null,
1396
1417
  next: {
1397
1418
  href: first,
1398
- title: routeMap2.get(first) || "",
1399
- description: readDescription(first)
1419
+ title: fm.title || routeMap2.get(first) || "",
1420
+ description: fm.description || ""
1400
1421
  }
1401
1422
  };
1402
1423
  }
@@ -1408,12 +1429,14 @@ function getPreviousNext(pathname) {
1408
1429
  const routeMap = getRouteMap();
1409
1430
  const prevHref = index > 0 ? paths[index - 1] : null;
1410
1431
  const nextHref = index < paths.length - 1 ? paths[index + 1] : null;
1432
+ const prevFm = prevHref ? readPageFrontmatter(prevHref) : null;
1433
+ const nextFm = nextHref ? readPageFrontmatter(nextHref) : null;
1411
1434
  return {
1412
- prev: prevHref ? { href: prevHref, title: routeMap.get(prevHref) || "" } : null,
1435
+ prev: prevHref ? { href: prevHref, title: prevFm?.title || routeMap.get(prevHref) || "" } : null,
1413
1436
  next: nextHref ? {
1414
1437
  href: nextHref,
1415
- title: routeMap.get(nextHref) || "",
1416
- description: readDescription(nextHref)
1438
+ title: nextFm?.title || routeMap.get(nextHref) || "",
1439
+ description: nextFm?.description || ""
1417
1440
  } : null
1418
1441
  };
1419
1442
  }
@@ -1642,7 +1665,7 @@ import { ListIcon } from "lucide-react";
1642
1665
  import { ArrowUpIcon } from "lucide-react";
1643
1666
  import { useEffect, useState, useCallback } from "react";
1644
1667
  import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1645
- function ScrollTo({ className, showIcon = true }) {
1668
+ function ScrollTo({ className, showIcon = true, onScrollToTop }) {
1646
1669
  const [isVisible, setIsVisible] = useState(false);
1647
1670
  const checkScroll = useCallback(() => {
1648
1671
  const container = document.getElementById("scroll-container");
@@ -1667,15 +1690,20 @@ function ScrollTo({ className, showIcon = true }) {
1667
1690
  if (timeoutId) clearTimeout(timeoutId);
1668
1691
  };
1669
1692
  }, [checkScroll]);
1670
- const scrollToTop = useCallback((e) => {
1671
- e.preventDefault();
1672
- const container = document.getElementById("scroll-container");
1673
- if (container) {
1674
- container.scrollTo({ top: 0, behavior: "smooth" });
1675
- } else {
1676
- window.scrollTo({ top: 0, behavior: "smooth" });
1677
- }
1678
- }, []);
1693
+ const scrollToTop = useCallback(
1694
+ (e) => {
1695
+ e.preventDefault();
1696
+ onScrollToTop?.();
1697
+ history.replaceState(null, "", "#top");
1698
+ const container = document.getElementById("scroll-container");
1699
+ if (container) {
1700
+ container.scrollTo({ top: 0, behavior: "smooth" });
1701
+ } else {
1702
+ window.scrollTo({ top: 0, behavior: "smooth" });
1703
+ }
1704
+ },
1705
+ [onScrollToTop]
1706
+ );
1679
1707
  return /* @__PURE__ */ jsx7(
1680
1708
  "div",
1681
1709
  {
@@ -1688,7 +1716,7 @@ function ScrollTo({ className, showIcon = true }) {
1688
1716
  children: /* @__PURE__ */ jsxs4(
1689
1717
  "a",
1690
1718
  {
1691
- href: "#",
1719
+ href: "#top",
1692
1720
  onClick: scrollToTop,
1693
1721
  className: cn(
1694
1722
  "inline-flex items-center text-sm",
@@ -1743,9 +1771,9 @@ function Toc({ tocs }) {
1743
1771
  break;
1744
1772
  }
1745
1773
  }
1746
- if (currentId && currentId !== activeIdRef.current) {
1774
+ if (currentId !== activeIdRef.current) {
1747
1775
  setActiveId(currentId);
1748
- history.replaceState(null, "", `#${currentId}`);
1776
+ history.replaceState(null, "", currentId ? `#${currentId}` : "#top");
1749
1777
  }
1750
1778
  };
1751
1779
  handleScroll();
@@ -1772,100 +1800,124 @@ function Toc({ tocs }) {
1772
1800
  clickedIdRef.current = null;
1773
1801
  }, 1e3);
1774
1802
  }, []);
1803
+ const handleScrollToTop = useCallback2(() => {
1804
+ clickedIdRef.current = "__top__";
1805
+ setActiveId(null);
1806
+ history.replaceState(null, "", "#top");
1807
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1808
+ clickTimerRef.current = setTimeout(() => {
1809
+ clickedIdRef.current = null;
1810
+ }, 1e3);
1811
+ }, []);
1775
1812
  useEffect2(() => {
1776
1813
  return () => {
1777
1814
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1778
1815
  };
1779
1816
  }, []);
1817
+ const activeItemRef = useRef(null);
1818
+ useEffect2(() => {
1819
+ if (activeId && activeItemRef.current && !clickedIdRef.current) {
1820
+ activeItemRef.current.scrollIntoView({
1821
+ block: "nearest",
1822
+ behavior: "smooth"
1823
+ });
1824
+ }
1825
+ }, [activeId]);
1780
1826
  if (!tocs.length) return null;
1781
- return /* @__PURE__ */ jsxs5("div", { className: "flex w-full flex-col gap-2", children: [
1782
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
1827
+ return /* @__PURE__ */ jsxs5("div", { className: "flex h-full min-h-0 w-full flex-col gap-2", children: [
1828
+ /* @__PURE__ */ jsxs5("div", { className: "flex shrink-0 items-center gap-2", children: [
1783
1829
  /* @__PURE__ */ jsx8(ListIcon, { className: "h-4 w-4" }),
1784
1830
  /* @__PURE__ */ jsx8("h3", { className: "text-sm font-medium", children: "On this page" })
1785
1831
  ] }),
1786
- /* @__PURE__ */ jsx8("div", { className: "relative", children: /* @__PURE__ */ jsxs5("div", { className: "relative text-sm", children: [
1787
- /* @__PURE__ */ jsx8("div", { className: "bg-base-300 absolute top-0 left-0 h-full w-px" }),
1788
- /* @__PURE__ */ jsx8("div", { className: "flex flex-col", children: tocs.map(({ href, level, text }) => {
1789
- const id = href.slice(1);
1790
- const isActive = activeId === id;
1791
- const levelPadding = (level - 2) * 16;
1792
- return /* @__PURE__ */ jsxs5(
1793
- "div",
1794
- {
1795
- className: cn(
1796
- "relative flex items-center transition-all duration-200",
1797
- isActive && "bg-primary/5"
1798
- ),
1799
- children: [
1800
- /* @__PURE__ */ jsxs5(
1801
- "div",
1802
- {
1803
- className: cn(
1804
- "flex shrink-0 items-center px-1 py-2 transition-all duration-200",
1805
- isActive && "border-primary -ml-px border-l-[3px]"
1806
- ),
1807
- children: [
1808
- /* @__PURE__ */ jsx8(
1809
- "div",
1810
- {
1811
- className: cn(
1812
- "h-px transition-colors duration-200",
1813
- isActive ? "bg-primary w-3" : "bg-base-300 w-2"
1814
- )
1815
- }
1816
- ),
1817
- /* @__PURE__ */ jsx8(
1818
- "div",
1819
- {
1820
- className: cn(
1821
- "h-1.5 w-1.5 shrink-0 rounded-full transition-colors duration-300",
1822
- isActive ? "bg-primary" : "bg-base-300"
1823
- )
1824
- }
1825
- )
1826
- ]
1827
- }
1832
+ /* @__PURE__ */ jsxs5("div", { className: "relative min-h-0 flex-1 overflow-y-auto overscroll-contain pr-1", children: [
1833
+ /* @__PURE__ */ jsxs5("div", { className: "relative text-sm", children: [
1834
+ /* @__PURE__ */ jsx8("div", { className: "bg-base-300 absolute top-0 left-0 h-full w-px" }),
1835
+ /* @__PURE__ */ jsx8("div", { className: "flex flex-col", children: tocs.map(({ href, level, text }) => {
1836
+ const id = href.slice(1);
1837
+ const isActive = activeId === id;
1838
+ const levelPadding = (level - 2) * 16;
1839
+ return /* @__PURE__ */ jsxs5(
1840
+ "div",
1841
+ {
1842
+ ref: isActive ? activeItemRef : void 0,
1843
+ className: cn(
1844
+ "relative flex items-center transition-all duration-200",
1845
+ isActive && "bg-primary/5"
1828
1846
  ),
1829
- /* @__PURE__ */ jsx8(
1830
- "a",
1831
- {
1832
- href,
1833
- onClick: (e) => {
1834
- e.preventDefault();
1835
- handleLinkClick(id);
1836
- const el = document.getElementById(id);
1837
- if (el) {
1838
- const offset = getAnchorOffset();
1839
- const scroller = window.innerWidth >= 1024 ? document.getElementById("scroll-container") : null;
1840
- const elTop = el.getBoundingClientRect().top;
1841
- if (scroller) {
1842
- scroller.scrollTo({
1843
- top: elTop - scroller.getBoundingClientRect().top + scroller.scrollTop - offset,
1844
- behavior: "smooth"
1845
- });
1846
- } else {
1847
- window.scrollTo({
1848
- top: elTop + window.scrollY - offset,
1849
- behavior: "smooth"
1850
- });
1847
+ children: [
1848
+ /* @__PURE__ */ jsxs5(
1849
+ "div",
1850
+ {
1851
+ className: cn(
1852
+ "flex shrink-0 items-center px-1 py-2 transition-all duration-200",
1853
+ isActive && "border-primary -ml-px border-l-[3px]"
1854
+ ),
1855
+ children: [
1856
+ /* @__PURE__ */ jsx8(
1857
+ "div",
1858
+ {
1859
+ className: cn(
1860
+ "h-px transition-colors duration-200",
1861
+ isActive ? "bg-primary w-3" : "bg-base-300 w-2"
1862
+ )
1863
+ }
1864
+ ),
1865
+ /* @__PURE__ */ jsx8(
1866
+ "div",
1867
+ {
1868
+ className: cn(
1869
+ "h-1.5 w-1.5 shrink-0 rounded-full transition-colors duration-300",
1870
+ isActive ? "bg-primary" : "bg-base-300"
1871
+ )
1872
+ }
1873
+ )
1874
+ ]
1875
+ }
1876
+ ),
1877
+ /* @__PURE__ */ jsx8(
1878
+ "a",
1879
+ {
1880
+ href,
1881
+ onClick: (e) => {
1882
+ e.preventDefault();
1883
+ handleLinkClick(id);
1884
+ const el = document.getElementById(id);
1885
+ if (el) {
1886
+ const offset = getAnchorOffset();
1887
+ const scroller = window.innerWidth >= 1024 ? document.getElementById("scroll-container") : null;
1888
+ const elTop = el.getBoundingClientRect().top;
1889
+ if (scroller) {
1890
+ scroller.scrollTo({
1891
+ top: elTop - scroller.getBoundingClientRect().top + scroller.scrollTop - offset,
1892
+ behavior: "smooth"
1893
+ });
1894
+ } else {
1895
+ window.scrollTo({
1896
+ top: elTop + window.scrollY - offset,
1897
+ behavior: "smooth"
1898
+ });
1899
+ }
1851
1900
  }
1852
- }
1853
- },
1854
- className: cn(
1855
- "flex flex-1 items-center py-2 transition-all duration-200",
1856
- isActive ? "text-primary font-medium" : "text-base-content/60 hover:text-base-content"
1857
- ),
1858
- style: { paddingLeft: `${levelPadding + 6}px` },
1859
- children: /* @__PURE__ */ jsx8("span", { className: "line-clamp-2 text-sm break-words", children: text })
1860
- }
1861
- )
1862
- ]
1863
- },
1864
- href
1865
- );
1866
- }) })
1867
- ] }) }),
1868
- /* @__PURE__ */ jsx8(ScrollTo, { className: "mt-2" })
1901
+ },
1902
+ className: cn(
1903
+ "flex flex-1 items-center py-2 transition-all duration-200",
1904
+ isActive ? "text-primary font-medium" : "text-base-content/60 hover:text-base-content"
1905
+ ),
1906
+ style: { paddingLeft: `${levelPadding + 6}px` },
1907
+ children: /* @__PURE__ */ jsx8("span", { className: "line-clamp-2 text-sm break-words", children: text })
1908
+ }
1909
+ )
1910
+ ]
1911
+ },
1912
+ href
1913
+ );
1914
+ }) })
1915
+ ] }),
1916
+ /* @__PURE__ */ jsxs5("div", { className: "mt-2 grid grid-cols-[28px_minmax(0,1fr)] items-start px-4 text-sm", children: [
1917
+ /* @__PURE__ */ jsx8("div", { "aria-hidden": "true" }),
1918
+ /* @__PURE__ */ jsx8(ScrollTo, { className: "mt-0", onScrollToTop: handleScrollToTop })
1919
+ ] })
1920
+ ] })
1869
1921
  ] });
1870
1922
  }
1871
1923
 
@@ -2650,6 +2702,10 @@ export {
2650
2702
  getGitLastModifiedBatch,
2651
2703
  frontmatterField,
2652
2704
  compileMdx,
2705
+ getPageFrontmatter,
2706
+ getPageStripped,
2707
+ registerPageContent,
2708
+ getPageContent,
2653
2709
  compileMdxModule,
2654
2710
  generateSearchIndex,
2655
2711
  initSentry,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-IN2QAGDZ.js";
3
+ } from "./chunk-6EOUQCRM.js";
4
4
  import "./chunk-4IQXHHPF.js";
5
5
 
6
6
  // .docu/node/clean.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runDeploy
3
- } from "./chunk-PJVCY4FK.js";
3
+ } from "./chunk-U7M5SK76.js";
4
4
  import "./chunk-4IQXHHPF.js";
5
5
 
6
6
  // .docu/node/deploy.deno.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runDeploy
3
- } from "./chunk-PJVCY4FK.js";
3
+ } from "./chunk-U7M5SK76.js";
4
4
  import "./chunk-4IQXHHPF.js";
5
5
 
6
6
  // .docu/node/deploy.node.ts
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  runPreview
3
- } from "./chunk-PGIHPW5L.js";
3
+ } from "./chunk-MQRFIDS4.js";
4
4
  import {
5
5
  denoAdapter
6
6
  } from "./chunk-UISOJ4RW.js";
7
7
  import "./chunk-EOK6KATZ.js";
8
- import "./chunk-IN2QAGDZ.js";
8
+ import "./chunk-6EOUQCRM.js";
9
9
  import "./chunk-4IQXHHPF.js";
10
10
 
11
11
  // .docu/node/preview.deno.ts
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  runPreview
3
- } from "./chunk-PGIHPW5L.js";
3
+ } from "./chunk-MQRFIDS4.js";
4
4
  import {
5
5
  nodeAdapter
6
6
  } from "./chunk-UISOJ4RW.js";
7
7
  import "./chunk-EOK6KATZ.js";
8
- import "./chunk-IN2QAGDZ.js";
8
+ import "./chunk-6EOUQCRM.js";
9
9
  import "./chunk-4IQXHHPF.js";
10
10
 
11
11
  // .docu/node/preview.node.ts
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  runServer
3
- } from "./chunk-HF3KKRQL.js";
4
- import "./chunk-42JQLAKP.js";
3
+ } from "./chunk-6HFBJUMG.js";
4
+ import "./chunk-URBATJEC.js";
5
5
  import {
6
6
  denoAdapter
7
7
  } from "./chunk-UISOJ4RW.js";
8
8
  import "./chunk-EOK6KATZ.js";
9
- import "./chunk-IN2QAGDZ.js";
9
+ import "./chunk-6EOUQCRM.js";
10
10
  import "./chunk-4IQXHHPF.js";
11
11
 
12
12
  // .docu/node/server.deno.ts
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  runServer
3
- } from "./chunk-HF3KKRQL.js";
4
- import "./chunk-42JQLAKP.js";
3
+ } from "./chunk-6HFBJUMG.js";
4
+ import "./chunk-URBATJEC.js";
5
5
  import {
6
6
  nodeAdapter
7
7
  } from "./chunk-UISOJ4RW.js";
8
8
  import "./chunk-EOK6KATZ.js";
9
- import "./chunk-IN2QAGDZ.js";
9
+ import "./chunk-6EOUQCRM.js";
10
10
  import "./chunk-4IQXHHPF.js";
11
11
 
12
12
  // .docu/node/server.node.ts
@@ -12,7 +12,16 @@ import { createHash } from "node:crypto";
12
12
  import { join, dirname } from "node:path";
13
13
  import React from "react";
14
14
  import { renderToString } from "react-dom/server";
15
- import { compileMdx, compileMdxModule, frontmatterField, getGitLastModifiedBatch } from "./mdx";
15
+ import {
16
+ compileMdx,
17
+ compileMdxModule,
18
+ frontmatterField,
19
+ getGitLastModifiedBatch,
20
+ getPageContent,
21
+ getPageFrontmatter,
22
+ getPageStripped,
23
+ registerPageContent,
24
+ } from "./mdx";
16
25
  import {
17
26
  DOCS_DIR,
18
27
  DIST_DIR,
@@ -105,7 +114,24 @@ async function renderDocsPage(
105
114
  try {
106
115
  const remarkPlugins = builder?.collectRemarkPlugins();
107
116
  const rehypePlugins = builder?.collectRehypePlugins();
108
- result = await compileMdx(content, filePath, gitDates, remarkPlugins, rehypePlugins);
117
+ // Parse-once: reuse the prePass frontmatter + stripped content so the
118
+ // SSR phase does not re-extract them. Only when the prePass actually
119
+ // registered them — otherwise compileMdx does its own extraction.
120
+ const preFm = getPageFrontmatter(`/${slug}`);
121
+ const preStripped = getPageStripped(`/${slug}`);
122
+ const pre =
123
+ preFm !== undefined && preStripped !== undefined
124
+ ? { frontmatter: preFm, strippedContent: preStripped }
125
+ : undefined;
126
+ result = await compileMdx(
127
+ content,
128
+ filePath,
129
+ gitDates,
130
+ remarkPlugins,
131
+ rehypePlugins,
132
+ undefined,
133
+ pre
134
+ );
109
135
  } catch (err) {
110
136
  const msg = err instanceof Error ? err.message : "Unknown MDX error";
111
137
  throw new Error(`MDX Error in: docs/${slug}.mdx\n${msg}`, { cause: err });
@@ -241,6 +267,9 @@ export async function runBuild(): Promise<void> {
241
267
  } catch {
242
268
  return;
243
269
  }
270
+ // Cache the original content so the page loop does not re-read the file
271
+ // (one disk read per file — the frontmatter is parsed once here too).
272
+ registerPageContent(`/${file.path}`, raw);
244
273
  let content = raw;
245
274
  if (builder) {
246
275
  const relPath = file.absPath.replace(PROJECT_ROOT + "/", "");
@@ -249,7 +278,12 @@ export async function runBuild(): Promise<void> {
249
278
  }
250
279
  const remarkPlugins = builder?.collectRemarkPlugins();
251
280
  const rehypePlugins = builder?.collectRehypePlugins();
252
- mdxSources[file.path] = await compileMdxModule(content, remarkPlugins, rehypePlugins);
281
+ mdxSources[file.path] = await compileMdxModule(
282
+ content,
283
+ remarkPlugins,
284
+ rehypePlugins,
285
+ `/${file.path}`
286
+ );
253
287
  });
254
288
  await Promise.all(prePassTasks);
255
289
 
@@ -319,12 +353,14 @@ export async function runBuild(): Promise<void> {
319
353
  }
320
354
  }
321
355
 
322
- let rawMdx: string;
323
- try {
324
- rawMdx = await readFile(file.absPath, "utf-8");
325
- } catch (err) {
326
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
327
- continue;
356
+ let rawMdx = getPageContent(`/${file.path}`);
357
+ if (rawMdx === undefined) {
358
+ try {
359
+ rawMdx = await readFile(file.absPath, "utf-8");
360
+ } catch (err) {
361
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
362
+ continue;
363
+ }
328
364
  }
329
365
 
330
366
  if (rebuildDecision === "hash_check") {
@@ -458,8 +494,11 @@ export async function runBuild(): Promise<void> {
458
494
 
459
495
  logger.indexStart();
460
496
  t = performance.now();
461
- const indexCount = await generateSearchIndex();
462
- logger.indexDone(indexCount, Math.round(performance.now() - t));
497
+ // No content changed (all pages cached) → the aggregated index is
498
+ // unchanged too; reuse the existing file instead of regenerating it.
499
+ const indexSkipped = built === 0 && existsSync(join(ASSETS_DIR, "search-index.json"));
500
+ const indexCount = indexSkipped ? 0 : await generateSearchIndex();
501
+ logger.indexDone(indexCount, Math.round(performance.now() - t), indexSkipped);
463
502
 
464
503
  logger.routes();
465
504