Syntax highlight with Shiki and Adding Copy Functionality to Codeblocks
December 22, 2024

Syntax highlight with Shiki and Adding Copy Functionality to Codeblocks

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,
});
Enter full screen mode

Exit full screen mode

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",
};
Enter full screen mode

Exit full screen mode

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>
  );
};
Enter full screen mode

Exit full screen mode

This component does the following:

  1. Find Figcaption and pre elements among their children
  2. Implement a handleCopyClick function to copy the code content
  3. 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>
  );
};
Enter full screen mode

Exit full screen mode


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} />;
};
Enter full screen mode

Exit full screen mode

The final rendered code block will look like this.

Try the demo here

2024-12-22 06:00:36

Leave a Reply

Your email address will not be published. Required fields are marked *