Code Syntax Highlighting in Headless WordPress

Francis Agulto Avatar

·

If you’re using Headless WordPress with Next.js or any other frontend framework, you might have run into a small issue when displaying code snippets: the native WordPress code block doesn’t support syntax highlighting. This can be a problem for developer-focused sites or tutorials where reading and understanding code snippets is key.

This shortcoming of the native Code block often leads developers to look for other solutions that support code syntax highlighting. For a recent headless WordPress project our team worked on, we researched a few options including the Syntax-highlighting Code Block (with Server-side Rendering) and Code Block Pro. We found Code Block Pro to offer the highest quality syntax highlighting, provide tons of features and customization options, and even support a number of popular VS Code themes to choose from, giving code snippets a nice, professional look.

In this guide, we’ll show you how to install and use Code Block Pro with a Headless WordPress setup, ensuring your code snippets are presented properly on the front end.

If you prefer the video format of this article, you can access it here:

Prerequisites

Before reading this article, you should have the following prerequisites checked off:

  • Basic knowledge of Next.js 14
  • Next.js boilerplate project that uses the App Router
  • A WordPress install
  • A basic knowledge of WPGraphQL and headless WordPress

In order to follow along step by step, you need the following:

  • A Next.js project that is connected with your WordPress backend.
  • A dynamic route that grabs a single post by its URI to display a single post detail page

If you do not have that yet and do want to follow along step by step, you can clone down my demo here: https://github.com/Fran-A-Dev/Kevin-Bacon-Code-Syntax-Highlighting-HeadlessWP/tree/main

To gain a basic understanding of Next.js, please review the Next.js docs.

If you need a WordPress install, you can use a free sandbox account on WP Engine’s Headless Platform:

Steps

Install and activate Code Block Pro

Steps to install and activate the Code Block Pro plugin:

  • Go to your WordPress admin dashboard.
  • Navigate to Plugins > Add New.
  • Search for “Code Block Pro“.
  • Click Install Now, then Activate.

You should have this:

Now that it is activated, create a new post in WordPress.  In the block editor, click the plus icon to insert a new block and select the Code Pro block.  It looks like this:

When you select it, a syntax-highlighted code block will be added.  Go ahead and paste whatever code you want in that block and then save the post.  In this case, I am going to add some jsx that renders a page.  Note that the panel on the right contains many configuration options.

I chose the “Dracula Soft” theme for this article and the header type is set to “none” to achieve a blank header.  The footer is set to “simple string start” which displays the kind of language the code is in at the bottom.  I also highlighted lines 1 and 2 to show off the line-by-line highlight feature:

That is it!  Stoked!  This is what you have to do. If simple syntax highlighting and formatting are all you need, as well as displaying the programming language in your post, you are done.  Now, let’s get this to render on your decoupled frontend.

Configure Next.js and App Router

In this article, we will use the App Router in Next.js 14. You should already have a boilerplate Next.js project spun up in the app router. Go ahead and open your Next.js project in your code editor.
The first thing we need to do is add some code to our CSS.  Navigate to your globals.css file in the app directory.  Add this CSS:

/* Line highlighting for Code Block Pro blocks */
pre.shiki {
  padding-inline: 0;
}
pre.shiki .line {
  padding-inline: 2rem;
}
pre.shiki .cbp-line-highlight {
  display: inline-block;
  width: 100%;
  background-color: rgba(255, 255, 255, 0.06);
}


Code language: CSS (css)

This css will ensure that line highlighting is properly formatted, creating a more readable and accessible presentation for longer code blocks.

Save that and run npm run dev to spin up your dev server and visit the single post detail page where you added the code.  You should have something like this:

Out of the box, the plugin with some css allows you to easily display code, highlighting, and the programming language in a nice, readable way. 

Taking it a Step Further – WPGraphQL & WPGraphQL Content Blocks

You can stop at just installing the plugin and adding the css to your frontend application to get the formatting and highlighting.  If you want to implement a copy-to-clipboard feature whereby users can click a button to copy the code within the code snippet to their clipboard, however, follow the additional steps below.

Install and activate WPGraphQL & WPGraphQL Content Blocks

WPGraphQL is a canonical WordPress plugin that provides an extendable GraphQL schema and API for any WordPress site.

WPGraphQL Content Blocks is a WordPress plugin that extends WPGraphQL to support querying (Gutenberg) block data. Let’s install both plugins.

Go to the plugins page in your WP admin and search for WPGraphQL. You can add and activate the plugin from there.

Go to the WPGraphQL Content Blocks repo and download the latest .zip version of the plugin.

