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

Next.js 14 / Contentful- Professionelle Syntaxhervorhebung

15. Juli 2024
Inhaltsverzeichnis

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/

Direktlink zum Codeblock

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.

Website-Codeblock
Website-Codeblock

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.

Contentful-Codeblock
Contentful-Codeblock

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.

Oder folgen Sie uns auf Twitter -> Cloudapp.dev

Verwandte Artikel