Recently, I decided to add a code snippets section to my website. I realized two key features would greatly improve the user experience:
- Better syntax highlighting
- A copy code block button
Let's walk through how to implement these features step by step.
Implementing Syntax Highlighting
First, let's look at our current setup. We're using MDX with next-mdx-remote to render our content. Here's the basic structure of our MDX processing:
const source = fs.readFileSync(contentPath);
const { data, content } = matter(source);
const mdxSource = await serialize(content, {
mdxOptions: {
format: "mdx",
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { properties: { className: ["anchor"] } }],
[rehypePrettyCode, shikiOptions],
rehypeAccessibleEmojis,
],
},
scope: data,
});
This code reads the content file, processes it with various plugins, and prepares it for rendering. The key plugin for our syntax highlighting is rehypePrettyCode. rehype-pretty-code is a Rehype plugin powered by the shiki syntax highlighter that provides beautiful code blocks for Markdown or MDX. It works on both the server at build-time (avoiding runtime syntax highlighting) and 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.
Adding copy button to codeblocks
Now that we have syntax highlighting working, let's add a copy button to our code blocks. Instead of creating a new custom component for each code block in our MDX files, we'll modify how the code blocks are rendered on the UI. Here's how a code block typically looks in the DOM:
We'll create a custom Figure component that will be used by MDXRemote to render these elements. This approach doesn't require importing the component 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:
- Finds the figcaption and pre elements among its children
- Implements a handleCopyClick function to copy the code content
- Renders a custom FigureCaption component with the 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>
);
};
Usages
import { MDXRemote } from "next-mdx-remote";
import Figure from "./Figure";
const MDXComponents = {
// ...other components
figure: Figure,
};
const RenderContent = () => {
return <MDXRemote {...snippet.source} components={MDXComponents} />;
};
Finally rendered codeblock will look like this.
Top comments (0)