Navigate to your WP install and upload the plugin .zip to your WordPress site.

Once that is done, activate the plugin.

Create an editor block query to get Code Block Pro data

Next, let’s query for the Code Block Pro data.  Head over to GraphQL IDE and paste in this query:


query GetPostsWithCodeBlocks {
  posts {
    nodes {
      title
      content
      editorBlocks {
        name
        ... on KevinbatdorfCodeBlockPro {
          attributes {
            language
            lineNumbers
            code
          }
          renderedHtml
        }
      }
    }
  }
}

Code language: JavaScript (javascript)

Now press play and you should get this response:


As shown in the IDE, this query returns all posts along with the Code Block Pro data, including the attributes we are asking for (programming language, HTML-rendered code snippet, code, line numbers, copy button, etc.).

Rendering the Code Block in Next.js

Now that we have the data, we need to render the code block in our Next.js frontend. Here’s how you can do it in Next.js 14.

Navigate to app/post/[uri]/page.jsx file.  In this file, paste this code block in:

"use client";
import { useState } from "react";
import "../../globals.css";

async function getPost(uri) {
  const query = `
  query GetPostByUri($uri: ID!) {
    post(id: $uri, idType: URI) {
      title
      editorBlocks {
        name
        ... on KevinbatdorfCodeBlockPro {
          attributes {
            language
            lineNumbers
            code
            copyButton
            copyButtonString
          }
          renderedHtml
        }
      }
    }
  }
  `;

  const variables = {
    uri,
  };

  const res = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    next: {
      revalidate: 60,
    },
    body: JSON.stringify({ query, variables }),
  });

  const responseBody = await res.json();
  return responseBody.data.post;
}

export default async function PostDetails({ params }) {
  const post = await getPost(params.uri);

  return (
    <main>
      <h1>{post.title}</h1>
      <div>
        {/* Loop through the editor blocks to render CodeBlockPro if available */}
        {post.editorBlocks.map((block, index) => {
          if (block.name === "kevinbatdorf/code-block-pro") {
            return (
              <CodeBlockDisplay
                key={index}
                attributes={block.attributes}
                renderedHtml={block.renderedHtml}
              />
            );
          }
          return null;
        })}
      </div>
    </main>
  );
}

// CodeBlockDisplay inline function to display code block
function CodeBlockDisplay({ attributes, renderedHtml }) {
  const [copied, setCopied] = useState(false);

  // Handle copy button functionality
  const handleCopy = async () => {
    try {
      if (navigator && navigator.clipboard) {
        console.log("Copying the text:", attributes.code);
        await navigator.clipboard.writeText(attributes.code);
        setCopied(true);
        setTimeout(() => setCopied(false), 3000); // Reset after 3 seconds
      } else {
        console.error("Clipboard API not available.");
      }
    } catch (err) {
      console.error("Failed to copy: ", err);
    }
  };

  return (
    <div className="code-block-container">
      {/* Render the HTML of the code block */}
      <div dangerouslySetInnerHTML={{ __html: renderedHtml }} />

      {/* Show the copy button */}
      {attributes.copyButton && (
        <button onClick={handleCopy} className="copy-button" title="Copy">
          {copied ? <CheckMarkIcon /> : <CopyIcon />}
        </button>
      )}

      {/* Show the language at the bottom */}
      {attributes.language && (
        <div className="language-label-right">
          {attributes.language}
          <span className="language-label">{attributes.language}</span>
        </div>
      )}
    </div>
  );
}

function CheckMarkIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      strokeWidth={1.5}
      stroke="currentColor"
      className="h-6 w-6 text-gray-400"
    >
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
      />
    </svg>
  );
}

function CopyIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 64 64"
      strokeWidth={5}
      stroke="currentColor"
      className="h-6 w-6 text-gray-500 hover:text-gray-400"
    >
      <rect x="11.13" y="17.72" width="33.92" height="36.85" rx="2.5" />
      <path d="M19.35,14.23V13.09a3.51,3.51,0,0,1,3.33-3.66H49.54a3.51,3.51,0,0,1,3.33,3.66V42.62a3.51,3.51,0,0,1-3.33,3.66H48.39" />
    </svg>
  );
}

Code language: JavaScript (javascript)

This is a big code block, so let’s break it down into sections. 

Starting at the top, we have our “use client” directive since we are importing and using the useState hook in React which needs to run on the client.  Then we have our globals.css file for the styling:

"use client";
import { useState } from "react";
import "../../globals.css";
Code language: JavaScript (javascript)

