Table of Contents
- Used Stack
- New Component for Syntax Highlighting in My Blog Posts
- Integration of new component into Contentful Richtext Component
- Passing Codeblock to the new Component
- Showing Codeblock with new syntax highlighting
- Content Management for Syntax Highlighting in Contentful
- Cloudapp-dev, and before you leave us
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/
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.

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.

Cloudapp-dev, and before you leave us
Thank you for reading until the end. Before you go:



