Syntax Highlighting with Shiki, React Server Components, and Next.js

lokman musliu
Lokman Musliu

February 8, 2024 ยท 4 min read ยท 2,037 views

Blog Image

08-06-2024 Update:

The article was updated to make a singleton highlighter, as mentioned in the latest Shiki update, to resolve certain memory leaks when the highlighter is called from React Server Components. Here's a list of the updates:

  • Updated getHighlighter to be a singleton with makeSingletonHighlighter

  • Removed custom Blade language as now it's bundled with Shiki!

Introduction

Wouldn't it be wonderful if we could deliver syntax highlighting to our readers without adding any additional JavaScript weight? That's precisely what we'll accomplish using Shiki, React Server Components (RSC), and Next.js.

Getting Started with Shiki

First, we need to install Shiki:

npm install -D shiki

Next, we'll create a React Server Component. In Next.js's App Router, Server Components are the default:

export default async function Code({ code, language }) {
	return <div></div>;
}

From the props in our component, we take a code and a language. The code prop might come from a Content Management System (CMS) and typically contains the HTML code block for our code examples. The language prop indicates which programming language the syntax highlighter should use.

Creating a Helper Function

To keep our code clean, we will wrap the syntax highlighting logic in a helper function. Go ahead and make a file called shiki.js inside the utils folder, and add the following content:

import { createHighlighter, makeSingletonHighlighter } from 'shiki';
import { bundledLanguages } from 'shiki/bundle/web';

import antlers from '../../content/languages/antlers.json';

const getHighlighter = makeSingletonHighlighter(createHighlighter);

export const codeToHtml = async ({ code, language }) => {

  const highlighter = await getHighlighter({
    themes: ['github-light', 'github-dark'],
    langs: [
      ...Object.keys(bundledLanguages),
      {
        id: 'antlers',
        scopeName: 'text.html.statamic',
        embeddedLangs: ['html'],
        ...antlers,
      },
    ],
  });

  return highlighter.codeToHtml(code, {
    lang: lang,
    themes: {
      dark: 'github-dark',
      light: 'github-light',
    },
  });
};

Let's break this down:

  1. We import the web bundle from Shiki to minimize the load. If you need support for additional languages, you can import the full bundle without worrying about client-side load since this occurs server-side.

  2. We use the getHighlighter function from Shiki to configure our highlighter with additional features before performing the syntax highlighting.

  3. We've set up two themes for light and dark modes. If your site doesn't flip between modes, you can just stick with one theme.

  4. Shiki has built-in support for a lot of themes. Feel free to check the list.

const highlighter = await getHighlighter({
	theme: 'github-dark',
});

Custom Languages with Shiki

We also include support for custom languages. For example, we've added Antlers for our Statamic ( Hey Statamic friends ๐Ÿ‘‹ ) audience. We sourced Antlers from this repository.

Implementing Syntax Highlighting with Shiki

At the bottom of our JavaScript helper, we export the highlighter and the codeToHtml function, which accepts our language and themes and returns syntax-highlighted HTML.

return highlighter.codeToHtml(code, {
    lang: lang,
    themes: {
      dark: 'github-dark',
      light: 'github-light',
    },
});

React Server Component for Syntax Highlighting

With our helper function ready, let's integrate it into our React Server Component:

import { codeToHtml } from '@/utils/shiki';

export default async function Code({ code, language }) {

  const html = await codeToHtml({
    code,
    language,
  });

  return <div className="px-5" dangerouslySetInnerHTML={{ __html: html }} />;
}

Adding the Necessary CSS for Dual Theme Support

With our component and syntax highlighting in place, we can enhance the user experience by incorporating CSS styles that respect the user's theme preferences. This step is optional but recommended if your site supports light and dark modes.

To implement theme-based rendering, you'll need to have TailwindCSS configured with darkMode set to class.

Here's an example of how you can define your CSS to switch between themes:

html.dark .shiki,
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
  /* Optional, if you also want font styles */
  font-style: var(--shiki-dark-font-style) !important;
  font-weight: var(--shiki-dark-font-weight) !important;
  text-decoration: var(--shiki-dark-text-decoration) !important;
}

/* Optional, for the code block we add overflow, borders and padding */
.shiki {
  @apply overflow-x-auto rounded-xl p-5;
}

By adding these styles to your CSS file and ensuring that your site's HTML includes the appropriate classes, you can provide a seamless and visually appealing experience for users, whether they prefer light or dark mode.

Remember to test your styles to ensure that they switch correctly based on the user's preference and that the syntax highlighting remains legible and attractive in both themes.

Conclusion

And there you have it! We've successfully set up beautiful, syntax-highlighted code blocks without shipping any extra JavaScript to our readers. Enjoy the seamless integration of Shiki, React Server Components, and Next.js in your projects.


Bring Your Ideas to Life ๐Ÿš€

If you need help with a Laravel project let's get in touch.

Lucky Media is proud to be recognized as a Top Next.js Development Agency

lokman musliu
Lokman Musliu

Founder and CEO of Lucky Media

Technologies:

Next.js
React
Heading Pattern

Related Posts

Stay up to date

Be updated with all news, products and tips we share!