Following that, we have our WPGraphQL query to fetch the post and code block data:

async function getPost(uri) {
  const query = `
    query GetPostByUri($uri: ID!) {
      post(id: $uri, idType: URI) {
        title
        editorBlocks {
          name
          ... on KevinbatdorfCodeBlockPro {
            attributes {
              language
              lineNumbers
              code
              copyButton
              copyButtonString
            }
            renderedHtml
          }
        }
      }
    }
  `;

  const variables = { uri };
  const res = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    next: { revalidate: 60 },
    body: JSON.stringify({ query, variables }),
  });

  const responseBody = await res.json();
  return responseBody.data.post;
}



Code language: JavaScript (javascript)

This function defines a GraphQL query to fetch post data, specifically targeting the KevinbatdorfCodeBlockPro block that contains attributes such as language, code, and the copyButton.

The uri (Unique Resource Identifier) is passed into the GraphQL query to fetch the correct post.

The query is then sent to the GraphQL API using fetch, with headers defining the request as POST and the body as JSON. The response is parsed as JSON, and the post data is returned for rendering.

Next, we have our PostDetails component:

export default async function PostDetails({ params }) {
  const post = await getPost(params.uri);

  return (
    <main>
      <h1>{post.title}</h1>
      <div>
        {post.editorBlocks.map((block, index) => {
          if (block.name === "kevinbatdorf/code-block-pro") {
            return (
              <CodeBlockDisplay
                key={index}
                attributes={block.attributes}
                renderedHtml={block.renderedHtml}
              />
            );
          }
          return null;
        })}
      </div>
    </main>
  );
}

Code language: JavaScript (javascript)

The PostDetails component fetches the post data using the getPost function, passing the post uri as a parameter.  Following that, the post title is displayed using a simple <h1> tag.

Then editorBlocks field is mapped over, and the function checks if the block name is "kevinbatdorf/code-block-pro". If it is, it renders the CodeBlockDisplay component, passing in the block’s attributes and HTML.

The next part is the CodeBlockDisplay component:

function CodeBlockDisplay({ attributes, renderedHtml }) {
  const [copied, setCopied] = useState(false);

  const handleCopy = async () => {
    try {
      if (navigator && navigator.clipboard) {
        console.log("Copying the text:", attributes.code);
        await navigator.clipboard.writeText(attributes.code);
        setCopied(true);
        setTimeout(() => setCopied(false), 3000);
      } else {
        console.error("Clipboard API not available.");
      }
    } catch (err) {
      console.error("Failed to copy: ", err);
    }
  };

  return (
    <div className="code-block-container">
      <div dangerouslySetInnerHTML={{ __html: renderedHtml }} />
      {attributes.copyButton && (
        <button onClick={handleCopy} className="copy-button" title="Copy">
          {copied ? <CheckMarkIcon /> : <CopyIcon />}
        </button>
      )}
      {attributes.language && (
        <div className="language-label-right">
          {attributes.language}
          <span className="language-label">{attributes.language}</span>
        </div>
      )}
    </div>
  );
}



Code language: JavaScript (javascript)

The component uses useState to manage whether the copy button was clicked. This means that after the copy action is triggered, the "Copied!" message will be displayed for 3 seconds before resetting back to the original "Copy" button state.

After that, the handleCopy function uses the Clipboard API to copy the code (contained in attributes.code) to the user’s clipboard. It checks if the API is available and logs an error if not.

The rendered HTML of the code block is then injected into the DOM using dangerouslySetInnerHTML, which is necessary because the content comes from WordPress in HTML format. If the block has a copyButton attribute, the copy button is conditionally displayed. Additionally, the programming language is displayed at the bottom of the block if it’s available.

The last thing we need to do is include components for rendering the SVG icons:

function CheckMarkIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="h-6 w-6 text-gray-400">
      <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
    </svg>
  );
}

function CopyIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64" strokeWidth={5} stroke="currentColor" className="h-6 w-6 text-gray-500 hover:text-gray-400">
      <rect x="11.13" y="17.72" width="33.92" height="36.85" rx="2.5" />
      <path d="M19.35,14.23V13.09a3.51,3.51,0,0,1,3.33-3.66H49.54a3.51,3.51,0,0,1,3.33,3.66V42.62a3.51,3.51,0,0,1-3.33,3.66H48.39" />
    </svg>
  );
}


Code language: JavaScript (javascript)

The CopyIcon here is displayed by default. After the user clicks on the button to copy the code, that SVG is hidden and swapped out with the CheckMarkIcon to indicate that the code snippet was successfully copied.

Update globals.css file

