← back

Shiki Code Blocks with Turbopack

October 28, 2025

#next.js#typescript

#
Overview

When building this blog, I wanted to have beautiful, syntax-highlighted code blocks with support for diff highlighting, line highlighting, and other visual enhancements. While Next.js has built-in MDX support, I had some problems getting custom Shiki configuration working properly with Turbopack, especially when configuring custom JavaScript functions in next.config.ts due to Turbopack being written in Rust.

#
The Problem

Out of the box, Next.js MDX support doesn't include advanced code highlighting features like:

  • Syntax highlighting with custom themes
  • Custom transformers for enhanced code blocks like Diff highlighting (// [!code ++] and // [!code --]) and line highlighting (// [!code highlight])

#
The Solution

This walks through how I moved my custom Shiki configuration from next.config.ts into a custom Next.js Webpack loader to solve the issues with Turbopack. Here's how I set it up:

#
1. Custom MDX Loader

First, I created a custom loader in the loader/ directory:

loader/package.json
{
  "name": "turbopack-mdx-loader",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "main": "index.mjs",
  "exports": {
    ".": "./index.mjs"
  },
  "dependencies": {
    "@mdx-js/mdx": "^3.1.0",
    "@shikijs/rehype": "^1.26.1",
    "@shikijs/transformers": "^1.26.1",
    "rehype-mdx-code-props": "^3.0.1",
    "rehype-slug": "^6.0.0"
  }
}
json

The main loader logic handles MDX compilation with Shiki:

loader/index.mjs
import * as mdx from "@mdx-js/mdx";
import rehypeShiki from "@shikijs/rehype";
import {
  transformerNotationDiff,
  transformerNotationHighlight,
} from "@shikijs/transformers";
import rehypeMdxCodeProps from "rehype-mdx-code-props";
import rehypeSlug from "rehype-slug";

const DEFAULT_RENDERER = `import React from 'react'`;

// custom shiki configuration matching next.config.ts
const rehypeShikiOptions = {
  themes: { light: "one-dark-pro", dark: "one-dark-pro" },
  addLanguageClass: true,
  transformers: [transformerNotationDiff(), transformerNotationHighlight()],

  // hack to pass data props through shiki
  // https://github.com/shikijs/shiki/issues/629
  parseMetaString: (str) => {
    return Object.fromEntries(
      str.split(" ").reduce((prev, curr) => {
        const [key, value] = curr.split("=");
        const isNormalKey = /^[A-Z0-9]+$/i.test(key);
        if (isNormalKey) {
          prev.push([key, value || true]);
        }
        return prev;
      }, []),
    );
  },
};

const loader = async function (content) {
  const callback = this.async();
  const isDev = this.mode === "development";

  const options = {
    development: isDev,
    providerImportSource: "@/mdx-components",
    remarkPlugins: [],
    rehypePlugins: [
      [rehypeShiki, rehypeShikiOptions],
      rehypeSlug, // add `id` to all headings
      rehypeMdxCodeProps, // provide custom props on `code` and `pre` blocks
    ],
  };

  let result;

  try {
    result = await mdx.compile(content, options);
  } catch (err) {
    return callback(err);
  }

  const { renderer = DEFAULT_RENDERER } = options;

  const code = `${renderer}\n${result}`;
  return callback(null, code);
};

export default loader;
js

#
2. Install the Loader Package

Add the custom loader to your main app's dependencies:

package.json
{
  "dependencies": {
    ...
    "turbopack-mdx-loader": "file:./loader"
  }
}
json

#
3. Next.js Configuration

The key is configuring Turbopack to use our custom loader for MDX files:

next.config.ts
const nextConfig: NextConfig = {
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
  turbopack: {
    rules: {
      "*.mdx": {
        loaders: ["turbopack-mdx-loader"],
        as: "*.tsx",
      },
    },
  },
  // ... rest of config
};
ts

This tells Turbopack to:

  • Use our custom turbopack-mdx-loader for all .mdx files
  • Treat the output as TypeScript JSX (.tsx)

#
4. Shiki Features

With this setup, I continue to get several powerful features while still using Turbopack:

Diff Highlighting:

example.js
function example() {
  const oldValue = "hello"; 
  const newValue = "world"; 
  return newValue;
}
js

Line Highlighting:

example.js
function example() {
  const highlighted = "this line is highlighted"; 
  const normal = "this line is normal";
}
js

#
Complete 🎉

You can find all the code for this setup in this website's GitHub repository. The custom loader approach works great with Turbopack and provides excellent performance in development!