Inhaltsverzeichnis
- Hier alle Dateien, welche hinzugefügt bzw. geändert wurden.
- Code Block hervorheben und Kopierfunktion
- Komponente ArticleCodeCopy.tsx
- H2-Überschriften werden für das Inhaltsverzeichnis verwendet.
- Als Code formatierte Textteile, werden hervorgehoben und der Kopierbutton erscheint.
- Cloudapp-dev und bevor Sie uns verlassen
Das Strukturieren Ihrer Inhalte ist sowohl für Ihre Nutzer als auch für Suchmaschinen großartig, führt aber manchmal zu viel Arbeit für die Autoren/Redakteure. Daher möchte ich Ihnen eine einfache und automatische Möglichkeit zeigen, ein Inhaltsverzeichnis und eine Hervorhebung von Codeblöcken zu erstellen. Diese Schritt-für-Schritt-Anleitung wird drei neue Komponenten hinzufügen. Wir werden Contentful als CMS verwenden, Sie können aber jedes CMS verwenden, das JSON-Daten bereitstellt
Der komplette Code ist in diesem Github repo verfügbar.
Hier ein Spoiler zur fertigen Webseite -> https://nextjs14-toc-codeblock-highlight-copy.vercel.app/testblogpost3
Der Link zeigt direkt auf den Blogeintrag "testblogpost3", welcher die durchgeführten Änderungen zeigt.
I will showcase how quickly and easily editors can automate certain parts of their work.
Hier alle Dateien, welche hinzugefügt bzw. geändert wurden.

Starten wir mit der TOC Komponente ArticleToc.tsx. Wie Sie sehen kommt die gesamte Magie von Tailwind und es wurden keine anderen Logiken verwendet.
"use client";
import { useTranslation } from "@/app/i18n/client";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useParams } from "next/navigation";
import Link from "next/link";
export const Toc = ({ headings }) => {
const locale = useParams()?.locale as LocaleTypes;
const { t } = useTranslation(locale, "common");
if (headings.length === 0) {
return null;
}
return (
<div>
<details className="mb-10 rounded-xl bg-emerald-700/10 dark:bg-white/3">
<summary className="flex dark:text-[#FAFAFA] text-gray700 items-center px-6 py-3 text-sm tracking-wide uppercase list-none select-none opacity-60">
{t("article.toc")}
</summary>
<div className="p-6 pt-0">
<ul className="space-y-2 list-disc list-inside">
{headings.map((heading: string, index: number) => (
<li key={index}>
<Link
className="text-base no-underline text-emerald-600 dark:text-emerald-400"
href={`#${heading}`}
>
{heading}
</Link>
</li>
))}
</ul>
</div>
</details>
</div>
);
};Die nächste Komponente ist die ArticleTocItem.tsx, welche recht einfach aufgebaut ist und für das Setzen der Anchor-id gebraucht wird. Sobald jemand im Inhaltsverzeichnis (TOC) auf einen Eintrag klickt, möchten wir direkt zur entsprechenden H2-Überschrift springen.
interface ArticleTocItemProps {
dynamicId: string;
heading: string;
}
export const ArticleTocItem: React.FC<ArticleTocItemProps> = ({
dynamicId,
heading,
}) => {
return (
<h2 id={dynamicId} className="text-2xl font-semibold dark:text-white">
{heading}
</h2>
);
};Beide Komponenten werden nun in die Komponente CtfRichText.component.tsx importiert, da diese für die Anzeige der Daten im Frontend verantwortlich ist.
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";
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 (
<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}
{/* {text.toString().replace(markedfilename, "")} */}
</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>
);
};Im unten angeführten Codesnippet, setzen wir die Anchor-id, idem wir die Komponente ArticleTocItem verwenden.
[BLOCKS.HEADING_2]: (node, children: any) => {
return <ArticleTocItem dynamicId={children[0]} heading={children[0]} />;
}At the end of the CtfRichText.component.tsx we loop through the json.content to create an array, which we use to show our TOC at the beginning of the blog post.
Gegen Ende der Datei CtfRichText.component.tsx iterieren wir über die "json.content", damit wir ein Array erstellen können, welches wir für die Darstellung des Inhaltsverzeichnisses am Anfang benötigen.
jsoncontent.map((item: any) => {
if (item.nodeType === "heading-2") {
const headingvalue: any = item.content[0].value;
headings.push(headingvalue);
}
});{source === "article" && <Toc headings={headings} />}Code Block hervorheben und Kopierfunktion
Wie Sie vielleicht bereits gesehen haben, importieren wir die Komponente CopyButton in die Hauptkomponente CtfRichText.component.tsx.
import { CopyButton } from "@/components/contentful/ArticleCodeCopy";In der MARKS.CODE Sektion, benutzen wir die Copybutton Komponente, damit wir die Codeteile hervorheben und einen Kopierbutton anzeigen können.
[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 (
<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}
{/* {text.toString().replace(markedfilename, "")} */}
</code>
</pre>
);
},Komponente ArticleCodeCopy.tsx
"use client";
import { useState } from "react";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";
import { useParams } from "next/navigation";
interface CopyButtonProps {
text: string;
}
export const CopyButton = ({ text }: CopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const locale = useParams()?.locale as LocaleTypes;
const { t } = useTranslation(locale, "common");
const copy = async () => {
await navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 10000);
};
return (
<div className="float-right px-2 py-1 text-base bg-gray-600 rounded">
<button disabled={isCopied} onClick={copy}>
{isCopied ? t("copybutton.afterclick")! : t("copybutton.beforeclick")}
</button>
</div>
);
};Im Contentfuleditor (RichTextField), muss der entsprechende Inhalt als "Code" formatiert werden, damit die Logik funktioniert.
H2-Überschriften werden für das Inhaltsverzeichnis verwendet.

Als Code formatierte Textteile, werden hervorgehoben und der Kopierbutton erscheint.

Als letzten Schritt, fügen wir die neuen Übersetzungen in den jeweiligen common.json Dateien hinzug und dann sind wir fertig.
Wie Sie sehen, ist der Aufbau der verwendeten Komponenten recht einfach und kann Mithilfe von Tailwindcss schnell implementiert werden.
Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:
Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.