The last thing we need to do before testing our code block page is to update the css to collectively enhance the presentation of the code block by ensuring the elements are properly spaced, visually appealing, and interactive (with the copy button).

This setup compliments the functionality provided in the JavaScript code for copying code snippets.  You can style it however you would like, but I chose to do it this way.

.code-block-container {
  position: relative;
  padding: 16px;
  background-color: #282a36; /* Dark theme for the code block */
  border-radius: 8px;
  margin-bottom: 24px;
}

.copy-button {
  position: absolute;
  top: 10px;
  right: 10px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.copy-button svg {
  height: 24px;
  width: 24px;
  color: #ccc;
  transition: color 0.3s ease;
}

.copy-button:hover svg {
  color: #fff;
}

.language-label {
  display: block;
  font-size: 12px;
  color: #bebebe;
  text-align: left;
  padding-top: 8px;
}


Code language: CSS (css)


We are ready to test this page.  Navigate to your WordPress admin and grab whatever slug is related to the post you embedded code with.  When you visit that page, you should have something that looks like this:

Now, to test the copy functionality, click on the clipboard box icon and then paste it into a document to test that it works:

Stoked!! This works!  Now let’s discuss more options and practices to use this feature.

Other Options

Here are two more options you can get stoked with in adding code syntax highlighting to your headless WordPress app.

Create A Separate Code Block Pro Component

Instead of embedding the entire code block logic directly into a single page file, it’s a good practice to create a separate component specifically for handling Code Block Pro blocks.

This approach enhances code readability, reusability, and maintainability by isolating the code block’s functionality. You can easily import this component into any page that requires the code block, such as your page.jsx file, without cluttering the page’s primary logic.  For this example, our separate component would look like this:

"use client";
import { useState } from "react";

// This component renders the Code Block Pro block with copy-to-clipboard functionality
export default function KevinBatdorfCodeBlockPro({ attributes, renderedHtml }) {
  const [copied, setCopied] = useState(false);

  // Handle the copy functionality
  const handleCopy = async () => {
    try {
      if (navigator && navigator.clipboard) {
        await navigator.clipboard.writeText(attributes.code);
        setCopied(true);
        setTimeout(() => setCopied(false), 3000); // Reset after 3 seconds
      } else {
        console.error("Clipboard API not available.");
      }
    } catch (err) {
      console.error("Failed to copy: ", err);
    }
  };

  return (
    <div className="code-block-container">
      {/* Render the HTML of the code block */}
      <div dangerouslySetInnerHTML={{ __html: renderedHtml }} />

      {/* Show the copy button */}
      {attributes.copyButton && (
        <button onClick={handleCopy} className="copy-button" title="Copy">
          {copied ? <CheckMarkIcon /> : <CopyIcon />}
        </button>
      )}

      {/* Show the language at the bottom */}
      {attributes.language && (
        <div className="language-label-right">
          {attributes.language}
          <span className="language-label">{attributes.language}</span>
        </div>
      )}
    </div>
  );
}

// CheckMarkIcon component for when the text has been copied
function CheckMarkIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      strokeWidth={1.5}
      stroke="currentColor"
      className="h-6 w-6 text-gray-400"
    >
      <path
        strokeLinecap="round"
        strokeLinejoin="round"
        d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
      />
    </svg>
  );
}

// CopyIcon component for the default copy button
function CopyIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 64 64"
      strokeWidth={5}
      stroke="currentColor"
      className="h-6 w-6 text-gray-500 hover:text-gray-400"
    >
      <rect x="11.13" y="17.72" width="33.92" height="36.85" rx="2.5" />
      <path d="M19.35,14.23V13.09a3.51,3.51,0,0,1,3.33-3.66H49.54a3.51,3.51,0,0,1,3.33,3.66V42.62a3.51,3.51,0,0,1-3.33,3.66H48.39" />
    </svg>
  );
}


Code language: JavaScript (javascript)

Since I already explained the code’s function and logic in the previous section, you can go over what it does there.

Conclusion

Syntax highlighting and copy-to-clipboard functionality are valuable enhancements to the code snippets on your headless WordPress sites.

By leveraging the Code Block Pro plugin and WPGraphQL, we were able to query and render code blocks with ease. This approach improves readability and user experience, allowing visitors to easily copy code snippets directly from your posts. The combination of server-side rendering with client-side interactivity using Next.js, along with a clean, simple styling approach, ensures that you can maintain a visually appealing and functional code block display.

As always, we’re stoked to hear your feedback and see what headless projects you’re building! Hit us up in our Discord!