Inhaltsverzeichnis
- Verwendeter Stack
- Neue Komponente für Syntax-Hervorhebung in meinen Blogbeiträgen
- Integration der neuen Komponente in die Contentful-Richtext-Komponente
- Übergeben des Codeblocks (Inhalt) an die neue Komponente
- Anzeigen des Codeblocks mit neuer Syntax-Hervorhebung
- Inhaltsverwaltung für Syntax-Hervorhebung in Contentful
- Cloudapp-dev und bevor Sie uns verlassen
Jetzt werde ich die Komponente, die ich im vorherigen Post erstellt habe, in Kombination mit dem Headless CMS Contentful wiederverwenden. So kann ich meine Inhalte in Contentful erstellen und Next.js 14 für die Visualisierung verwenden, was mein Leben sehr viel einfacher macht.
Hier ist das GitHub Repo mit dem gesamten Code und darunter der Link zur Beispielwebsite.
Example page hosted on Vercel -> https://nextjs14-contentful-syntax-highlighting.vercel.app/
Verwendeter Stack
Ich werde mit meinem Standard-Stack beginnen:
Next.js 14 als Web-Framework, und ich werde die mitgelieferte Middleware Edge-Funktion verwenden
NPM-Paket Shiki für die Syntaxhervorhebung und das entsprechende Transformers-Paket
Contentful CMS (Kostenloses Abo)
Vercel für das Hosting
Neue Komponente für Syntax-Hervorhebung in meinen Blogbeiträgen
Ich habe den Code aus der alten Komponente (//src/components/tools/syntax-highlight/syntax-highlight.component.tsx) kopiert, die ich auf der Startseite verwendet habe.
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>
);
}Da ich die Komponente für die Blogbeiträge neu gestalten möchte, habe ich eine neue Datei //src/components/tools/syntax-highlight/syntax-highlightPost.component.tsx mit diesem Code erstellt.
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 der neuen Komponente in die Contentful-Richtext-Komponente
Wenn ich damit fertig bin, importiere ich die neu erstellte Komponente in meine Haupt-Contentful-Rich-Text-Komponente unter //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>
);
};Übergeben des Codeblocks (Inhalt) an die neue Komponente
Die komplette Magie passiert im Block [MARKS.CODE]
[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 der Variablen
let showCodeText = text.toString() || "";Anzeigen des Codeblocks mit neuer Syntax-Hervorhebung
<SyntaxHighlightPost code={showCodeText} lang="typescript" />
Sie können auch viele Kommentare sehen. Diese Codeabschnitte wurden zuvor für den alten Codeblock verwendet.

Inhaltsverwaltung für Syntax-Hervorhebung in Contentful
Unten sehen Sie einen Inhaltsdatensatz vom Typ „page — Blog post“, bei dem ich den entsprechenden Inhaltsblock als „Code“ im Rich-Text-Feld „Inhalt“ markiere. Am Ende jeder Zeile, die speziell formatiert werden soll, füge ich die richtigen Schlüsselwörter aus dem Shiki-Paket ein. Das war's, und jetzt haben wir ein großartiges CMS kombiniert mit einer ansprechend aussehenden Ausgabe auf der Frontend-Seite, die von next.js 14 verarbeitet wird.

Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:



