feat(seo): implement AI SEO and structured data schema
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 49s
Build & Deploy / 🧪 QA (push) Successful in 2m27s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m28s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 49s
Build & Deploy / 🧪 QA (push) Successful in 2m27s
CI - Lint, Typecheck & Test / quality-assurance (pull_request) Successful in 3m28s
Build & Deploy / 🏗️ Build (push) Successful in 3m13s
Build & Deploy / 🚀 Deploy (push) Successful in 47s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
- 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)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<Thing> | WithContext<Thing>[];
|
||||
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 (
|
||||
<script
|
||||
id={id}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(schemaData),
|
||||
__html: JSON.stringify(finalSchema),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
64
components/blocks/FaqBlock.tsx
Normal file
64
components/blocks/FaqBlock.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { getFAQSchema } from '@/lib/schema';
|
||||
import JsonLd from '@/components/JsonLd';
|
||||
|
||||
export interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FaqBlockProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
questions: FAQItem[];
|
||||
}
|
||||
|
||||
export const FaqBlock: React.FC<FaqBlockProps> = ({
|
||||
title = "Häufig gestellte Fragen",
|
||||
subtitle,
|
||||
questions
|
||||
}) => {
|
||||
if (!questions || questions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="mb-10 text-center">
|
||||
{subtitle && (
|
||||
<span className="text-sm font-bold text-accent uppercase tracking-wider block mb-2">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-text-primary">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{questions.map((item, index) => (
|
||||
<details
|
||||
key={index}
|
||||
className="group border border-neutral-200 bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<summary className="flex items-center justify-between cursor-pointer p-6 font-semibold text-lg text-text-primary hover:text-primary transition-colors select-none">
|
||||
{item.question}
|
||||
<span className="ml-4 flex-shrink-0 transition-transform duration-300 group-open:-rotate-180 text-accent">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</summary>
|
||||
<div className="px-6 pb-6 text-text-secondary leading-relaxed border-t border-neutral-100 pt-4 prose prose-p:my-2 prose-a:text-primary">
|
||||
{/* Parse simple HTML or just text if answer has formatting */}
|
||||
<div dangerouslySetInnerHTML={{ __html: item.answer }} />
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inject FAQ Schema automatically */}
|
||||
<JsonLd data={getFAQSchema(questions)} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -178,3 +178,26 @@ description: Willkommen bei E-TIB GmbH
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<FaqBlock
|
||||
title="Häufig gestellte Fragen"
|
||||
subtitle="Wissenswertes"
|
||||
questions={[
|
||||
{
|
||||
question: "Was macht die E-TIB GmbH?",
|
||||
answer: "Die E-TIB GmbH ist spezialisiert auf den Leitungs- und Kabeltiefbau. Zu den Kernleistungen zählen der klassische Kabelbau, hochpräzise Horizontalspülbohrungen, Kabelpflugarbeiten sowie Elektromontagen bis 110 kV. Zusammen mit unseren Tochtergesellschaften decken wir auch die komplette Planung und Dokumentation von Infrastrukturprojekten ab."
|
||||
},
|
||||
{
|
||||
question: "Welche Vorteile bietet das Horizontalspülbohrverfahren (Bohrtechnik)?",
|
||||
answer: "Die Horizontalspülbohrung ist ein grabenloses Verfahren zur unterirdischen Verlegung von Kabeln und Rohren. Der größte Vorteil ist die oberflächenschonende Arbeitsweise: Flüsse, Autobahnen, Bahntrassen oder Naturschutzgebiete können gekreuzt werden, ohne den Verkehr oder die Umwelt zu beeinträchtigen. Dies spart erhebliche Kosten bei der Oberflächenwiederherstellung."
|
||||
},
|
||||
{
|
||||
question: "Seit wann gibt es die E-TIB GmbH und wie groß ist das Team?",
|
||||
answer: "Die E-TIB GmbH wurde im Dezember 2015 in Guben gegründet. Heute beschäftigt die Unternehmensgruppe über 50 qualifizierte Mitarbeiter, die komplexe Infrastruktur- und Tiefbauprojekte für Energieversorger und Telekommunikationsanbieter bundesweit realisieren."
|
||||
},
|
||||
{
|
||||
question: "Bieten Sie auch die Planung von Netzanschlüssen an?",
|
||||
answer: "Ja, über die E-TIB Ingenieurgesellschaft mbH übernehmen wir die komplette Genehmigungs- und Ausführungsplanung. Dies beinhaltet Elektro- und Netzanschlussplanungen, Machbarkeitsstudien sowie die finale digitale Bestandsdokumentation per GIS."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -135,3 +135,30 @@ layout: "fullBleed"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Kabelbau",
|
||||
"description": "Klassischer Grabenbau und professionelle Verlegung von Mittel- und Niederspannungskabeln.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Horizontalspülbohrungen",
|
||||
"description": "Horizontalspülbohrverfahren (HDD) für grabenlose, oberflächenschonende Verlegung.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Glasfaserausbau",
|
||||
"description": "Ausbau von zukunftssicheren Breitbandnetzen (FTTX) und Glasfaser-Kabelmontagen.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
},
|
||||
{
|
||||
"@type": "Service",
|
||||
"name": "Planung und Vermessung",
|
||||
"description": "Genehmigungs- und Ausführungsplanung, Elektro- und Netzanschlussplanung, sowie digitale Bestandsdokumentation per GIS.",
|
||||
"provider": { "@type": "Organization", "name": "E-TIB GmbH" }
|
||||
}
|
||||
]} />
|
||||
|
||||
@@ -27,3 +27,72 @@ layout: "fullBleed"
|
||||
</div>
|
||||
|
||||
<CallToAction />
|
||||
|
||||
<JsonLd data={[
|
||||
{
|
||||
"@type": "Event",
|
||||
"name": "Intersolar München",
|
||||
"startDate": "2026-06-19",
|
||||
"endDate": "2026-06-21",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "Messe München",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "München",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "E-TIB GmbH",
|
||||
"url": "https://e-tib.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Event",
|
||||
"name": "Windenergietage Linstow",
|
||||
"startDate": "2026-11-04",
|
||||
"endDate": "2026-11-06",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "Van der Valk Resort Linstow",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Linstow",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "E-TIB GmbH",
|
||||
"url": "https://e-tib.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Event",
|
||||
"name": "Kabelwerkstatt Wiesbaden",
|
||||
"startDate": "2026-12-02",
|
||||
"endDate": "2026-12-03",
|
||||
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||
"eventStatus": "https://schema.org/EventScheduled",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "RheinMain CongressCenter",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Wiesbaden",
|
||||
"addressCountry": "DE"
|
||||
}
|
||||
},
|
||||
"organizer": {
|
||||
"@type": "Organization",
|
||||
"name": "E-TIB GmbH",
|
||||
"url": "https://e-tib.com"
|
||||
}
|
||||
}
|
||||
]} />
|
||||
|
||||
@@ -34,3 +34,85 @@ export const getBreadcrumbSchema = (items: { name: string; item: string }[]) =>
|
||||
item: item.item.startsWith('http') ? item.item : `${SITE_URL}${item.item}`,
|
||||
})),
|
||||
});
|
||||
|
||||
export const getLocalBusinessSchema = () => ({
|
||||
'@context': 'https://schema.org' as const,
|
||||
'@type': 'LocalBusiness' as const,
|
||||
name: 'E-TIB GmbH',
|
||||
url: SITE_URL,
|
||||
logo: LOGO_URL,
|
||||
image: LOGO_URL,
|
||||
description: 'Ihr Partner für Kabelleitungstiefbau, Horizontalspülbohrungen, Planung und Vermessung in Guben und überregional.',
|
||||
telephone: '+49-15207230518',
|
||||
email: 'd.joseph@e-tib.com',
|
||||
address: {
|
||||
'@type': 'PostalAddress' as const,
|
||||
streetAddress: 'Gewerbestraße 22',
|
||||
addressLocality: 'Guben',
|
||||
postalCode: '03172',
|
||||
addressCountry: 'DE',
|
||||
},
|
||||
geo: {
|
||||
'@type': 'GeoCoordinates' as const,
|
||||
latitude: 51.9547, // Approx coordinates for Guben area
|
||||
longitude: 14.7175,
|
||||
},
|
||||
openingHoursSpecification: [
|
||||
{
|
||||
'@type': 'OpeningHoursSpecification' as const,
|
||||
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
opens: '08:00',
|
||||
closes: '17:00',
|
||||
},
|
||||
],
|
||||
sameAs: ['https://www.instagram.com/me.and.eloise/'],
|
||||
});
|
||||
|
||||
export const getFAQSchema = (questions: { question: string; answer: string }[]) => ({
|
||||
'@context': 'https://schema.org' as const,
|
||||
'@type': 'FAQPage' as const,
|
||||
mainEntity: questions.map((q) => ({
|
||||
'@type': 'Question' as const,
|
||||
name: q.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer' as const,
|
||||
text: q.answer,
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
export const getServiceSchema = (serviceName: string, description: string, image?: string) => ({
|
||||
'@context': 'https://schema.org' as const,
|
||||
'@type': 'Service' as const,
|
||||
name: serviceName,
|
||||
description: description,
|
||||
provider: {
|
||||
'@type': 'Organization' as const,
|
||||
name: 'E-TIB GmbH',
|
||||
},
|
||||
...(image && { image }),
|
||||
});
|
||||
|
||||
export const getEventSchema = (event: { name: string; startDate: string; endDate?: string; locationName: string; locationAddress?: string; url?: string }) => ({
|
||||
'@context': 'https://schema.org' as const,
|
||||
'@type': 'Event' as const,
|
||||
name: event.name,
|
||||
startDate: event.startDate,
|
||||
...(event.endDate && { endDate: event.endDate }),
|
||||
location: {
|
||||
'@type': 'Place' as const,
|
||||
name: event.locationName,
|
||||
...(event.locationAddress && {
|
||||
address: {
|
||||
'@type': 'PostalAddress' as const,
|
||||
streetAddress: event.locationAddress,
|
||||
},
|
||||
}),
|
||||
},
|
||||
organizer: {
|
||||
'@type': 'Organization' as const,
|
||||
name: 'E-TIB GmbH',
|
||||
url: SITE_URL,
|
||||
},
|
||||
...(event.url && { url: event.url }),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user