Skip to content

Enhance the MDX setup #2265

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 76 additions & 60 deletions src/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type { MDXComponents } from "mdx/types";
import React, { ReactNode } from "react";
import React from "react";
import { CodeExample } from "./components/code-example";
import Link from "next/link";

declare module "mdx/types" {
// Augment the MDX types to make it understand React.
namespace JSX {
type Element = React.JSX.Element;
type ElementClass = React.JSX.ElementClass;
type ElementType = React.JSX.ElementType;
type IntrinsicElements = React.JSX.IntrinsicElements;
}
}

function getTextContent(node: React.ReactNode): string {
if (typeof node === "string" || typeof node === "number") {
return String(node);
Expand Down Expand Up @@ -51,64 +61,70 @@ function createHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
};
}

const components = {
// Allows customizing built-in components, e.g. to add styling.
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,

h2: createHeading(2),
h3: createHeading(3),
h4: createHeading(4),
h5: createHeading(5),
h6: createHeading(6),

a(props) {
return <Link {...(props as React.ComponentProps<typeof Link>)} />;
},

code({ children }) {
if (typeof children !== "string") {
return <code>{children}</code>;
}

if (children.startsWith("<")) {
return <code>{children}</code>;
}

return (
<code>
{children
.split(/(<[^>]+>)/g)
.map((part, i) => (part.startsWith("<") && part.endsWith(">") ? <var key={i}>{part}</var> : part))}
</code>
);
},

pre(props) {
let child = React.Children.only(props.children) as React.ReactElement;
if (!child) return null;

// @ts-ignore
let { className, children: code } = child.props;
let lang = className ? className.replace("language-", "") : "";
let filename = undefined;

// Extract `[!code filename:…]` directives from the first line of code
let lines = code.split("\n");
let filenameRegex = /\[\!code filename\:(.+)\]/;
let match = lines[0].match(filenameRegex);
if (match) {
filename = match[1];
code = lines.splice(1).join("\n");
}

return (
<div>
<CodeExample example={{ lang, code }} className="not-prose" filename={filename} />
</div>
);
},
} satisfies MDXComponents;

declare global {
// Provide type-safety of provided components inside MDX files.
type MDXProvidedComponents = typeof components;
}

// This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Allows customizing built-in components, e.g. to add styling.
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
...components,

h2: createHeading(2),
h3: createHeading(3),
h4: createHeading(4),
h5: createHeading(5),
h6: createHeading(6),

a(props: any) {
return <Link {...props} />;
},

code({ children }: { children: string | ReactNode }) {
if (typeof children !== "string") {
return <code>{children}</code>;
}

if (children.startsWith("<")) {
return <code>{children}</code>;
}

return (
<code>
{children
.split(/(<[^>]+>)/g)
.map((part, i) => (part.startsWith("<") && part.endsWith(">") ? <var key={i}>{part}</var> : part))}
</code>
);
},

pre(props) {
let child = React.Children.only(props.children) as React.ReactElement;
if (!child) return null;

// @ts-ignore
let { className, children: code } = child.props;
let lang = className ? className.replace("language-", "") : "";
let filename = undefined;

// Extract `[!code filename:…]` directives from the first line of code
let lines = code.split("\n");
let filenameRegex = /\[\!code filename\:(.+)\]/;
let match = lines[0].match(filenameRegex);
if (match) {
filename = match[1];
code = lines.splice(1).join("\n");
}

return (
<div>
<CodeExample example={{ lang, code }} className="not-prose" filename={filename} />
</div>
);
},
};
export function useMDXComponents(): MDXProvidedComponents {
return components;
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
"@/*": ["./src/*"]
}
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also add the following to change the subtle TypeScript MDX editor hints into red squiggly type errors inside MDX files:

Suggested change
},
},
"mdx": {
"checkMdx": true
},

"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.mdx", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default TypeScript include pattern is **/*. Next.js forces you to be explicit. This alternative works too and is also accepted by Next.js:

Suggested change
"include": ["next-env.d.ts", "**/*.mdx", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*", ".next/types/**/*.ts"],

"exclude": ["node_modules"]
}