A

anishfn

Fullstack Engineer

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-matter

If 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-code for 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.