If you're writing about code, your blog probably needs...
- Syntax highlighting so your code blocks are easier for readers to parse
- Line numbers so you can reference the code you're discussing
- Line highlighting so you can draw attention to specific parts of code
- Block titles so you can add additional context
- You'd probably want colors that matched your website, right? What if you could use absolutely any VS Code theme?
- Oh, and you'll need light and dark mode of course, because of reasons
- And wouldn't it be great if you could do this without any runtime performance costs?
Sound good? Let's get started.
Example
Here is an example of what we will be creating today:
import { allPosts, type Post } from "contentlayer/generated"
import { type GetStaticProps, type InferGetStaticPropsType } from "next"
export const getStaticPaths = () => ({
paths: allPosts.map((post) => ({ params: { slug: post.slug } })),
fallback: false,
})
export const getStaticProps: GetStaticProps<{
post: Post
}> = ({ params }) => ({
props: { post: allPosts.find((post) => post.slug === params?.slug) },
})
export default function SinglePostPage(
props: InferGetStaticPropsType<typeof getStaticProps>,
) {
return (
<div>
<h1>{props.post.title}</h1>
</div>
)
}
How It Works
As discussed in a previous post, we're using Contentlayer to integrate MDX and manage our content. Today we'll use the Pretty Code plugin to add syntax highlighting to code blocks in our markdown posts.
Here is how it works:
- At build time, Contentlayer transforms our markdown content into JSX. We can intercept this process and apply additional transformations using plugins.
- The Pretty Code plugin searches markdown for code blocks, leverages VS Codes syntax highlighting engine to evaluate the code, and adds an inline style to each token based on the selected VS Code theme.
- React and Next.js render the JSX and generate a static HTML version, ready to be served to the user.
Before syntax highlighting:
const multiply = (a, b) => a * b
After syntax highlighting:
const multiply = (a, b) => a * b
The code block above generates the following HTML:
<div>
<pre>
<code>
<span class="line">
<span style="color:#C678DD">const</span>
<span style="color:#ABB2BF"> </span>
<span style="color:#61AFEF">multiply</span>
<span style="color:#ABB2BF"> </span>
<span style="color:#56B6C2">=</span>
<span style="color:#ABB2BF"> (</span>
<span style="color:#E06C75;font-style:italic">a</span>
<span style="color:#ABB2BF">, </span>
<span style="color:#E06C75;font-style:italic">b</span>
<span style="color:#ABB2BF">) </span>
<span style="color:#C678DD">=></span>
<span style="color:#ABB2BF"> </span>
<span style="color:#E06C75">a</span>
<span style="color:#ABB2BF"> </span>
<span style="color:#56B6C2">*</span>
<span style="color:#ABB2BF"> </span>
<span style="color:#E06C75">b</span>
</span>
</code>
</pre>
</div>
Why Is This a Big Deal?
- Leveraging VS Code's ecosystem means highly accurate syntax highlighting and access to the entire VS Code theme catalog.
- Generating the exact syntax colors at build-time means no runtime performance cost because there isn't any additional JavaScript or large CSS files sent to the client.
- Methods that evaluate syntax and generate styles at runtime increase the JavaScript client-side bundle, produce flashes of unstyled code, and slow down the user's device.
- Methods that use static CSS files either produce large files because they have to account for every possible language or have inaccurate highlighting because they rely on token pattern matching.
Install Pretty Code
- Install Pretty Code and its dependencies
npm install rehype-pretty-code shiki
Setup Pretty Code and Contentlayer
- Add Pretty Code to the list of rehype plugins for MDX files.
import { makeSource } from "contentlayer/source-files"
import rehypePrettyCode from "rehype-pretty-code"
import { Post } from "./content/definitions/Post"
export default makeSource({
contentDirPath: "content",
documentTypes: [Post],
mdx: {
rehypePlugins: [rehypePrettyCode],
},
})
Create a Code Block
Let's create a post to preview our progress so far.
# Code Block
This is my first code block:
```js
const multiply = (a, b) => a * b
```
- Create a fenced code block in markdown by wrapping code in three backticks
- Add a language annotation e.g.
js
ormd
to the code block to inform Pretty Code what syntax to use for highlighting
Customize Theme
We can customize the VS Code theme used to highlight our code.
import { type Options } from "rehype-pretty-code"
import vercelLightTheme from "./lib/themes/vercel-light.json"
export const rehypePrettyCodeOptions: Partial<Options> = {
// use a prepackaged theme
theme: "one-dark-pro",
// or import a custom theme
theme: JSON.parse(vercelLightTheme),
}
- Create a new file to store our Pretty Code customizations
- Select a prepackaged theme (6), or import a JSON theme definition (2, 8)
import { makeSource } from "contentlayer/source-files"
import rehypePrettyCode from "rehype-pretty-code"
import { Post } from "./content/definitions/Post"
+ import { rehypePrettyCodeOptions } from "./lib/rehypePrettyCode"
export default makeSource({
contentDirPath: "content",
documentTypes: [Post],
mdx: {
rehypePlugins: [
- rehypePrettyCode
+ [rehypePrettyCode, rehypePrettyCodeOptions]
],
},
})
- To pass our configuration into the pretty code plugin, we can change the plugin definition to an array, where the first item is the plugin definition and the second is the options object.
Customizing Code Blocks
Using Annotations
We can enable features per code block using annotations. For example, we can set the language, enable line numbers, add a title, and highlight lines.
The following annotations:
```js showLineNumbers title="multiply.js" {3}
const multiply = (a, b) => a * b
multiply(2, 2) // 4
```
Become:
const multiply = (a, b) => a * b
multiply(2, 2) // 4
Using Styling
Outside of the syntax highlighting itself, Pretty Code doesn't come with any styling. We can use the data attributes it adds to code blocks to style elements.
The HTML structure and data attributes can be found by inspecting a code block in your browser's developers tools:
<div data-rehype-pretty-code-fragment>
<pre data-language="js" data-theme="default">
<code data-language="js" data-theme="default">
<span class="line">
<span style="color:#C678DD">a</span>
<span style="color:#ABB2BF">b</span>
<span style="color:#61AFEF">c</span>
<!-- [...] -->
</span>
</code>
</pre>
</div>
Create a new css file:
div[data-rehype-pretty-code-fragment] {
/* ... */
}
And import it in your main Tailwind file:
@tailwind base;
@import "./syntax-highlighting.css";
@tailwind components;
@tailwind utilities;
Code Block Container
Let's start by creating a styled wrapper for our code blocks.
div[data-rehype-pretty-code-fragment] {
overflow: hidden;
/* stylist preferences */
background-color: rgb(255 255 255 / 0.1);
border-radius: 0.5rem;
}
div[data-rehype-pretty-code-fragment] pre {
overflow-x: auto;
/* stylist preferences */
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-size: 0.875rem;
line-height: 1.5rem;
}
- Hide overflow (2) and enable horizontal scrolling (10) for long lines of code
Block Titles
We can add title annotations to our code blocks using the title="..."
syntax:
```js title="multiply.js"
const multiply = (a, b) => a * b
```
And we can style those titles using the data-rehype-pretty-code-title
data attribute:
div[data-rehype-pretty-code-title] {
/* stylistic preferences */
margin-bottom: 0.125rem;
border-radius: 0.375rem;
background-color: rgb(255 228 230 / 0.1);
padding-left: 0.75rem;
padding-right: 0.75rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.75rem;
line-height: 1rem;
color: rgb(255 228 230 / 0.7);
}
Lines
Pretty Code adds the .line
class to each line of code. We can use this to add styling to our lines.
div[data-rehype-pretty-code-fragment] .line {
/* stylistic preferences */
padding-left: 0.75rem;
padding-right: 0.75rem;
}
Line Highlights
We can highlight different lines using the {<line number>}
annotation e.g. {3}
. Multiple lines and ranges are supported e.g {3,4-8}
.
```js title="multiply.js"
const multiply = (a, b) => a * b
```
We can change our .line
styles to accommodation highlighted lines:
div[data-rehype-pretty-code-fragment] .line {
/* stylistic preferences */
padding-left: 0.5rem
padding-right: 0.75rem;
border-left-width: 4px;
border-left-color: transparent;
}
div[data-rehype-pretty-code-fragment] .line--highlighted {
border-left-color: rgb(253 164 175 / 0.7);
background-color: rgb(254 205 211 / 0.1);
}
- (6,7): Add a left border with a transparent border to every line.
- (3): Reduce the left padding to account for the "invisible" border
- (11,12): Make highlighted lines stand out by adding a background color and a border color to the "invisible" line border.
While Pretty Code automatically adds a .line
class to each line of code, it doesn't automatically add classes for highlighted lines. We can push line--highlighted
to the list of line classes using the onVisitHighlightedLine()
method.
import { type Options } from "rehype-pretty-code"
export const rehypePrettyCodeOptions: Partial<Options> = {
theme: "one-dark-pro",
onVisitHighlightedLine(node) {
node.properties.className.push("line--highlighted")
},
}
If you preview your changes you will notice the highlighted line doesn't span the full width of the code block. This is because lines are wrapped in a span
(inline) and not a div
(block). I'd imagine this is not to interfere with the intrinsic indentation of pre
elements).
Converting the code
element to a grid layout ensures the spans fill the full width of a horizontally-scrollable code block.
div[data-rehype-pretty-code-fragment] code {
display: grid;
}
Line Numbers
Similarly, we can enable line numbers in a code block using showLineNumbers
:
```js title="multiply.js" showLineNumbers
const multiply = (a, b) => a * b
```
However, this simply adds a data attribute to indicate the option should be enabled and doesn't add any HTML elements to the code block.
Instead, we can make clever use of the special counter()
CSS function to add line numbers to our code blocks.
code[data-line-numbers] {
counter-reset: lineNumber;
}
code[data-line-numbers] .line::before {
counter-increment: lineNumber;
content: counter(lineNumber);
display: inline-block;
text-align: right;
/* stylistic preferences */
margin-right: 0.75rem;
width: 1rem;
color: rgb(255 255 255 / 0.2);
}
- (5): Add a
:before
pseudo element to house our line numbers - (6): Increment the
lineNumber
counter by 1 for every instance of the elements - (7): Populate the contents of the pseudo element with the current
lineNumber
value - (2): Reset the
lineNumber
counter each instance of a code block
Dark and Light Mode
There is a performance compromise with supporting theme switching. We need to generate and send two themes for every code block because syntax highlighting is generated at build-time, and theme switching happens on the client at run-time.
First, change the theme option from a value to an object with dark and light options:
import { type Options } from "rehype-pretty-code"
export const rehypePrettyCodeOptions: Partial<Options> = {
theme: {
dark: "one-dark-pro",
light: "solarized-light",
},
}
Then use CSS to show and hide the light or dark theme depending on the user's selection.
pre[data-theme="dark"] {
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
pre[data-theme="light"] {
display: none;
}
}
@media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) {
pre[data-theme="dark"] {
display: none;
}
}
We will discuss adding the actual functionality of a theme switcher in a future post.
Closing Thoughts
...