Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Next.js und Tailwind CSS
    4. Nextjs JSON LD: typisiertes Schema, das zur Seite passt
    Read in English
    1. Blog
    2. Next.js und Tailwind CSS
    3. Nextjs JSON LD: typisiertes Schema, das zur Seite passt
    Next.js und Tailwind CSS
    Next.js und Tailwind CSS

    Nextjs JSON LD: typisiertes Schema, das zur Seite passt

    SR
    Serban Rusu · Gründer von Wingo UI
    16. Okt. 2026 · 10 Min. Lesezeit

    Auf dieser Seite

    0%
    1. Wohin gehört JSON-LD im Next.js App Router?
    2. Welche Schema-Typen braucht eine Produkt-Website?
    3. Wie typisiere ich JSON-LD mit TypeScript in Next.js?
    4. Wie halte ich JSON-LD synchron mit der sichtbaren Seite?
      1. Wie baue ich Breadcrumb JSON-LD in Next.js ein?
      2. Wie baue ich FAQPage JSON-LD in Next.js ein?
    5. Wie setze ich alles auf einer Blogpost-Seite zusammen?
    6. Wie teste ich, ob das Markup zur Seite passt?
    7. Was mache ich als Nächstes?
    8. Häufige Fragen
      1. Wie füge ich JSON-LD im Next.js App Router ein?
      2. Sollte ich next/script für JSON-LD nutzen?
      3. Zeigt Google noch FAQ-Rich-Results?
      4. Kann eine Client-Komponente JSON-LD rendern?
      5. Brauche ich schema-dts, um JSON-LD in TypeScript zu typisieren?

    Kurz gesagt

    Im Next.js App Router ist JSON-LD ein einfaches Script-Tag vom Typ application/ld+json, das du aus page.tsx oder layout.tsx renderst: JSON.stringify, dann jedes < durch \u003c ersetzen. Die Metadata API hat dafür kein Feld. Schreib mit schema-dts einen typisierten Builder pro Schema-Typ (SoftwareApplication, BlogPosting, FAQPage, BreadcrumbList) und gib jedem Builder dasselbe Array, das auch die Seite rendert. So kann das Markup nie etwas beschreiben, das Leser nicht sehen.

    Veröffentlicht am 16. Okt. 2026

    Du hast einmal strukturierte Daten eingebaut, einen JSON-Block aus einem Generator kopiert, und sechs Monate später behauptet er, die Seite habe sieben FAQ-Fragen, während sie fünf zeigt. Dieses Tutorial richtet nextjs JSON LD so ein, dass das nicht passieren kann: ein typisierter Builder pro Schema-Typ, eine winzige Komponente, die das Script sicher ausgibt, und die Regel, dass jeder Builder dieselben Daten liest, die die Seite rendert. Wir behandeln die vier Typen, die eine Produkt-Website braucht: SoftwareApplication, BlogPosting, FAQPage und BreadcrumbList.

    Alles hier ist TypeScript für Next.js 16 und React 19. Mehr Guides dieser Art findest du unter Next.js und Tailwind CSS.

    Wohin gehört JSON-LD im Next.js App Router?

    Direkt in die Page oder das Layout, als einfaches Script-Tag. Der offizielle JSON-LD-Guide von Next.js (opens in a new tab) (zuletzt aktualisiert am 2. März 2026, geprüft am 16. Oktober 2026) empfiehlt, strukturierte Daten als Script-Tag in layout.js oder page.js zu rendern. Die Next.js Metadata API hilft dabei nicht: metadata und generateMetadata erzeugen Head-Tags wie Titel, Beschreibung, Canonical und Open Graph. Ein Feld für JSON-LD gibt es dort nicht.

    Zwei Details aus demselben Guide sind wichtig:

    • JSON.stringify bereinigt nichts. Ein Post-Titel mit </script> würde das Tag zu früh beenden, also ersetz jedes < durch seine Unicode-Escape-Sequenz.
    • Verzichte auf next/script. Es ist dafür da, JavaScript zu laden und auszuführen. JSON-LD sind Daten, also ist ein natives Script-Tag das richtige Element.

    Pack das in eine Komponente und schreib das Tag nie wieder von Hand:

    tsx
    // components/json-ld.tsx
    import type { Graph } from "schema-dts";
    // "<" maskiert, damit ein "</script>" in einem Titel oder einer Antwort das Tag nicht beendet
    export function JsonLd({ data }: { data: Graph }) {
    return (
    <script
    type="application/ld+json"
    dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, "\\u003c") }}
    />
    );
    }

    Sie hat kein "use client", rendert also auf dem Server, und das Script steht in der ersten HTML-Antwort. Dort lesen es auch Crawler und KI-Systeme, die nie JavaScript ausführen.

    Welche Schema-Typen braucht eine Produkt-Website?

    Vier decken die meisten Seiten einer SaaS- oder Bibliotheks-Website ab. Was Google mit jedem davon macht, hat sich dieses Jahr geändert. Hier der Stand vom 16. Oktober 2026, laut Google Search Central:

    TypWoWas Google sagt
    SoftwareApplicationStartseite, PreisseiteEin Rich Result braucht name, offers.price und entweder aggregateRating oder review (Doku (opens in a new tab))
    BlogPostingjeder PostKeine Pflicht-Properties; empfohlen sind author, datePublished, dateModified, headline und image (Doku (opens in a new tab))
    FAQPageFAQ-BereicheSeit September 2023 auf Websites von Behörden und aus dem Gesundheitsbereich beschränkt; das FAQ-Rich-Result erscheint seit dem 7. Mai 2026 nicht mehr, die Doku wurde am 15. Juni 2026 entfernt (Search Updates (opens in a new tab))
    BreadcrumbListjede Seite unterhalb der StartseiteMindestens zwei ListItems mit position, name und item; das letzte darf item weglassen (Doku (opens in a new tab))

    Zwei Anmerkungen zur Tabelle. Erstens verlangt das Rich Result für SoftwareApplication Bewertungen oder Rezensionen. Wenn du keine hast, liefer den Knoten ohne sie aus: Er beschreibt trotzdem dein Produkt, seinen Preis und den Herausgeber. Eine Bewertung, die kein Leser auf der Seite sieht, verstößt gegen die Sichtbarkeitsregel, um die es weiter unten geht. Zweitens bringt FAQPage in Google nichts Sichtbares mehr. Es ist aber weiter ein gültiger schema.org-Typ, den andere Parser lesen können, und es kostet nichts, wenn es aus Daten kommt, die du sowieso renderst. Genau diese Bedingung gilt für alle vier Typen.

    Wie typisiere ich JSON-LD mit TypeScript in Next.js?

    Nutz die Typen von schema-dts und schreib eine kleine Builder-Funktion pro Schema-Typ. Installier die Typen einmal:

    bash
    npm install -D schema-dts

    schema-dts (opens in a new tab) (Version 2.1.0, veröffentlicht am 2. Oktober 2026) ist das schema.org-Vokabular als TypeScript-Typen. Importier es mit import type, dann landet nichts im Browser. Danach schreibst du pro Typ einen Builder in einem einfachen Modul. Jeder nimmt die Daten, die deine Seite schon hat, und gibt einen typisierten Knoten zurück. Eine falsch geschriebene Property oder eine Zahl, wo schema.org Text erwartet, lässt dadurch den Typecheck in next build scheitern. Diese Datei ist die ganze Schema-Schicht:

    ts
    // lib/json-ld.ts
    import type { BlogPosting, BreadcrumbList, FAQPage, Graph, SoftwareApplication, Thing } from "schema-dts";
    const ORIGIN = "https://example.com";
    const SITE_NAME = "Example";
    // "/blog/post" -> "https://example.com/blog/post"
    export const absoluteUrl = (path: string) => new URL(path, ORIGIN).toString();
    // stabile ids, damit Knoten auf verschiedenen Seiten auf dasselbe zeigen
    export const ids = {
    organization: `${ORIGIN}/#organization`,
    software: `${ORIGIN}/#software`,
    };
    // eine @id plus das Nötigste, damit ein Tool, das nur diese Seite liest, trotzdem sieht, wer und was
    const organizationRef = () => ({ "@type": "Organization", "@id": ids.organization, name: SITE_NAME, url: absoluteUrl("/") }) as const;
    const softwareRef = () => ({ "@type": "SoftwareApplication", "@id": ids.software, name: SITE_NAME, url: absoluteUrl("/") }) as const;
    export type Crumb = { name: string; href: string };
    export type FaqEntry = { question: string; answer: string };
    export function softwareApplication(input: { name: string; description: string; price: number; currency: string }): SoftwareApplication {
    return {
    "@type": "SoftwareApplication",
    "@id": ids.software,
    name: input.name,
    description: input.description,
    url: absoluteUrl("/"),
    applicationCategory: "DeveloperApplication",
    operatingSystem: "Any",
    offers: { "@type": "Offer", price: input.price, priceCurrency: input.currency, url: absoluteUrl("/pricing") },
    publisher: organizationRef(),
    };
    }
    export function blogPosting(input: {
    title: string;
    description: string;
    path: string;
    datePublished: string;
    dateModified?: string;
    image: string;
    author: { name: string; path: string };
    keywords: string[];
    }): BlogPosting {
    const url = absoluteUrl(input.path);
    return {
    "@type": "BlogPosting",
    "@id": `${url}#article`,
    headline: input.title,
    description: input.description,
    url,
    mainEntityOfPage: url,
    image: absoluteUrl(input.image),
    datePublished: input.datePublished,
    dateModified: input.dateModified ?? input.datePublished,
    author: { "@type": "Person", name: input.author.name, url: absoluteUrl(input.author.path) },
    publisher: organizationRef(),
    about: softwareRef(),
    keywords: input.keywords.join(", "),
    };
    }
    export function breadcrumbList(crumbs: Crumb[]): BreadcrumbList {
    return {
    "@type": "BreadcrumbList",
    itemListElement: crumbs.map((crumb, i) => ({
    "@type": "ListItem",
    position: i + 1,
    name: crumb.name,
    item: absoluteUrl(crumb.href),
    })),
    };
    }
    export function faqPage(entries: FaqEntry[]): FAQPage {
    return {
    "@type": "FAQPage",
    mainEntity: entries.map((entry) => ({
    "@type": "Question",
    name: entry.question,
    acceptedAnswer: { "@type": "Answer", text: entry.answer },
    })),
    };
    }
    // mehrere Knoten in einem Script
    export function graph(...nodes: Thing[]): Graph {
    return { "@context": "https://schema.org", "@graph": nodes };
    }

    Die Verknüpfung läuft über die @id-Werte. Der Blogpost verweist über ihre ids darauf, dass er von der Software handelt (about) und von der Organisation veröffentlicht wird. So müssen die vollständigen Knoten nur einmal auf der Startseite stehen. Ein reines { "@id": ... } hat aber einen Haken: Ein Tool, das nur den Post liest, sieht einen leeren Knoten, weil es die Startseite nie abruft. Deshalb wiederholen die Refs oben @type, name und url neben der id. Genauso macht es wingo-ui.com für seine Organisations- und Produktknoten. graph() packt mehrere Knoten in ein Script, und das liest sich im Validator leichter als vier einzelne Tags.

    Der price, den du an softwareApplication() übergibst, sollte aus derselben Config kommen, die deine Preistabelle rendert. Auf wingo-ui.com entstehen die Offers im JSON-LD aus derselben Plan-Datei, die auch die Preisseite rendert. Ändert sich ein Preis, zieht das Markup automatisch mit.

    Wie halte ich JSON-LD synchron mit der sichtbaren Seite?

    Render den sichtbaren Inhalt und das JSON-LD aus demselben Array, im selben Komponentenbaum. Die Richtlinien für strukturierte Daten (opens in a new tab) von Google (zuletzt aktualisiert am 10. Juli 2026) fassen die Regel in einem Satz: Zeichne keine Inhalte aus, die für Leser der Seite nicht sichtbar sind. Kopiertes JSON verletzt diese Regel schleichend, mit jeder Änderung ein Stück mehr. Mit einem Array und zwei Renderern lesen die sichtbare Komponente und der JSON-LD-Builder denselben Wert, deshalb können sie sich nicht widersprechen.

    Wie baue ich Breadcrumb JSON-LD in Next.js ein?

    Bau ein Crumb[]-Array und gib es im selben Render an den sichtbaren Breadcrumb-Pfad und an breadcrumbList():

    tsx
    import { Breadcrumb } from "@/components/ui/breadcrumb";
    import { JsonLd } from "@/components/json-ld";
    import { breadcrumbList, graph, type Crumb } from "@/lib/json-ld";
    export function PageTrail({ crumbs }: { crumbs: Crumb[] }) {
    return (
    <>
    <Breadcrumb items={crumbs.map((crumb) => ({ label: crumb.name, href: crumb.href }))} />
    <JsonLd data={graph(breadcrumbList(crumbs))} />
    </>
    );
    }

    Die Breadcrumb behandelt den letzten Eintrag als aktuelle Seite und rendert ihn als einfachen Text mit aria-current="page". Du kannst sein href also für das JSON-LD im Array lassen. Die Smartphone-Ansicht (ein einzelner Zurück-Link zur übergeordneten Seite unter 640px, aus den Defaults mobileVariant="back" und mobileBreakpoint="sm") wird per CSS umgeschaltet. Das Server-HTML enthält also immer den vollständigen Pfad, den das JSON-LD beschreibt.

    Mach das Fenster schmaler: Die mittleren Einträge klappen nacheinander in die Ellipse, und unter 640px wird der Pfad zu einem einzigen Zurück-Link
    $ npx wingo-ui@latest add breadcrumb
    ProDoku: Breadcrumb

    Über diese Regel sind wir in unserem eigenen Blog gestolpert. Die Vorlage Blog Post Page rendert ihren eigenen Pfad (Blog, dann die Kategorie, dann der Post), während die Breadcrumb der Website bei Home beginnt und Kategorien auf ihre eigenen Seiten verlinkt. Zwei Pfade auf einer Seite würden bedeuten, dass das JSON-LD höchstens zu einem davon passt. Deshalb blendet die Website den Pfad der Vorlage mit classNames={{ breadcrumb: "hidden" }} aus und rendert eine Server-Breadcrumb, deren Array auch die BreadcrumbList füttert.

    Wie baue ich FAQPage JSON-LD in Next.js ein?

    Für ein FAQ auf einer Preis- oder Landingpage erledigt die FAQ Section beide Aufgaben mit einer Prop. Sie baut auf dem Accordion auf, die Fragen sind also echte Buttons mit Pfeiltasten-Steuerung. Übergib deine items und schalte jsonLd ein. Dann gibt sie neben der Liste ein FAQPage-Script aus, gebaut aus denselben Items:

    tsx
    // app/pricing/page.tsx
    import { FaqSection, type FaqItem } from "@/components/blocks/faq-section";
    const faq: FaqItem[] = [
    {
    question: "Kann ich den Plan später wechseln?",
    answer: "Ja. Upgrades gelten sofort, Downgrades ab der nächsten Verlängerung.",
    },
    {
    question: "Wo fange ich an?",
    answer: (
    <>
    Lies den <a href="/docs">Einstiegsguide</a> und installier dann deine erste Komponente.
    </>
    ),
    },
    ];
    export default function PricingPage() {
    return <FaqSection items={faq} jsonLd />;
    }

    Wenn eine Antwort JSX ist, sammelt die Section den reinen Text aus ihren Children. Die zweite Antwort wird im Script also zu „Lies den Einstiegsguide und installier dann deine erste Komponente.“ Steht der Text in den Props einer Komponente statt in ihren Children, setz answerText auf dieses Item. Die Section maskiert < genauso wie die JsonLd-Komponente, also kann auch eine Antwort aus einem CMS das Script-Tag nicht schließen.

    Öffne eine Frage, und die vorherige schließt sich; mit jsonLd schreiben dieselben Items zusätzlich ein FAQPage-Script ins Server-HTML
    $ npx wingo-ui@latest add faq-section
    FreeDoku: FAQ Section

    Nutz jsonLd, wenn die Section das einzige FAQ auf der Seite ist. Auf einer Seite, auf der du schon einen @graph baust, lass es aus und häng stattdessen faqPage(items) an den Graph, aus demselben Array. Zwei FAQPage-Knoten für eine Liste sagen dasselbe doppelt und machen die Ausgabe schwerer zu prüfen.

    Wie setze ich alles auf einer Blogpost-Seite zusammen?

    Hier ist eine Post-Route, die alles aus einem post-Objekt rendert: den Pfad, den Artikel, das FAQ und einen @graph mit allen drei Knoten. Das FAQ wird an den Markdown-Body angehängt, also kommen die sichtbaren Fragen und die FAQPage aus demselben Array post.faq:

    tsx
    // app/blog/[slug]/page.tsx
    import type { Metadata } from "next";
    import { notFound } from "next/navigation";
    import { BlogPostPage } from "@/components/templates/blog-post-page";
    import { Breadcrumb } from "@/components/ui/breadcrumb";
    import { JsonLd } from "@/components/json-ld";
    import { blogPosting, breadcrumbList, faqPage, graph, type Crumb } from "@/lib/json-ld";
    import { getPost, getPosts } from "@/lib/posts";
    export const dynamicParams = false;
    export function generateStaticParams() {
    return getPosts().map((post) => ({ slug: post.slug }));
    }
    export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
    const post = getPost((await params).slug);
    if (!post) return {};
    return { title: post.title, description: post.excerpt, alternates: { canonical: `/blog/${post.slug}` } };
    }
    export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
    const post = getPost((await params).slug);
    if (!post) notFound();
    const path = `/blog/${post.slug}`;
    const crumbs: Crumb[] = [
    { name: "Startseite", href: "/" },
    { name: "Blog", href: "/blog" },
    { name: post.categoryName, href: `/blog/category/${post.category}` },
    { name: post.title, href: path },
    ];
    // das sichtbare FAQ und der FAQPage-Knoten lesen dasselbe Array
    const faqMarkdown = post.faq.map((entry) => `### ${entry.question}\n\n${entry.answer}`).join("\n\n");
    return (
    <>
    <JsonLd
    data={graph(
    blogPosting({
    title: post.title,
    description: post.excerpt,
    path,
    datePublished: post.date,
    dateModified: post.updated,
    image: `${path}/opengraph-image`,
    author: { name: post.author.name, path: post.author.href ?? "/about" },
    keywords: post.tags ?? [],
    }),
    breadcrumbList(crumbs),
    faqPage(post.faq),
    )}
    />
    <Breadcrumb
    items={crumbs.map((crumb) => ({ label: crumb.name, href: crumb.href }))}
    className="mx-auto w-full max-w-[1200px] px-4 pt-6 sm:px-6"
    />
    <BlogPostPage
    article={{ ...post, body: `${post.body}\n\n## FAQ\n\n${faqMarkdown}` }}
    categories={[{ value: post.category, label: post.categoryName }]}
    relatedPosts={post.related}
    header={false}
    footer={false}
    classNames={{ breadcrumb: "hidden" }}
    />
    </>
    );
    }

    getPost ist dein eigener Loader. Hier gibt er die BlogArticle-Felder der Vorlage zurück (slug, title, excerpt, category, date, readingTime, author, body, tags) plus categoryName, updated, faq und related. headline ist post.title, also derselbe String, den die Vorlage als h1 rendert, und dateModified fällt im Builder auf das Veröffentlichungsdatum zurück.

    Ein Stolperstein bei image: Der opengraph-image-Pfad oben stimmt nur, wenn im Pfad der Route keine Route Group und keine Parallel Route steckt. Unter einem Ordner wie app/(site)/blog/[slug] hängt Next.js an die Metadata-Route einen kurzen Hash des übergeordneten Pfads an (opengraph-image-<hash>, siehe getMetadataRouteSuffix in next/dist/lib/metadata/get-metadata-route.js). Ein hartkodierter Pfad zeigt dann auf eine 404-Seite. Kopier die URL aus dem gerenderten og:image-Tag oder berechne das Suffix so wie Next.js. Unsere eigene Post-Route liegt unter app/(library)/(site)/blog/[slug], deshalb hat wingo-ui.com einen Helper wie diesen, hier für eine Route unter app/(site)/blog/[slug]:

    ts
    // djb2 des übergeordneten Ordners der Route, Basis 36, die ersten 6 Zeichen (wie getMetadataRouteSuffix in next)
    function metadataRouteSuffix(parent: string) {
    let hash = 5381;
    for (let i = 0; i < parent.length; i++) hash = ((hash << 5) + hash + parent.charCodeAt(i)) & 0xffffffff;
    return (hash >>> 0).toString(36).slice(0, 6);
    }
    const ogImage = (slug: string) => `/blog/${slug}/opengraph-image-${metadataRouteSuffix("/(site)/blog/[slug]")}`;

    Übergib den Ordnerpfad genau so, wie er unter app/ liegt, mit Route Groups, und prüf ihn erneut, wenn du die Route verschiebst.

    So ähnlich läuft es auch auf wingo-ui.com. Unsere Post-Seiten packen BlogPosting und FAQPage in einen Graph und geben die BreadcrumbList neben dem sichtbaren Pfad aus. Das BlogPosting listet außerdem in mentions jede Komponente, die der Post behandelt, jeweils als SoftwareSourceCode-Knoten mit der URL ihrer Doku. Wenn du auch Markdown-Versionen für KI-Systeme veröffentlichst, gilt dort dieselbe Idee mit einer einzigen Quelle; siehe Next.js llms.txt mit .md-Seiten.

    Wie teste ich, ob das Markup zur Seite passt?

    Prüf drei Dinge in dieser Reihenfolge: das Server-HTML, die zwei öffentlichen Validatoren und ein eigenes Script, das das Markup mit dem sichtbaren Text vergleicht. Fang mit dem Server-HTML an, denn das liest ein Crawler:

    bash
    curl -s http://localhost:3000/blog/my-post | grep -o '<script type="application/ld+json">[^<]*'

    Dann füg die URL oder das HTML in den Schema Markup Validator (opens in a new tab) ein, der jeden schema.org-Typ prüft, und in den Rich Results Test (opens in a new tab), der nur die Typen meldet, die Google noch anzeigen kann.

    Keines der beiden Tools weiß, was deine Seite anzeigt, also ergänz einen eigenen Check. Dieses Script ruft eine Seite ab, liest jeden JSON-LD-Knoten und schlägt fehl, wenn eine Headline, eine Frage oder der Name eines Breadcrumb-Eintrags im sichtbaren Text fehlt:

    ts
    // scripts/check-json-ld.mts
    // Aufruf: npx tsx scripts/check-json-ld.mts http://localhost:3000/blog/my-post
    type Node = { "@type"?: string; headline?: string; mainEntity?: { name: string }[]; itemListElement?: { name: string }[] };
    const html = await (await fetch(process.argv[2])).text();
    const nodes: Node[] = [...html.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/gs)]
    .map((match) => JSON.parse(match[1]))
    .flatMap((doc) => doc["@graph"] ?? [doc]);
    // der sichtbare Text: ohne Scripts, ohne Tags, die Entities, die react schreibt, dekodiert
    const visible = html
    .replace(/<script[\s\S]*?<\/script>/g, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/&#x27;/g, "'")
    .replace(/&quot;/g, '"')
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&")
    .replace(/\s+/g, " ");
    const missing: string[] = [];
    for (const node of nodes) {
    const names =
    node["@type"] === "BlogPosting" ? [node.headline] :
    node["@type"] === "FAQPage" ? node.mainEntity?.map((q) => q.name) :
    node["@type"] === "BreadcrumbList" ? node.itemListElement?.map((item) => item.name) : [];
    for (const name of names ?? []) if (name && !visible.includes(name)) missing.push(`${node["@type"]}: ${name}`);
    }
    if (missing.length > 0) {
    console.error(`nicht auf der Seite:\n${missing.join("\n")}`);
    process.exit(1);
    }
    console.log(`ok: ${nodes.length} Knoten passen zur sichtbaren Seite`);

    Wir haben es beim Schreiben dieses Posts auf unserem eigenen Dev-Server gegen zwei Posts laufen lassen, und beide meldeten drei passende Knoten (BlogPosting und FAQPage aus dem Graph, dazu die BreadcrumbList). Lass es in der CI gegen ein Preview-Deploy laufen, dann kann der Bug „sieben Fragen im Markup, fünf auf der Seite“ nicht mehr live gehen.

    Was mache ich als Nächstes?

    Installier die zwei Komponenten, die sichtbaren Inhalt und sein JSON-LD aus einem Array rendern:

    bash
    npx wingo-ui@latest add faq-section breadcrumb

    Die FAQ Section ist kostenlos. Die Breadcrumb und die Vorlage Blog Post Page gehören zu Pro. Für den Rest des Setups rund um diese Seiten (Tokens, Dark Mode, Kontrast) lies den Pillar-Guide zu Design Tokens in Tailwind v4. Und wenn du ein Projekt neu aufsetzt, erklärt das Setup einer Komponentenbibliothek in Next.js den Teil im App Router.

    Komponenten in diesem Artikel

    • FAQ Section

      A React FAQ section for marketing pages: an accordion, a split layout, an open grid or expandable cards, with topics and live search.

      Free
    • Breadcrumb

      A React breadcrumb that shows where you are, folds the middle of long paths and gives phones a back link.

      Pro
    • Blog Post Page

      A React blog post template: byline, share actions, a drawn cover, a Markdown body with a sticky table of contents and reading progress.

      Pro

    Häufige Fragen

    Wie füge ich JSON-LD im Next.js App Router ein?

    Render aus page.tsx oder layout.tsx ein Script-Tag mit type application/ld+json und setz den Inhalt per dangerouslySetInnerHTML: JSON.stringify, dann jedes < durch \u003c ersetzen. Die Metadata API hat kein JSON-LD-Feld, deshalb gehört es nicht in generateMetadata.

    Sollte ich next/script für JSON-LD nutzen?

    Nein. Laut dem JSON-LD-Guide von Next.js ist next/script dafür gebaut, JavaScript zu laden und auszuführen, und für strukturierte Daten ist ein natives Script-Tag die richtige Wahl.

    Zeigt Google noch FAQ-Rich-Results?

    Nein. Laut Google Search Central erscheint das FAQ-Rich-Result seit dem 7. Mai 2026 nicht mehr in der Google-Suche, und die Doku dazu wurde am 15. Juni 2026 entfernt; seit September 2023 war es nur noch für maßgebliche Websites von Behörden und aus dem Gesundheitsbereich zu sehen. FAQPage ist weiter ein gültiger schema.org-Typ, also behalte ihn nur für Fragen, die auf der Seite sichtbar sind.

    Kann eine Client-Komponente JSON-LD rendern?

    Ja, solange das Script Teil der gerenderten Ausgabe ist, denn Client-Komponenten im App Router rendern auch auf dem Server, und das Tag landet im HTML. Füg es nie aus useEffect ein, denn ein Crawler, der kein JavaScript ausführt, würde es nie sehen.

    Brauche ich schema-dts, um JSON-LD in TypeScript zu typisieren?

    Nein, aber es hilft. schema-dts (Version 2.1.0, Stand Oktober 2026) liefert das schema.org-Vokabular als TypeScript-Typen; mit import type eingebunden landet nichts im Bundle, und falsch geschriebene Properties oder falsche Werttypen fallen schon beim Build auf.

    • Next.js
    • JSON-LD
    • SEO
    • App Router
    • TypeScript

    Teilen

    SR

    Über den Autor

    Serban Rusu

    Gründer von Wingo UI

    Serban Rusu hat Wingo UI gegründet. Er baut die Komponenten, die CLI und den MCP-Server und schreibt über React-UIs für Smartphones und KI-Agents.

    Mehr von Serban
    Weiterlesen

    Ähnliche Artikel

    Alle Artikel
    Praxis: Screens und Apps

    Next.js Blog Template: Markdown, Live-Demos und JSON-LD

    So haben wir ein Next.js Blog Template auf Markdown gebaut: zod-Frontmatter, Live-Demos, Sticky-Inhaltsverzeichnis, JSON-LD, Feeds und ein strenger Validator.

    SRSerban Rusu·16. Okt. 2026·10 Min. Lesezeit
    Praxis: Screens und Apps

    Next.js SaaS Landing Page: Was jeder Abschnitt beweisen muss

    Bau eine Next.js SaaS Landing Page Abschnitt für Abschnitt: was Hero, Social Proof, Features, Preise und FAQ beweisen müssen, was rausfliegt, was mobil zählt.

    SRSerban Rusu·16. Okt. 2026·11 Min. Lesezeit
    Next.js und Tailwind CSS

    Next.js Cookie Banner: DSGVO und Consent Mode v2

    Ein Next.js Cookie Banner, der der DSGVO standhält: Zustimmen und Ablehnen gleich groß, Schalter je Kategorie, Consent Mode v2 und Embeds erst nach Zustimmung.

    SRSerban Rusu·16. Okt. 2026·12 Min. Lesezeit
    Newsletter

    Neue Artikel per E-Mail

    Neue Guides, Tutorials und Vergleiche aus dem Wingo UI Blog, sobald sie erscheinen. Die meisten Artikel sind auf Englisch.

    Kein Spam. Du kannst dich jederzeit abmelden.

    WingoUI

    Animierte, konfigurierbare, mobile-first React-Komponenten und Blöcke. Kopier den Quellcode, pass ihn an und lass deinen Coding-Agenten damit bauen.

    Produkt

    • Komponenten
    • Vorlagen
    • Preise
    • KI-Agenten
    • Theme-Generator

    Ressourcen

    • Doku
    • Blog
    • Changelog
    • Changelog-RSS
    • llms.txt

    Rechtliches

    • AGB
    • Datenschutz
    • Rückerstattung
    • Lizenz
    • Impressum

    Komponenten-Kategorien

    • Buttons & Actions
    • Inputs
    • Forms
    • Navigation
    • Overlays
    • Feedback
    • Data Display
    • Tables & Lists
    • Charts & Stats
    • Layout
    • Media
    • AI Kit
    • Text & Effects
    • Mobile
    • Commerce
    • Marketing Sections
    • Blocks
    • Hooks & Utilities

    © 2026 Wingo UI

    Wingo UI