Loading...
Contentful-Syntax-Highlight
Author Cloudapp
E.G.

Next.js 14 / Contentful- Professional Syntax Highlighting

July 15, 2024
Table of Contents

Now, I will reuse the component I created in the previous story combined with the headless CMS Contentful. So I can create my Content within Contentful and use Next.js 14 for the visualization, which makes my life a lot easier.

Here is the GitHub repo with the entire code. Below, you will find the link to the example page.

Example page hosted on Vercel -> https://nextjs14-contentful-syntax-highlighting.vercel.app/

Direct link to the code block

Used Stack

I will start with my default stack:

  • Next.js 14 as the web framework, and I will use the provided middleware edge function

  • NPM Package Shiki for syntax highlighting and the corresponding transformers package

  • Contentful CMS (Free Plan)

  • Vercel for hosting

New Component for Syntax Highlighting in My Blog Posts

I copied the code from the old component (//src/components/tools/syntax highlight/syntax highlight.component.tsx) that I used on the homepage.

import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki";
import {
  transformerNotationHighlight,
  transformerNotationDiff,
} from "@shikijs/transformers";
import CopyToClipboard from "./copyToClipboard.component";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
  filename?: string;
};

export default async function SyntaxHighlight({
  code,
  lang = "javascript",
  theme = "nord",
  filename,
}: Props) {
  const html = await codeToHtml(code, {
    lang,
    theme,
    transformers: [transformerNotationHighlight(), transformerNotationDiff()],
  });

  return (
    <div className="mt-4 rounded-lg bg-gradient-to-r from-sky-200 to-sky-400 p-4 !pr-0 md:p-8 lg:p-12 [&>pre]:rounded-none max-w-4xl">
      <div className="overflow-hidden rounded-s-lg">
        <div className="flex items-center justify-between bg-gradient-to-r from-neutral-900 to-neutral-800 py-2 pl-2 pr-4 text-sm">
          <span className="-mb-[calc(0.5rem+2px)] rounded-t-lg border-2 border-white/5 border-b-neutral-700 bg-neutral-800 px-4 py-2 ">
            {filename}
          </span>
          <CopyToClipboard code={code} />
        </div>
        <div
          className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
          dangerouslySetInnerHTML={{ __html: html }}
        ></div>
      </div>
    </div>
  );
}

Since I would like to restyle the component for the blog posts, I created a new file //src/components/tools/syntax highlight/syntax highlightPost.component.tsx with this code.

import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki";
import {
  transformerNotationHighlight,
  transformerNotationDiff,
} from "@shikijs/transformers";
import CopyToClipboard from "./copyToClipboard.component";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
  filename?: string;
};

export default async function SyntaxHighlightPost({
  code,
  lang = "javascript",
  theme = "nord",
  filename,
}: Props) {
  const html = await codeToHtml(code, {
    lang,
    theme,
    transformers: [transformerNotationHighlight(), transformerNotationDiff()],
  });

  return (
    <div className="mt-4 rounded-lg bg-gradient-to-r from-sky-200 to-sky-400 p-4 md:p-8 lg:p-12 [&>pre]:rounded-none ">
      <div className="overflow-hidden rounded-s-lg">
        <div className="flex items-center justify-between bg-gradient-to-r from-neutral-900 to-neutral-800 py-2 pl-2 pr-4 text-sm">
          <span className="-mb-[calc(0.5rem+2px)] px-4 py-2 "></span>
          <CopyToClipboard code={code} />
        </div>
        <div
          className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
          dangerouslySetInnerHTML={{ __html: html }}
        ></div>
      </div>
    </div>
  );
}

Integration of new component into Contentful Richtext Component

When I am done with this, I will import the newly created component into my main Contentful Rich Text Component under //src/components/CtfRichText.component.tsx

"use client";
import {
  documentToReactComponents,
  Options,
} from "@contentful/rich-text-react-renderer";
import { BLOCKS, MARKS, Document, INLINES } from "@contentful/rich-text-types";
import { ArticleImage } from "@/components/contentful/ArticleImage.component";
import { ComponentRichImage } from "@/lib/__generated/sdk";
import { CopyButton } from "@/components/contentful/ArticleCodeCopy";
import { Toc } from "@/components/contentful/ArticleToc";
import { ArticleTocItem } from "@/components/contentful/ArticleTocItem";
import { CtfPicture } from "@/components/contentful/CtfPicture.component";
import { twMerge } from "tailwind-merge";
import Link from "next/link";

import SyntaxHighlightPost from "@/components/tools/syntaxhighlight/syntaxhighlightPost.component";

export type EmbeddedEntryType = ComponentRichImage | null;

export interface ContentfulRichTextInterface {
  json: Document;
  links?:
    | {
        entries: {
          block: Array<EmbeddedEntryType>;
        };
      }
    | any;
  source: string;
}

function getFileName(text: string) {
  return text.split("#");
}

export const EmbeddedEntry = (entry: EmbeddedEntryType) => {
  switch (entry?.__typename) {
    case "ComponentRichImage":
      return <ArticleImage image={entry} />;
    default:
      return null;
  }
};

