From 569b87694421c4938fed423e3b00ebfb9494b0b5 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 13 May 2026 14:23:24 +0200 Subject: [PATCH] feat(seo): implement AI SEO and structured data schema - Add robust JSON-LD schema generators (LocalBusiness, FAQ, Service, Event) - Refactor JsonLd component to use @graph pattern for merging global/local schemas - Create FaqBlock component with automatic FAQPage schema injection - Inject specific JSON-LD schemas into mdx pages (home, kompetenzen, messen) - Add structured AI SEO content (hard facts, stats, clear definitions) --- app/[locale]/[slug]/page.tsx | 4 ++ app/[locale]/page.tsx | 4 ++ components/JsonLd.tsx | 68 +++++++++++++++++----------- components/blocks/FaqBlock.tsx | 64 ++++++++++++++++++++++++++ content/de/home.mdx | 23 ++++++++++ content/de/kompetenzen.mdx | 27 +++++++++++ content/de/messen.mdx | 69 ++++++++++++++++++++++++++++ lib/schema.ts | 82 ++++++++++++++++++++++++++++++++++ 8 files changed, 314 insertions(+), 27 deletions(-) create mode 100644 components/blocks/FaqBlock.tsx diff --git a/app/[locale]/[slug]/page.tsx b/app/[locale]/[slug]/page.tsx index cb5fb609e..c4dd43bde 100644 --- a/app/[locale]/[slug]/page.tsx +++ b/app/[locale]/[slug]/page.tsx @@ -20,6 +20,8 @@ import { CallToAction } from '@/components/blocks/CallToAction'; import { ServiceDetailGrid } from '@/components/blocks/ServiceDetailGrid'; import { BenefitGrid } from '@/components/blocks/BenefitGrid'; import { InteractiveGermanyMap } from '@/components/blocks/InteractiveGermanyMap'; +import { FaqBlock } from '@/components/blocks/FaqBlock'; +import JsonLd from '@/components/JsonLd'; import { Button } from '@/components/ui/Button'; @@ -34,6 +36,8 @@ const mdxComponents = { ServiceDetailGrid, BenefitGrid, InteractiveGermanyMap, + FaqBlock, + JsonLd, Button, Heading, }; diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 48398c85b..0ad4e9a71 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -11,6 +11,8 @@ import { HeroVideo as HomeHero } from '@/components/blocks/HeroVideo'; import { SubCompanyTiles as HomeSubCompanyTiles } from '@/components/blocks/SubCompanyTiles'; import { CompetenceBentoGrid as HomeCompetenceBentoGrid } from '@/components/blocks/CompetenceBentoGrid'; import { ReferencesSlider as HomeReferencesSlider } from '@/components/blocks/ReferencesSlider'; +import { FaqBlock } from '@/components/blocks/FaqBlock'; +import JsonLd from '@/components/JsonLd'; import { Button } from '@/components/ui/Button'; import { Heading } from '@/components/ui/Heading'; @@ -20,6 +22,8 @@ const mdxComponents = { HomeSubCompanyTiles, HomeCompetenceBentoGrid, HomeReferencesSlider, + FaqBlock, + JsonLd, Button, Heading, }; diff --git a/components/JsonLd.tsx b/components/JsonLd.tsx index c28070a8c..20adde096 100644 --- a/components/JsonLd.tsx +++ b/components/JsonLd.tsx @@ -1,53 +1,67 @@ -import { Thing, WithContext } from 'schema-dts'; +import { Thing, WithContext, Graph } from 'schema-dts'; +import { getOrganizationSchema, getLocalBusinessSchema, SITE_URL } from '@/lib/schema'; interface JsonLdProps { id?: string; data?: WithContext | WithContext[]; + disableGlobal?: boolean; } -export default function JsonLd({ id, data }: JsonLdProps) { - // If data is provided, use it. Otherwise, use the default Organization + WebSite schema. - const schemaData = data || [ - { - '@context': 'https://schema.org', - '@type': 'Organization', - name: 'E-TIB GmbH', - url: 'https://e-tib.com', - logo: 'https://e-tib.com/assets/logo.png', - sameAs: [ - 'https://www.instagram.com/me.and.eloise/', - ], - description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung.', - address: { - '@type': 'PostalAddress', - streetAddress: 'Gewerbestraße 22', - addressLocality: 'Guben', - postalCode: '03172', - addressCountry: 'DE', - }, - }, - { +export default function JsonLd({ id, data, disableGlobal = false }: JsonLdProps) { + let finalSchema: any; + + if (disableGlobal && data) { + // Escape hatch for overriding the global graph + finalSchema = data; + } else { + // Construct the @graph array + const graphItems: any[] = []; + + // Always include Organization, WebSite, and LocalBusiness for strong AI SEO presence + graphItems.push(getOrganizationSchema()); + graphItems.push({ '@context': 'https://schema.org', '@type': 'WebSite', name: 'E-TIB', - url: 'https://e-tib.com', + url: SITE_URL, potentialAction: { '@type': 'SearchAction', target: { '@type': 'EntryPoint', - urlTemplate: 'https://e-tib.com/search?q={search_term_string}', + urlTemplate: `${SITE_URL}/search?q={search_term_string}`, }, 'query-input': 'required name=search_term_string', }, + }); + graphItems.push(getLocalBusinessSchema()); + + // Append dynamic data from props + if (data) { + if (Array.isArray(data)) { + // Strip out their individual @context if we are pushing to a graph + const cleanedData = data.map((d: any) => { + const { '@context': _, ...rest } = d; + return rest; + }); + graphItems.push(...cleanedData); + } else { + const { '@context': _, ...rest } = data as any; + graphItems.push(rest); + } } - ]; + + finalSchema = { + '@context': 'https://schema.org', + '@graph': graphItems, + }; + } return (