Vernetzte Schema-Knoten als leuchtender Graph auf dunklem Hintergrund

    JSON-LD Schema für SPAs automatisieren: Structured Data ohne Backend

    Till FreitagTill Freitag14. April 20265 min Lesezeit
    Till Freitag

    TL;DR: „JSON-LD Schema macht deine SPA für Google's Rich Results sichtbar. Mit React-Komponenten kannst du Organization, BlogPosting und BreadcrumbList automatisch auf jeder Seite generieren – ohne Backend, ohne CMS."

    — Till Freitag

    Warum SPAs kein Structured Data haben

    Single Page Applications haben ein doppeltes SEO-Problem:

    1. Rendering: Der HTML-Body ist initial leer (gelöst durch Playwright SSG)
    2. Structured Data: Es gibt kein Backend, das JSON-LD automatisch in den HTML-Head injiziert

    Ohne Structured Data verpasst du:

    • Rich Snippets in Google-Suchergebnissen (Sternebewertungen, FAQ-Dropdowns, Breadcrumbs)
    • Knowledge Graph Einträge für dein Unternehmen
    • Event-Listings in der Google-Suche
    • Artikel-Karussells für Blog-Content

    Kontext: Dieser Artikel ist Teil unseres Vibe Coding SEO Playbooks. Dort findest du den Gesamtüberblick.

    JSON-LD Grundlagen

    JSON-LD (JavaScript Object Notation for Linked Data) ist Googles bevorzugtes Format für Structured Data. Es wird als <script type="application/ld+json"> im HTML platziert und beschreibt den Inhalt der Seite maschinenlesbar.

    Beispiel: Organization Schema

    {
      "@context": "https://schema.org",
      "@type": "Organization",
      "name": "Till Freitag",
      "url": "https://till-freitag.com",
      "logo": "https://till-freitag.com/logo.png",
      "sameAs": [
        "https://linkedin.com/company/tillfreitag",
        "https://github.com/tillfreitag"
      ]
    }

    Dieses Schema sagt Google: „Hier ist ein Unternehmen namens Till Freitag, das unter dieser URL erreichbar ist."

    Die React-Komponente: SEOHead

    Anstatt JSON-LD manuell in jede Seite zu kopieren, bauen wir eine wiederverwendbare React-Komponente, die das Schema automatisch generiert:

    // components/seo/SEOHead.tsx
    import { Helmet } from 'react-helmet-async';
    
    interface SEOHeadProps {
      title: string;
      description: string;
      canonicalUrl: string;
      jsonLd?: Record<string, unknown>[];
      ogImage?: string;
      ogType?: string;
    }
    
    export default function SEOHead({
      title,
      description,
      canonicalUrl,
      jsonLd = [],
      ogImage,
      ogType = 'website',
    }: SEOHeadProps) {
      return (
        <Helmet>
          <title>{title}</title>
          <meta name="description" content={description} />
          <link rel="canonical" href={canonicalUrl} />
          
          {/* Open Graph */}
          <meta property="og:title" content={title} />
          <meta property="og:description" content={description} />
          <meta property="og:type" content={ogType} />
          <meta property="og:url" content={canonicalUrl} />
          {ogImage && <meta property="og:image" content={ogImage} />}
          
          {/* JSON-LD Schemas */}
          {jsonLd.map((schema, i) => (
            <script key={i} type="application/ld+json">
              {JSON.stringify(schema)}
            </script>
          ))}
        </Helmet>
      );
    }

    Wichtig: Die Komponente akzeptiert ein Array von Schemas. So kannst du pro Seite mehrere Schema-Typen kombinieren.

    Schema-Typen für typische Seiten

    1. BlogPosting (für Artikel)

    function generateBlogPostingSchema(post: BlogPost) {
      return {
        '@context': 'https://schema.org',
        '@type': 'BlogPosting',
        headline: post.title,
        description: post.excerpt,
        url: `https://deine-domain.com/blog/${post.slug}`,
        datePublished: post.date,
        dateModified: post.modified || post.date,
        wordCount: post.content.split(/\s+/).length,
        author: {
          '@type': 'Person',
          name: post.author.name,
          url: `https://deine-domain.com/blog/autor/${post.author.slug}`,
        },
        publisher: {
          '@type': 'Organization',
          name: 'Dein Unternehmen',
          logo: {
            '@type': 'ImageObject',
            url: 'https://deine-domain.com/logo.png',
          },
        },
        mainEntityOfPage: {
          '@type': 'WebPage',
          '@id': `https://deine-domain.com/blog/${post.slug}`,
        },
      };
    }

    2. BreadcrumbList (für Navigation)

    function generateBreadcrumbSchema(
      items: { name: string; path: string }[]
    ) {
      return {
        '@context': 'https://schema.org',
        '@type': 'BreadcrumbList',
        itemListElement: items.map((item, index) => ({
          '@type': 'ListItem',
          position: index + 1,
          item: {
            '@type': 'WebPage',
            '@id': `https://deine-domain.com${item.path}`,
            name: item.name,
          },
        })),
      };
    }

    ⚠️ Häufiger Fehler: Das item Feld muss ein strukturiertes Objekt mit @id sein – nicht nur ein String. Google's Rich Results Test wird sonst Fehler melden.

    3. ProfessionalService (für Dienstleistungsseiten)

    function generateServiceSchema(service: Service) {
      return {
        '@context': 'https://schema.org',
        '@type': 'ProfessionalService',
        name: service.name,
        description: service.description,
        url: `https://deine-domain.com${service.path}`,
        provider: {
          '@type': 'Organization',
          name: 'Dein Unternehmen',
        },
        areaServed: [
          { '@type': 'Country', name: 'Germany' },
          { '@type': 'Country', name: 'Austria' },
          { '@type': 'Country', name: 'Switzerland' },
        ],
        serviceType: service.type,
      };
    }

    4. FAQPage (für FAQ-Sektionen)

    function generateFAQSchema(
      faqs: { question: string; answer: string }[]
    ) {
      return {
        '@context': 'https://schema.org',
        '@type': 'FAQPage',
        mainEntity: faqs.map(faq => ({
          '@type': 'Question',
          name: faq.question,
          acceptedAnswer: {
            '@type': 'Answer',
            text: faq.answer,
          },
        })),
      };
    }

    Automatische Schema-Generierung pro Route

    Der eleganteste Ansatz: Definiere Schema-Regeln pro Route-Typ und lass die Komponente das richtige Schema automatisch wählen:

    // lib/schema-config.ts
    type SchemaRule = {
      pattern: RegExp;
      generate: (params: RouteParams) => Record<string, unknown>[];
    };
    
    const schemaRules: SchemaRule[] = [
      {
        pattern: /^\/blog\/[^/]+$/,
        generate: (params) => [
          generateBlogPostingSchema(params.post),
          generateBreadcrumbSchema([
            { name: 'Home', path: '/' },
            { name: 'Blog', path: '/blog' },
            { name: params.post.title, path: params.path },
          ]),
        ],
      },
      {
        pattern: /^\/services\//,
        generate: (params) => [
          generateServiceSchema(params.service),
          generateBreadcrumbSchema([
            { name: 'Home', path: '/' },
            { name: 'Services', path: '/services' },
            { name: params.service.name, path: params.path },
          ]),
        ],
      },
      {
        pattern: /^\/$/, // Homepage
        generate: () => [
          generateOrganizationSchema(),
          generateWebSiteSchema(),
        ],
      },
    ];

    Häufige Schema-Fehler

    1. Fehlendes item in BreadcrumbList

    Falsch:

    {
      "@type": "ListItem",
      "position": 1,
      "name": "Blog",
      "item": "/blog"
    }

    Richtig:

    {
      "@type": "ListItem",
      "position": 1,
      "item": {
        "@type": "WebPage",
        "@id": "https://deine-domain.com/blog",
        "name": "Blog"
      }
    }

    2. Fehlendes offers in Event-Schemas

    Auch bei kostenlosen Events erwartet Google ein offers-Feld:

    {
      "@type": "Event",
      "offers": {
        "@type": "Offer",
        "availability": "https://schema.org/SoldOut",
        "price": "0",
        "priceCurrency": "EUR"
      }
    }

    3. Relative URLs statt absolute URLs

    "url": "/blog/mein-artikel"
    "url": "https://deine-domain.com/blog/mein-artikel"

    Google benötigt immer absolute URLs in JSON-LD.

    Validierung

    Nach der Implementierung:

    1. Google Rich Results Test: search.google.com/test/rich-results
    2. Schema.org Validator: validator.schema.org
    3. Browser DevTools: document.querySelectorAll('script[type="application/ld+json"]') → Prüfe den Output

    Pro-Tipp: Baue einen automatisierten Test, der alle Routen auf gültiges JSON-LD prüft:

    // tests/schema.test.ts
    import { test, expect } from '@playwright/test';
    
    test('all pages have valid JSON-LD', async ({ page }) => {
      const routes = ['/', '/blog', '/services/vibe-coding-seo'];
      
      for (const route of routes) {
        await page.goto(`http://localhost:4173${route}`);
        const schemas = await page.$$eval(
          'script[type="application/ld+json"]',
          scripts => scripts.map(s => JSON.parse(s.textContent || '{}'))
        );
        
        expect(schemas.length).toBeGreaterThan(0);
        schemas.forEach(schema => {
          expect(schema['@context']).toBe('https://schema.org');
          expect(schema['@type']).toBeTruthy();
        });
      }
    });

    Fazit: Schema macht deine SPA smart

    JSON-LD Schema ist die zweite Säule nach Playwright SSG für vollständige SPA-SEO. Zusammen sorgen sie dafür, dass Google deine Inhalte nicht nur sehen, sondern auch verstehen kann.

    Der komplette Vibe Coding SEO Stack:

    1. Lovable → Generiert die App
    2. Playwright SSGRendert statisches HTML
    3. JSON-LD Schema → Strukturierte Daten (dieser Artikel)
    4. Vercel Edge → Schnelle Auslieferung

    Klingt komplex? Wir setzen das für dich um →

    In unserem SEO-Audit prüfen wir dein Schema, dein Rendering und deine Core Web Vitals – und liefern einen konkreten Implementierungsplan.

    Verwandte Artikel

    TeilenLinkedInWhatsAppE-Mail

    Verwandte Artikel

    Google Search Console Dashboard mit Performance-Graphen und Coverage Reports
    14. April 20264 min

    Google Search Console für Vibe-Coding-Projekte: Setup, Debugging & Indexierung

    Deine Lovable-App ist live auf Vercel – aber Google indexiert nichts? So richtest du die Search Console ein, debuggst Cr…

    Weiterlesen
    Social Media Preview Cards mit OG-Image Meta Tags schweben im dunklen Raum
    14. April 20264 min

    OG-Image Best Practices für SPAs: So werden deine Vibe-Coding-Projekte teilbar

    Wenn du einen Link aus deiner Lovable-App teilst, zeigt LinkedIn ein leeres Kästchen. Warum SPAs keine Social Previews l…

    Weiterlesen
    Rakete aus Code-Elementen startet durch Suchergebnisseiten mit Lighthouse Score 100
    14. April 20265 min

    Vibe Coding & SEO: Warum KI-generierte Apps unsichtbar bleiben – und wie wir das lösen

    Lovable, Bolt, v0 – Vibe Coding Tools erzeugen SPAs, die Google nicht sieht. Unser Playbook macht sie SEO-ready: SSG, Sc…

    Weiterlesen
    Pipeline-Diagramm mit drei Stationen: Lovable, GitHub, Vercel
    14. April 20264 min

    Lovable → GitHub → Vercel: Der komplette Deployment-Flow für SEO-ready Apps

    Lovable generiert die App, GitHub versioniert den Code, Vercel liefert mit < 50ms TTFB aus. Dieser Guide zeigt den kompl…

    Weiterlesen
    Headless Browser rendert SPA-Seiten als statisches HTML für Suchmaschinen
    14. April 20265 min

    Playwright SSG Tutorial: So machst du Lovable Apps für Google sichtbar

    SPAs sind für Suchmaschinen unsichtbar. Mit Playwright kannst du jede Lovable App in statisches HTML rendern – automatis…

    Weiterlesen
    Futuristische Code-Editor-Fenster mit Turquoise- und Blue-Akzenten auf dunklem Hintergrund
    10. März 20266 min

    Wir sind keine Webagentur – und das ist der Punkt

    Ihr sucht eine Webagentur? Dann seid ihr bei uns falsch. Ihr sucht jemanden, der euer digitales Problem löst? Dann seid …

    Weiterlesen
    Gegenüberstellung von No-Code-Bausteinen und klassischem Custom-Code auf geteiltem Bildschirm
    25. Juni 20252 min

    No-Code vs. Custom Development – Wann reicht was? (Entscheidungshilfe 2026)

    No-Code, Vibe Coding oder Custom Development? Wir zeigen, wann welcher Ansatz sinnvoll ist – mit konkreten Beispielen un…

    Weiterlesen
    Schachfiguren als Metapher für den Plattformkonflikt zwischen Anthropic und Lovable
    14. April 20263 min

    Anthropic baut einen App-Builder – und greift Europas Vibe-Coding-Star Lovable an

    Geleakte Screenshots zeigen einen integrierten App-Builder in Claude. Was das für Lovable, das europäische Startup-Ökosy…

    Weiterlesen
    AI Website Builder Vergleich – Framer, Webflow AI, Wix AI, Durable und Lovable-Stack im SEO-Test
    10. April 20265 min

    AI Website Builder im Vergleich: Framer vs. Webflow AI vs. Wix AI vs. Durable vs. Lovable-Stack

    Fünf Wege zur Website im SEO-Vergleich: Framer, Webflow AI, Wix AI, Durable – und der Lovable + GitHub + Vercel-Stack. W…

    Weiterlesen