export const contentfulBaseRichTextOptions = ({
  links,
}: ContentfulRichTextInterface): Options => ({
  renderMark: {
    [MARKS.BOLD]: (text) => {
      return <b key={`${text}-key`}>{text}</b>;
    },
    [MARKS.CODE]: (text: any) => {
      let markedfilename = undefined;
      let showCodeText = text.toString() || "";
      const filename = getFileName(text.toString())[1];
      if (filename) {
        markedfilename = "#" + filename + "#";
        showCodeText = text.toString().replace(markedfilename, "");
        showCodeText = showCodeText.replace("#", "");
      }
      return (
        <>
          <SyntaxHighlightPost code={showCodeText} lang="typescript" />

          {/* <pre>
            <div className="mb-3">
              {" "}
              <CopyButton text={text} />
            </div>
            <code key={`${text}-key`}>
              {markedfilename && (
                <span className="inline-block px-1 py-1 text-base text-white bg-[#3c4f6a] rounded-lg">
                  {markedfilename}
                </span>
              )}
              {showCodeText}
           
            </code>
          </pre> */}
        </>
      );
    },
    [MARKS.ITALIC]: (text) => {
      return <i key={`${text}-key`}>{text}</i>;
    },
  },
  renderNode: {
    [INLINES.HYPERLINK]: (node, children) => {
      if (node.data.uri.includes("https://")) {
        return (
          <a
            className="text-blue-500 underline hover:text-blue-700"
            target="_blank"
            rel="noopener noreferrer"
            href={node.data.uri}
          >
            {children}
          </a>
        );
      }

      return <Link href={node.data.uri}>{children}</Link>;
    },
    [BLOCKS.HEADING_2]: (node, children: any) => {
      return <ArticleTocItem dynamicId={children[0]} heading={children[0]} />;
    },

    [BLOCKS.EMBEDDED_ENTRY]: (node) => {
      const entry = links?.entries?.block?.find(
        (item: EmbeddedEntryType) => item?.sys?.id === node.data.target.sys.id
      );

      if (!entry) return null;

      return <EmbeddedEntry {...entry} />;
    },
    [BLOCKS.EMBEDDED_ASSET]: (node) => {
      const asset = links?.assets?.block?.find(
        (item: any) => item?.sys?.id === node.data.target.sys.id
      );
      if (!asset) return null;
      return (
        <>
          <figure>
            <div className="flex justify-center">
              <CtfPicture
                nextImageProps={{
                  className: twMerge(
                    "mt-0 mb-0 ",
                    "rounded-2xl border border-gray-300 shadow-lg"
                  ),
                }}
                {...asset}
              />
              ;
            </div>
          </figure>
        </>
      );
    },
  },
});

export const CtfRichText = ({
  json,
  links,
  source,
}: ContentfulRichTextInterface) => {
  const baseOptions = contentfulBaseRichTextOptions({ links, json, source });
  if (!json) return null; // IF there is no content, return null
  const jsoncontent: any = json.content;
  const headings: string[] = [];

  jsoncontent.map((item: any) => {
    if (item.nodeType === "heading-2") {
      const headingvalue: any = item.content[0].value;
      headings.push(headingvalue);
    }
  });
  return (
    <article className="prose prose-lg max-w-none">
      {source === "article" && <Toc headings={headings} />}
      {documentToReactComponents(json, baseOptions)}
    </article>
  );
};

Passing Codeblock to the new Component

The complete magic happens in the [MARKS.CODE] Block

 [MARKS.CODE]: (text: any) => {
      let showCodeText = text.toString() || "";
      // let markedfilename = undefined;      
      // const filename = getFileName(text.toString())[1];
      // if (filename) {
      //   markedfilename = "#" + filename + "#";
      //   showCodeText = text.toString().replace(markedfilename, "");
      //   showCodeText = showCodeText.replace("#", "");
      // }
      return (
        <>
          <SyntaxHighlightPost code={showCodeText} lang="typescript" />

          {/* <pre>
            <div className="mb-3">
              {" "}
              <CopyButton text={text} />
            </div>
            <code key={`${text}-key`}>
              {markedfilename && (
                <span className="inline-block px-1 py-1 text-base text-white bg-[#3c4f6a] rounded-lg">
                  {markedfilename}
                </span>
              )}
              {showCodeText}
           
            </code>
          </pre> */}
        </>
      );
    },

Definition of variable

let showCodeText = text.toString() || "";

Showing Codeblock with new syntax highlighting

<SyntaxHighlightPost code={showCodeText} lang="typescript" />

You can also see many comments. These code pieces were used before for the old code block.

Website-Codeblock
Website-Codeblock

Content Management for Syntax Highlighting in Contentful

Below you can see a content record of type “page — Blog post” where I mark the corresponding content block as “Code” in the rich text field “Content”. At the end of each line, which should be formatted specially, I put the right keywords from the Shiki package. That’s it, and now we have a great CMS combined with a nice-looking output on the frontend side, handled by next.js 14.

Contentful-Codeblock
Contentful-Codeblock

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles