Recently, I decided to add a snippets section to my website. I realized that two key features would greatly improve the user experience:
- Better syntax highlighting
- Copy code block button
Let’s walk through how to implement these features.
Implement syntax highlighting
First, let’s look at the current setup. We use MDX and next-mdx-remote to render our content. The following is the basic architecture of MDX processing:
const source = fs.readFileSync(contentPath);
const = matter(source);
const mdxSource = await serialize(content, {
mdxOptions: ,
scope: data,
});
This code reads the content file, processes it using various plug-ins, and prepares it for rendering. Our key plugin for syntax highlighting is rehypePrettyCode. rehype-pretty-code is a Rehype plugin powered by shiki syntax highlighter that provides pretty code blocks for Markdown or MDX. It can be run either on the server at build time (to avoid runtime syntax highlighting) or on the client for dynamic highlighting.
To use a specific theme we can configure shikiOptions like this:
const shikiOptions = {
theme: "catppuccin-latte",
};
I’m using the “catppuccin-latte” theme, but you can explore more themes at https://shiki.style/themes.
Add a copy button to the code section
Now that we can use syntax highlighting, let’s add a copy button to the code block. Instead of creating new custom components for each code block in the MDX file, we will modify how the code blocks are rendered on the UI. Here’s how a block of code typically looks in the DOM:
We will create a custom graphics symbol that MDXRemote will use to render these elements. This method does not require importing components in each MDX file.
const Figure = (props) => {
const { children, ...rest } = props;
const figureRef = useRef(null);
const isReactElement = (node) => {
return React.isValidElement(node);
};
const childArray = React.Children.toArray(children);
const figCaptionChild = childArray.find(
(node) => isReactElement(node) && node.type === "figcaption"
);
const preChild = childArray.find(
(node) => isReactElement(node) && node.type === "pre"
);
const handleCopyClick = async () => {
const codeBlock = figureRef.current;
if (codeBlock) {
const codeNode = codeBlock.querySelector("code");
if (codeNode) {
navigator.clipboard.writeText(codeNode.textContent || "");
}
}
};
return (
<figure ref={figureRef} {...rest}>
{figCaptionChild && React.isValidElement(figCaptionChild) ? (
<FigureCaption
{...figCaptionChild.props}
handleCopyClick={handleCopyClick}
/>
) : null}
{preChild}
figure>
);
};
This component does the following:
- Find Figcaption and pre elements among their children
- Implement a handleCopyClick function to copy the code content
- Render custom FigureCaption element using copy button
const FigureCaption = ({ children, handleCopyClick, ...rest }) => {
const [isCopied, setIsCopied] = useState(false);
const onClick = () => {
setIsCopied(true);
handleCopyClick();
setTimeout(() => setIsCopied(false), 1000);
};
return (
<figcaption {...rest} className="flex items-center justify-between">
{children}
<button type="button" onClick={onClick}>
{isCopied ? <CopiedSVG /> : <CopySVG />}
button>
figcaption>
);
};
usage
import { MDXRemote } from "next-mdx-remote";
import Figure from "./Figure";
const MDXComponents = {
// ...other components
figure: Figure,
};
const RenderContent = () => {
return <MDXRemote {...snippet.source} components={MDXComponents} />;
};
The final rendered code block will look like this.