Note: This post is not finished. I sometimes publish early in case it will be helpful to someone.
If you're writing about code, your blog probably needs...
Sound good? Let's get started.
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>
)
}
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:
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>
npm install rehype-pretty-code shiki
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],
},
})
Let's create a post to preview our progress so far.
If you're using VS Code to author your post, you may notice you're code blocks are automatically highlighted when you add the language annotation. Also, Prettier formats them on save. Pretty cool!
# Code Block
This is my first code block:
```js
const multiply = (a, b) => a * b
```
js
or md
to the code block to inform Pretty Code what syntax to use for highlightingWe 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),
}
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]
],
},
})
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
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;
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;
}
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);
}
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;
}
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);
}
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;
}
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);
}
:before
pseudo element to house our line numberslineNumber
counter by 1 for every instance of the elementslineNumber
valuelineNumber
counter each instance of a code blockThere 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.
Another way to add theme switching involves adding global CSS variables to a page and using a special theme that sets CSS variables instead of hard-coded colors. I like the idea of this approach, but right now the theme only supports a few tokens, making it much less granular than most VSCode themes.
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.
...