Mastering Mintlify's MDX pipeline — undocumented constraints, snippet sandbox rules, CSS override patterns, and npm bundling via yoink. Use when writing or debugging Mintlify snippets, customizing docs.json/styles.css, bundling npm packages into snippets, hitting cryptic MDX compile errors, or when the user says 'mintlify', 'snippet won't compile', 'MDX error', 'yoink', 'lint-snippets', 'docs.json', 'Mintlify CSS', 'function-body mode', 'baseUrl error', or 'findExport'. Reverse-engineered from Mintlify internals at Dedalus Labs.
Mintlify is a Next.js App Router site that compiles MDX on the server, hydrates in the browser, and serves via Next. This skill covers the parts the docs don't — what the pipeline actually does, where the sandbox sharp edges are, and how to customize around them.
For content writing, read writing/voice and writing/structure in the docs repo. This skill is for engineers building custom components, integrations, or bundling npm packages.
.mdx file
→ remark parser (@mdx-js/mdx + acorn)
→ remarkMdxInjectSnippets ← inlines /snippets/*.jsx ASTs
→ estree-util-to-js (findExport) ← extracts one export at a time
→ next-mdx-remote-client/serialize (function-body mode)
→ Reflect.construct(SyncFunction, keys.concat(compiledSource))
→ component tree → SSR → hydrateThe critical detail: findExport extracts each ExportNamedDeclaration AST subtree in isolation. Sibling imports, sibling const declarations, sibling exports — all dropped from each export's scope.
Sibling declarations are invisible. This fails:
// FAILS: "ReferenceError: __lib is not defined"
export const __lib = (() => { return { Terminal, useTerminal }; })();
export const Terminal = __lib.Terminal; // __lib not in scope
export const useTerminal = __lib.useTerminal; // nor hereFix: one self-contained IIFE per export, even if it duplicates bytes.
export const Terminal = (() => { /* full body */ return Terminal; })();
export const useTerminal = (() => { /* full body again */ return useTerminal; })();import expressions are rejectedFunction-body mode refuses ESM syntax needing module resolution: import(), import.meta.url, export ... from. Error:
Unexpected missing options.baseUrl needed to support "export … from", "import", or "import.meta.url" when generating "function-body"
Fix: drop dynamic imports. Use plain fetch() instead of SDK imports.
Mintlify inlines snippets by walking imports in the consuming .mdx file only — no recursive snippet-to-snippet resolution. If a.jsx imports from b.jsx, b will be undefined.
Fix: colocate dependent code in a single snippet file.
react / react-dom imports resolveAny other npm specifier at module scope fails silently. To use third-party packages, bundle them via scripts/yoink.ts.
const SHADOW = "..."; // ← disappears after processing
export const Drawer = () => <div style={{ boxShadow: SHADOW }} />;
// SHADOW is undefined at renderFix: move constants inside the exported arrow, or export them too.
The function keyword is unsupported at snippet module scope. Arrow functions always work. Inside closures, both forms are fine.
!important font rules beat docs.jsonstyles.css sets font-family: var(--font-sans) !important on body/p/div/span/headings. Those out-specify the fonts block in docs.json. Components wanting a different font must re-declare with equal or higher specificity + !important.
The MDX compiler's acorn subset for inline export const blocks in .mdx files is stricter than .jsx snippets. These fail in inline MDX:
a ?? b → use a || bfn?.() → if (fn) fn()[a, ...rest] → rest = arr.slice(1).at(-1) → arr[arr.length - 1]import() expressions<word> in strings parsed as JSX tags → use [word] or {word}If acorn errors with "Could not parse expression," binary-search-comment from this list.
rounded-* globally** in :root, not per-component (the SHARP EDGES block).!important liberally. Mintlify injects Tailwind utilities with inline styles that otherwise win.div { font-family: X !important } with .wterm, .wterm * { font-family: Y !important }.var(--color-amethyst), var(--color-cloud), var(--color-nyx).grid-cols-[minmax(180px,max-content)_1fr] silently breaks. Write plain CSS classes in styles.css.Run scripts/lint-snippets.ts to catch sandbox violations before they hit runtime:
| Rule | Catches |
|---|---|
no-module-level-decl | Non-exported const/let/var/function/class at module scope |
no-npm-imports | Any import outside react, react-dom, react/jsx-runtime |
no-nested-imports | Snippet importing from /snippets/*.jsx |
missing-hook-import | useState() used without import { useState } from "react" |
no-exports | File has no export at all |
dom-takeover | innerHTML = "", replaceChildren(), outerHTML = — flags libraries that hijack React-owned DOM |
node --experimental-strip-types apps/docs/scripts/lint-snippets.ts apps/docs/src/snippetsFor any package beyond react/react-dom, use yoink. It runs esbuild, strips non-export keywords, wraps each export in a self-contained IIFE.
node --experimental-strip-types apps/docs/scripts/yoink.ts <package> \
--exports Foo,Bar --out src/snippets/foo.jsxConstraints:
import() — packages with dynamic imports will be rejected by Mintlifyreact and react-dom are externalized; other peer deps are bundled inlineLibraries calling innerHTML = "" or replaceChildren() on their React-owned host element need a client-only gate, or React 19's strict-mode double-mount collides with the DOM wipe.
export const Wrap = (props) => {
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []);
if (!mounted) return <div className="host-class" style={{ height: 300 }} />;
return <div ref={containerRef} className="host-class" style={{ height: 300 }} />;
};The dom-takeover lint rule warns when a yoinked snippet triggers this need.
text-decoration-line is not animatable (discrete). text-decoration-color is. Pin the line to underline, start color transparent, transition color on hover:
.dedalus-link {
text-decoration-line: underline !important;
text-decoration-color: transparent !important;
text-decoration-thickness: 1px;
text-underline-offset: 0.25em;
transition: text-decoration-color 200ms ease-out;
}
.dedalus-link:hover {
text-decoration-color: var(--color-amethyst-deep) !important;
}Generalization: any discrete → continuous property swap needs an animatable proxy (display → opacity, visibility → opacity, text-decoration-line → text-decoration-color).
EventSource doesn't support custom Authorization headers. Use fetch() with Accept: text/event-stream and manually read the body via ReadableStream. See references/constraints.md for the full implementation.
| Gotcha | Cause | Workaround |
|---|---|---|
"Invalid hook call" in mint dev stdout | Monorepo pins react@19.2.3, @mintlify/components pulls 19.2.4. CLI renderer loads both. | Ignore — browser rendering unaffected |
Concurrent mint dev instances interfere | Both share ~/.mintlify/mint/apps/client/.next cache | Run one at a time |
<id> in JSX strings parsed as tag | MDX treats <word> as potential JSX everywhere | Use [id], {id}, or escape |
| CORS failures on DCS shell | dcs.dedaluslabs.ai missing Access-Control-Allow-Origin | Fix at ingress (Terraform/ALB/CloudFront) |
apps/docs/ → docs.dedaluslabs.ai (external, public)
docs/ → docs-internal.dedaluslabs.ai (this site, internal)Both run Mintlify's Sequoia theme. Config in each tree's src/docs.json. Custom CSS in src/styles.css. Snippets in src/snippets/*.jsx. Keep stylesheets in sync when touching brand-level CSS.
# dev servers (run ONE at a time — shared .next cache)
pnpm dev docs # apps/docs → port 3001
pnpm dev docs --internal # docs/ → port 3002
# build
pnpm -C apps/docs build
pnpm -C docs build
# bundle npm package into snippet
cd apps/docs
node --experimental-strip-types scripts/yoink.ts <package> \
--exports Foo,Bar --out src/snippets/foo.jsx
# lint all snippets
node --experimental-strip-types scripts/lint-snippets.ts src/snippets
# broken link + a11y checks
pnpm -C apps/docs lintWhen you hit a new Mintlify constraint, add it with:
vercel-react-best-practices — React/Next.js performance patterns (Mintlify is Next.js under the hood)frontend-design — Distinctive frontend aesthetics for docs componentsseo-audit — Technical SEO for the docs sites