diff --git a/src/lib/docs/mdx-components.tsx b/mdx-components.tsx
similarity index 71%
rename from src/lib/docs/mdx-components.tsx
rename to mdx-components.tsx
index 5d0e74a..49362eb 100644
--- a/src/lib/docs/mdx-components.tsx
+++ b/mdx-components.tsx
@@ -1,3 +1,4 @@
+import type { MDXComponents } from "mdx/types";
import Blockquote from "@/components/blockquote";
import ButtonLinks from "@/components/button-links";
import Callout, {
@@ -9,7 +10,6 @@ import Callout, {
} from "@/components/callout";
import CardLinks from "@/components/card-links";
import CodeBlock from "@/components/codeblock";
-import s from "@/components/custom-mdx/CustomMDX.module.css";
import DonateCard from "@/components/donate-card";
import GitHub from "@/components/github";
import { processGitHubLinks } from "@/components/github/mdx";
@@ -19,7 +19,13 @@ import SponsorCard from "@/components/sponsor-card";
import { BodyParagraph, LI } from "@/components/text";
import VTSequence from "@/components/vt-sequence";
import Video from "@/components/video";
-import { isValidElement, type ReactElement } from "react";
+import s from "@/lib/docs/docs-mdx.module.css";
+import {
+ isValidElement,
+ type ComponentPropsWithoutRef,
+ type ImgHTMLAttributes,
+ type ReactElement,
+} from "react";
type MermaidCodeElement = {
className?: string;
@@ -33,35 +39,35 @@ function isReactElement(
return isValidElement(children);
}
-// mdxComponents defines the React component map used by docs MDX compilation.
-export const mdxComponents = {
- h1: (props: React.ComponentPropsWithoutRef<"h1">) => (
+// mdxComponents defines the React component map used while rendering docs MDX.
+const mdxComponents: MDXComponents = {
+ h1: (props: ComponentPropsWithoutRef<"h1">) => (
),
- h2: (props: React.ComponentPropsWithoutRef<"h2">) => (
+ h2: (props: ComponentPropsWithoutRef<"h2">) => (
),
- h3: (props: React.ComponentPropsWithoutRef<"h3">) => (
+ h3: (props: ComponentPropsWithoutRef<"h3">) => (
),
- h4: (props: React.ComponentPropsWithoutRef<"h4">) => (
+ h4: (props: ComponentPropsWithoutRef<"h4">) => (
),
- h5: (props: React.ComponentPropsWithoutRef<"h5">) => (
+ h5: (props: ComponentPropsWithoutRef<"h5">) => (
),
- h6: (props: React.ComponentPropsWithoutRef<"h6">) => (
+ h6: (props: ComponentPropsWithoutRef<"h6">) => (
),
- li: (props: React.ComponentPropsWithoutRef<"li">) => {
+ li: (props: ComponentPropsWithoutRef<"li">) => {
const processedChildren = processGitHubLinks(props.children);
return
{processedChildren};
},
- p: (props: React.ComponentPropsWithoutRef<"p">) => {
+ p: (props: ComponentPropsWithoutRef<"p">) => {
const processedChildren = processGitHubLinks(props.children);
return {processedChildren};
},
- code: (props: React.ComponentPropsWithoutRef<"code">) => {
+ code: (props: ComponentPropsWithoutRef<"code">) => {
if (!props.className) {
return ;
}
@@ -75,7 +81,7 @@ export const mdxComponents = {
return ;
},
- pre: (props: React.ComponentPropsWithoutRef<"pre">) => {
+ pre: (props: ComponentPropsWithoutRef<"pre">) => {
const { children } = props;
if (isReactElement(children)) {
const className = children.props?.className;
@@ -93,7 +99,7 @@ export const mdxComponents = {
return ;
},
blockquote: Blockquote,
- img: (props: React.ImgHTMLAttributes) => (
+ img: (props: ImgHTMLAttributes) => (
// biome-ignore lint/performance/noImgElement: Docs content deliberately uses plain img tags.
),
@@ -115,3 +121,11 @@ export const mdxComponents = {
),
};
+
+// useMDXComponents returns the component map that Next.js uses while rendering MDX files.
+export function useMDXComponents(components: MDXComponents): MDXComponents {
+ return {
+ ...mdxComponents,
+ ...components,
+ };
+}
diff --git a/next.config.mjs b/next.config.mjs
index 78e23dc..8b43d47 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,5 +1,42 @@
+import createMDX from "@next/mdx";
+import { createRequire } from "node:module";
+
+// require resolves plugin module paths for the MDX loader in ESM config files.
+const require = createRequire(import.meta.url);
+
+// gfmAlertsAsCalloutsPlugin points at the local remark plugin that maps GFM alerts to Callout nodes.
+const gfmAlertsAsCalloutsPlugin = require.resolve(
+ "./src/lib/docs/remark-gfm-alerts-as-callouts.mjs",
+);
+
+// headingIdsPlugin points at the local remark plugin that applies stable heading IDs.
+const headingIdsPlugin = require.resolve(
+ "./src/lib/docs/remark-heading-ids.mjs",
+);
+
+// syntaxHighlightingPlugin points at the local rehype plugin that enables code highlighting.
+const syntaxHighlightingPlugin = require.resolve(
+ "./src/lib/docs/rehype-highlight-all.mjs",
+);
+
+// withMDX configures the Next.js MDX pipeline for docs rendering.
+const withMDX = createMDX({
+ extension: /\.mdx?$/,
+ options: {
+ remarkPlugins: [
+ "remark-frontmatter",
+ "remark-gfm",
+ gfmAlertsAsCalloutsPlugin,
+ headingIdsPlugin,
+ ],
+ rehypePlugins: [syntaxHighlightingPlugin],
+ },
+});
+
+// nextConfig contains shared website framework and header behavior.
/** @type {import('next').NextConfig} */
const nextConfig = {
+ pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
reactStrictMode: true,
experimental: {
// The homepage animation and docs reference pages intentionally ship
@@ -28,4 +65,4 @@ const nextConfig = {
},
};
-export default nextConfig;
+export default withMDX(nextConfig);
diff --git a/package-lock.json b/package-lock.json
index 0a17791..694b50c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "ghostty-website",
"version": "0.1.0",
"dependencies": {
+ "@mdx-js/loader": "^3.1.1",
+ "@next/mdx": "^16.1.6",
"@r4ai/remark-callout": "^0.6.2",
"classnames": "^2.5.1",
"fast-xml-parser": "^5.3.7",
@@ -15,12 +17,14 @@
"lucide-react": "^0.575.0",
"mermaid": "^11.12.3",
"next": "^16.1.6",
- "next-mdx-remote": "^6.0.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-intersection-observer": "^10.0.3",
"rehype-highlight": "^7.0.2",
+ "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
+ "remark-mdx": "^3.1.1",
+ "remark-parse": "^11.0.0",
"slugify": "^1.6.6",
"zustand": "^5.0.11"
},
@@ -45,29 +49,6 @@
"url": "https://github.com/sponsors/antfu"
}
},
- "node_modules/@babel/code-frame": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
- "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@biomejs/biome": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.4.tgz",
@@ -769,6 +750,28 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@mdx-js/loader": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-3.1.1.tgz",
+ "integrity": "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@mdx-js/mdx": "^3.0.0",
+ "source-map": "^0.7.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "webpack": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@mdx-js/mdx": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz",
@@ -806,23 +809,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/@mdx-js/react": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz",
- "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==",
- "license": "MIT",
- "dependencies": {
- "@types/mdx": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- },
- "peerDependencies": {
- "@types/react": ">=16",
- "react": ">=16"
- }
- },
"node_modules/@mermaid-js/parser": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz",
@@ -838,6 +824,27 @@
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"license": "MIT"
},
+ "node_modules/@next/mdx": {
+ "version": "16.1.6",
+ "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-16.1.6.tgz",
+ "integrity": "sha512-PT5JR4WPPYOls7WD6xEqUVVI9HDY8kY7XLQsNYB2lSZk5eJSXWu3ECtIYmfR0hZpx8Sg7BKZYKi2+u5OTSEx0w==",
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "^0.7.0"
+ },
+ "peerDependencies": {
+ "@mdx-js/loader": ">=0.15.0",
+ "@mdx-js/react": ">=0.15.0"
+ },
+ "peerDependenciesMeta": {
+ "@mdx-js/loader": {
+ "optional": true
+ },
+ "@mdx-js/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@next/swc-darwin-arm64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
@@ -1315,6 +1322,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1566,6 +1574,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
"license": "MIT"
},
"node_modules/cytoscape": {
@@ -2343,6 +2352,27 @@
"fxparser": "src/cli/cli.js"
}
},
+ "node_modules/fault": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz",
+ "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
@@ -2562,12 +2592,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
"node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
@@ -2757,6 +2781,24 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-frontmatter": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz",
+ "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-extension-frontmatter": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-gfm": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
@@ -3101,6 +3143,22 @@
"micromark-util-types": "^2.0.0"
}
},
+ "node_modules/micromark-extension-frontmatter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz",
+ "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==",
+ "license": "MIT",
+ "dependencies": {
+ "fault": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/micromark-extension-gfm": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
@@ -3838,28 +3896,6 @@
}
}
},
- "node_modules/next-mdx-remote": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/next-mdx-remote/-/next-mdx-remote-6.0.0.tgz",
- "integrity": "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==",
- "license": "MPL-2.0",
- "dependencies": {
- "@babel/code-frame": "^7.23.5",
- "@mdx-js/mdx": "^3.0.1",
- "@mdx-js/react": "^3.0.1",
- "unist-util-remove": "^4.0.0",
- "unist-util-visit": "^5.1.0",
- "vfile": "^6.0.1",
- "vfile-matter": "^5.0.0"
- },
- "engines": {
- "node": ">=14",
- "npm": ">=7"
- },
- "peerDependencies": {
- "react": ">=16"
- }
- },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -4109,6 +4145,22 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark-frontmatter": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz",
+ "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-frontmatter": "^2.0.0",
+ "micromark-extension-frontmatter": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -4564,21 +4616,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unist-util-remove": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz",
- "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==",
- "license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-is": "^6.0.0",
- "unist-util-visit-parents": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@@ -4648,20 +4685,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/vfile-matter": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/vfile-matter/-/vfile-matter-5.0.1.tgz",
- "integrity": "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==",
- "license": "MIT",
- "dependencies": {
- "vfile": "^6.0.0",
- "yaml": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@@ -4725,21 +4748,6 @@
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"license": "MIT"
},
- "node_modules/yaml": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
- "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
"node_modules/zustand": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
diff --git a/package.json b/package.json
index 76dc819..0164ec9 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"format": "biome format --write ."
},
"dependencies": {
+ "@mdx-js/loader": "^3.1.1",
+ "@next/mdx": "^16.1.6",
"@r4ai/remark-callout": "^0.6.2",
"classnames": "^2.5.1",
"fast-xml-parser": "^5.3.7",
@@ -17,12 +19,14 @@
"lucide-react": "^0.575.0",
"mermaid": "^11.12.3",
"next": "^16.1.6",
- "next-mdx-remote": "^6.0.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-intersection-observer": "^10.0.3",
"rehype-highlight": "^7.0.2",
+ "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
+ "remark-mdx": "^3.1.1",
+ "remark-parse": "^11.0.0",
"slugify": "^1.6.6",
"zustand": "^5.0.11"
},
diff --git a/src/app/docs/DocsPageContent.tsx b/src/app/docs/DocsPageContent.tsx
index 7ddcbbe..5842018 100644
--- a/src/app/docs/DocsPageContent.tsx
+++ b/src/app/docs/DocsPageContent.tsx
@@ -4,10 +4,10 @@ import ScrollToTopButton from "@/components/scroll-to-top";
import Sidecar from "@/components/sidecar";
import { H1, P } from "@/components/text";
import { DOCS_PAGES_ROOT_PATH, GITHUB_REPO_URL } from "@/lib/docs/config";
+import customMdxStyles from "@/lib/docs/docs-mdx.module.css";
import type { DocsPageData } from "@/lib/docs/page";
import { Pencil } from "lucide-react";
import s from "./DocsPage.module.css";
-import customMdxStyles from "@/components/custom-mdx/CustomMDX.module.css";
interface DocsPageContentProps {
navTreeData: NavTreeNode[];
diff --git a/src/components/custom-mdx/CustomMDX.module.css b/src/lib/docs/docs-mdx.module.css
similarity index 100%
rename from src/components/custom-mdx/CustomMDX.module.css
rename to src/lib/docs/docs-mdx.module.css
diff --git a/src/lib/docs/page.ts b/src/lib/docs/page.ts
index dfa5b27..48407b6 100644
--- a/src/lib/docs/page.ts
+++ b/src/lib/docs/page.ts
@@ -1,21 +1,13 @@
-import remarkCallout, {
- type Options as RemarkCalloutOptions,
-} from "@r4ai/remark-callout";
import matter from "gray-matter";
-import { all } from "lowlight";
-import type { Root } from "mdast";
-import { compileMDX } from "next-mdx-remote/rsc";
import { promises as fs } from "node:fs";
-import type { ReactNode } from "react";
-import rehypeHighlight, {
- type Options as RehypeHighlightOptions,
-} from "rehype-highlight";
+import { createElement, type ComponentType, type ReactNode } from "react";
+import remarkMdx from "remark-mdx";
+import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import slugify from "slugify";
-import type { Plugin } from "unified";
+import { unified } from "unified";
import type { Node } from "unist";
import { visit } from "unist-util-visit";
-import { mdxComponents } from "./mdx-components";
const nodePath = require("node:path");
@@ -75,36 +67,8 @@ async function loadDocsPageFromRelativeFilePath(
): Promise {
const mdxFileContent = matter.read(relativeFilePath);
const slug = slugFromRelativeFilePath(relativeFilePath);
-
- const pageHeaders: PageHeader[] = [];
-
- const { content } = await compileMDX({
- source: mdxFileContent.content,
- components: mdxComponents,
- options: {
- // next-mdx-remote v6 blocks JS expressions by default. Our docs use
- // trusted MDX expressions, so keep JS enabled while retaining the
- // dangerous-global protections introduced in v6.
- blockJS: false,
- blockDangerousJS: true,
- mdxOptions: {
- remarkPlugins: [
- remarkGfm,
- gfmAlertsAsCallouts(),
- parseAnchorLinks({ pageHeaders }),
- ],
- rehypePlugins: [
- [
- rehypeHighlight,
- {
- detect: false,
- languages: all,
- } satisfies RehypeHighlightOptions,
- ],
- ],
- },
- },
- });
+ const pageHeaders = await extractPageHeaders(mdxFileContent.content);
+ const MdxContent = await loadMdxComponent(relativeFilePath);
return {
slug,
relativeFilePath,
@@ -116,33 +80,40 @@ async function loadDocsPageFromRelativeFilePath(
hideSidecar: Object.hasOwn(mdxFileContent.data, "hideSidecar")
? mdxFileContent.data.hideSidecar
: false,
- content,
+ content: createElement(MdxContent),
pageHeaders,
};
}
-// gfmAlertsAsCallouts converts GFM alert blocks into typed Callout MDX nodes.
-function gfmAlertsAsCallouts(): [
- Plugin<[RemarkCalloutOptions], Root>,
- RemarkCalloutOptions,
-] {
- return [
- remarkCallout,
- {
- root: (callout) => ({
- tagName: "Callout",
- properties: {
- type: callout.type.toLowerCase(),
- isFoldable: String(callout.isFoldable),
- },
- }),
- // We won't use title, just type.
- title: () => ({
- tagName: "callout-title",
- properties: {},
- }),
- } satisfies RemarkCalloutOptions,
- ];
+// MdxModule is the expected shape of an imported MDX module.
+type MdxModule = {
+ default: ComponentType;
+};
+
+// loadMdxComponent loads the statically-compiled MDX React component for one docs file.
+async function loadMdxComponent(
+ relativeFilePath: string,
+): Promise {
+ const normalizedRelativePath = relativeFilePath
+ .replaceAll(nodePath.sep, "/")
+ .replace(/^\.\//, "");
+ const docsRelativePath = normalizedRelativePath.replace(/^docs\//, "");
+ const importPath = `../../../docs/${docsRelativePath}`;
+ const mdxModule = (await import(importPath)) as MdxModule;
+ return mdxModule.default;
+}
+
+// extractPageHeaders parses MDX source and returns stable heading metadata.
+async function extractPageHeaders(source: string): Promise {
+ const pageHeaders: PageHeader[] = [];
+ const processor = unified()
+ .use(remarkParse)
+ .use(remarkMdx)
+ .use(remarkGfm)
+ .use(parseAnchorLinks({ pageHeaders }));
+ const tree = processor.parse(source);
+ await processor.run(tree);
+ return pageHeaders;
}
// parseAnchorLinks captures headings into pageHeaders and assigns stable heading IDs.
diff --git a/src/lib/docs/rehype-highlight-all.mjs b/src/lib/docs/rehype-highlight-all.mjs
new file mode 100644
index 0000000..c37b4b1
--- /dev/null
+++ b/src/lib/docs/rehype-highlight-all.mjs
@@ -0,0 +1,10 @@
+import { all } from "lowlight";
+import rehypeHighlight from "rehype-highlight";
+
+// rehypeHighlightAll enables syntax highlighting with the full lowlight language set.
+export default function rehypeHighlightAll() {
+ return rehypeHighlight({
+ detect: false,
+ languages: all,
+ });
+}
diff --git a/src/lib/docs/remark-gfm-alerts-as-callouts.mjs b/src/lib/docs/remark-gfm-alerts-as-callouts.mjs
new file mode 100644
index 0000000..ecbe719
--- /dev/null
+++ b/src/lib/docs/remark-gfm-alerts-as-callouts.mjs
@@ -0,0 +1,22 @@
+import remarkCallout from "@r4ai/remark-callout";
+
+// calloutTransform maps GFM alerts to the custom MDX callout elements used by docs pages.
+const calloutTransform = remarkCallout({
+ root: (callout) => ({
+ tagName: "Callout",
+ properties: {
+ type: callout.type.toLowerCase(),
+ isFoldable: String(callout.isFoldable),
+ },
+ }),
+ // We won't use title, just type.
+ title: () => ({
+ tagName: "callout-title",
+ properties: {},
+ }),
+});
+
+// remarkGfmAlertsAsCallouts exposes the callout transform as a remark plugin.
+export default function remarkGfmAlertsAsCallouts() {
+ return calloutTransform;
+}
diff --git a/src/lib/docs/remark-heading-ids.mjs b/src/lib/docs/remark-heading-ids.mjs
new file mode 100644
index 0000000..b4e2595
--- /dev/null
+++ b/src/lib/docs/remark-heading-ids.mjs
@@ -0,0 +1,39 @@
+import slugify from "slugify";
+import { visit } from "unist-util-visit";
+
+// remarkHeadingIds applies stable IDs and de-duplication indices to heading nodes.
+export default function remarkHeadingIds() {
+ // encounteredIDs tracks duplicate IDs so each heading receives a unique anchor.
+ const encounteredIDs = new Map();
+
+ return (node) => {
+ visit(node, "heading", (headingNode) => {
+ if (!Array.isArray(headingNode.children) || headingNode.children.length === 0) {
+ return;
+ }
+
+ const text = headingNode.children
+ .map((child) => (typeof child.value === "string" ? child.value : ""))
+ .join("");
+ const baseId = slugify(text.toLowerCase());
+ const encounteredCount = (encounteredIDs.get(baseId) || 0) + 1;
+ encounteredIDs.set(baseId, encounteredCount);
+ const resolvedID = encounteredCount >= 2 ? `${baseId}-${encounteredCount}` : baseId;
+
+ if (!headingNode.data) {
+ headingNode.data = {};
+ }
+ headingNode.data.hProperties = {
+ ...headingNode.data.hProperties,
+ id: resolvedID,
+ };
+
+ if (encounteredCount >= 2) {
+ headingNode.data.hProperties = {
+ ...headingNode.data.hProperties,
+ "data-index": encounteredCount.toString(),
+ };
+ }
+ });
+ };
+}