Working with MDX and Shiki
A step-by-step guide to wiring MDX and Shiki highlighting.
This is a practical guide to set up MDX and add Shiki-based code highlighting. I will keep it direct and show real code. The goal is simple: write MDX, render it in Next.js, and get clean code blocks with theme-based tokens.
1. Install the packages
You need a renderer and a highlighter. In this setup, I use next-mdx-remote
with rehype-pretty-code, which wraps Shiki.
pnpm add next-mdx-remote rehype-pretty-code shiki gray-matterIf you are not already using React server components, this still works. The same idea applies in either case.
2. Store MDX files
Place MDX content in a folder you can read from the server. I keep mine here:
public/content/writings
Example file:
---
title: Working with MDX
description: A short guide.
date: 2026-01-30
---
This is the content of the MDX file.
```tsx
export const hello = () => "Hello from MDX";
```3. Read the MDX file and parse frontmatter
This example loads a file by slug and returns frontmatter + content. I use
gray-matter because it is small and reliable.
import { readFile } from "node:fs/promises";
import path from "node:path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "public", "content", "writings");
export const getWritingSource = async (slug: string) => {
const filePath = path.join(CONTENT_DIR, `${slug}.mdx`);
const source = await readFile(filePath, "utf8");
const { content, data } = matter(source);
return {
content,
frontmatter: data,
};
};4. Render MDX with Shiki tokens
rehype-pretty-code turns code blocks into Shiki-colored tokens. Use the theme
you like. I keep the background off in the plugin, then style the block with
CSS so it matches the site.
import { compileMDX } from "next-mdx-remote/rsc";
import rehypePrettyCode from "rehype-pretty-code";
const { content: mdxContent } = await compileMDX({
source: content,
options: {
mdxOptions: {
rehypePlugins: [
[rehypePrettyCode, { theme: "one-dark-pro", keepBackground: false }],
],
},
},
});5. Add CSS to make code blocks look good
Shiki injects token colors, but you still want consistent spacing and layout. Here is the minimal CSS I use:
.mdx-content :where(pre) {
background: #bfbfbf09;
border: 1px solid oklch(1 0 0 / 12%);
border-radius: 12px;
color: var(--shiki-color-text);
font-family: var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875rem;
line-height: 1.7;
padding: 0.75rem;
padding-left: 1.1rem;
overflow: auto;
}
.mdx-content :where(pre code) {
background: transparent;
padding: 0;
}6. Optional: add a copy button
If you want a copy button, wrap the pre in a client component and expose a
small button on hover.
"use client";
import { useRef } from "react";
const CopyablePre = ({ children, ...props }: React.ComponentProps<"pre">) => {
const preRef = useRef<HTMLPreElement>(null);
const handleCopy = async () => {
const text = preRef.current?.innerText?.trim();
if (text) {
await navigator.clipboard.writeText(text);
}
};
return (
<div className="mdx-code-group">
<button type="button" onClick={handleCopy} className="mdx-code-copy">
Copy
</button>
<pre ref={preRef} {...props}>
{children}
</pre>
</div>
);
};Then pass it into the MDX renderer:
const { content: mdxContent } = await compileMDX({
source: content,
options: {
mdxOptions: {
rehypePlugins: [
[rehypePrettyCode, { theme: "one-dark-pro", keepBackground: false }],
],
},
},
components: {
pre: CopyablePre,
},
});7. Summary
That is the full loop:
- Write MDX files with frontmatter.
- Read them on the server.
- Render with
compileMDX. - Use
rehype-pretty-codefor tokens. - Style blocks so they match the site.
Once this is in place, every post gets consistent code styling and it becomes easy to write new articles without touching the renderer.