Quartz MDX
Quartz MDX
Section titled “Quartz MDX”“Quartz 4 is great, but I need MDX.”
Reality check on Quartz 4
Section titled “Reality check on Quartz 4”Quartz 4’s pipeline is Markdown-first; it ships Obsidian-flavored and GFM syntax (callouts, wikilinks, tables, etc.) but does not document MDX support. That’s consistent with community notes that simply adding remark-mdx isn’t enough to enable MDX end-to-end in Quartz’s build.
Quartz: Authoring Content Remark MDX note
Yes: you can fork Quartz and add MDX. Here’s the shortest credible path that fits Quartz’s pipeline.
Why this is non-trivial
Section titled “Why this is non-trivial”Quartz parses Markdown → runs remark/rehype → converts HAST to JSX (Preact) → renders to static HTML. MDX needs either (A) MDX nodes to survive into HAST and be evaluated at render time, or (B) a compile step that turns .mdx into a component before emit.
Quartz Architecture @mdx-js/mdx remark-mdx docs
Two workable implementation strategies
Section titled “Two workable implementation strategies”1) “Pass-through MDX” (no separate compile step)
Section titled “1) “Pass-through MDX” (no separate compile step)”Goal: keep Quartz’s unified pipeline, add remark-mdx, and make HAST→JSX understand MDX nodes.
- Add a transformer plugin that registers
remark-mdxand configuresremark-rehypewithpassThrough: ['mdxjsEsm','mdxFlowExpression','mdxJsxFlowElement','mdxJsxTextElement','mdxTextExpression']so MDX nodes aren’t dropped.
remark-mdx docs Quartz: Making Plugins
- Update Quartz’s core parse step (where it calls
remark-rehype) to honor thatpassThroughoption from transformer plugins (today it converts mdast→hast without MDX awareness).
Quartz Architecture Quartz: Making Plugins
- At emit time, provide a
createEvaluatertohast-util-to-jsx-runtimeso embedded MDX expressions/ESM can be evaluated during server render (static HTML). You also map MDX components via theComponentsoption.
Tradeoffs: simplest patch footprint; supports JSX/expressions; still static output. Caveat: you’re evaluating arbitrary code at build—sandbox it.
2) “Compile MDX files” (separate path for .mdx)
Section titled “2) “Compile MDX files” (separate path for .mdx)”Goal: treat .mdx as first-class: compile to an ESM component with @mdx-js/mdx, then render it with Preact to HTML inside the existing emitter.
-
Extend Quartz’s content glob to include
**/*.mdx. -
Add a transformer that, when a file has
.mdx, runscompile()from@mdx-js/mdx(with your remark/rehype plugins), writes the JS to.quartz-cache/mdx/<slug>.mjs, then dynamically import it inside the emitter andrender()withpreact-render-to-string.
@mdx-js/mdx Quartz Architecture
- Provide a components map (shortcodes) so authors can import via frontmatter or a local registry.
Tradeoffs: slightly more code; the separation keeps Markdown path untouched and gives you full MDX parity.
Minimal patches (sketch)
Section titled “Minimal patches (sketch)”Files to touch
Section titled “Files to touch”quartz/build.ts: include.mdxin the glob; thread MDX plugin options into the unified pipeline.
quartz/plugins/transformers/(new):mdx.tstransformer registeringremark-mdxand (if you pick path 1) pass-through options.
quartz/components/renderPage.tsx(or where HAST→JSX happens): injectcreateEvaluaterand aComponentsmap; or (path 2) add an MDX branch that imports compiled modules and renders them.
Quartz Architecture hast-to-jsx README
Guardrails & security
Section titled “Guardrails & security”- Do not evaluate untrusted MDX blindly. If you enable expressions/ESM (path 1), wire
createEvaluaterto a restrictive VM (no fs/net; whitelist exports/imports). Or prefer path 2 and disable ESM in authoring conventions.
hast-to-jsx README @mdx-js/mdx
- Start with “static MDX” (components without client interactivity). Quartz already emits static HTML via Preact SSR; interactive islands would require client bundles you manually attach (Quartz can bundle “inline” client scripts, but there’s no automatic hydration).
Reality check
Section titled “Reality check”Multiple maintainers note that adding only remark-mdx will not enable MDX—you must either pass MDX nodes through and evaluate them, or compile MDX to JS. Plan accordingly.
Recommendation
Section titled “Recommendation”Fork Quartz and start with Strategy 2 (compile .mdx) for clear isolation and easier sandboxing. When stable, you can attempt Strategy 1 to unify the pipeline. I can draft the exact diffs (glob, transformer, emitter shim) and a tiny components registry if you want.
Quartz Architecture @mdx-js/mdx
// quartz/plugins/transformers/mdx.ts — Strategy 2 (compile)import {QuartzTransformerPlugin} from "../types"import {compile} from "@mdx-js/mdx"import * as fs from "node:fs/promises"import * as path from "node:path"
export const MdxCompile: QuartzTransformerPlugin = () => ({ name: "MdxCompile", async textTransform(ctx, src) { // leave raw text unchanged for non-MDX return src }, // Hook: after Quartz reads the file, detect .mdx in vfile.data and precompile markdownPlugins() { return [] }, htmlPlugins() { return [] }, async externalResources(ctx) { return {} },})
// Pseudocode wiring (keep in your build step):// if (filepath.endsWith(".mdx")) {// const out = await compile(src, {remarkPlugins: [...], rehypePlugins: [...]})// const outPath = path.join(".quartz-cache/mdx", slug + ".mjs")// await fs.mkdir(path.dirname(outPath), {recursive: true})// await fs.writeFile(outPath, String(out))// vfile.data.mdxModulePath = outPath// }// quartz/plugins/emitters/contentPage.tsx — MDX branch (sketch)import {render} from "preact-render-to-string"import {Fragment, jsx, jsxs} from "preact/jsx-runtime"import * as path from "node:path"export const ContentPage = () => ({ name: "ContentPage", getQuartzComponents() { /* unchanged */ }, async emit(ctx, content, resources, write) { const pages = [] for (const [tree, file] of content) { const slug = file.data.slug! let html: string if (file.data.mdxModulePath) { const mod = await import(path.resolve(file.data.mdxModulePath)) const MDXComp = mod.default html = render(<MDXComp components={{/* shortcodes */}} />) } else { html = renderPage(ctx.cfg, slug, { tree, fileData: file.data, /* ... */ }) } pages.push(await write({ ctx, slug, ext: ".html", content: html })) } return pages }})--- a/quartz/build.ts+++ b/quartz/build.ts- const files = await glob("content/**/*.md") // Markdown only+ const files = await glob("content/**/*.{md,mdx}") // Add MDX // ensure transformers run before emit and expose vfile.data.mdxModulePathReferences: Quartz pipeline & plugin interfaces; MDX pass-through & compile/eval details.[Quartz Architecture](https://quartz.jzhao.xyz/advanced/architecture)[Quartz: Making Plugins](https://quartz.jzhao.xyz/advanced/making-plugins)[remark-mdx docs](https://mdxjs.com/packages/remark-mdx/)[hast-to-jsx README](https://github.com/syntax-tree/hast-util-to-jsx-runtime)[@mdx-js/mdx](https://mdxjs.com/packages/mdx/)