feat: Integrate Directus CMS, add i18n with next-intl, and configure project tooling with pnpm, husky, and commitlint.**

This commit is contained in:
2026-02-05 01:18:06 +01:00
parent 765cfd4c69
commit e80140f7cf
65 changed files with 12793 additions and 5879 deletions

108
app/[locale]/agb/page.tsx Normal file
View File

@@ -0,0 +1,108 @@
import { Download } from "lucide-react";
import fs from "fs";
import path from "path";
export default function AGB() {
const filePath = path.join(process.cwd(), "context/agbs.md");
const fileContent = fs.readFileSync(filePath, "utf8");
// Split by double newlines to get major blocks (headers + their first paragraphs, or subsequent paragraphs)
const blocks = fileContent
.split(/\n\s*\n/)
.map((b) => b.trim())
.filter((b) => b !== "");
const title = blocks[0] || "Liefer- und Zahlungsbedingungen";
const stand = blocks[1] || "Stand Januar 2026";
const sections: { title: string; content: string[] }[] = [];
let currentSection: { title: string; content: string[] } | null = null;
// Skip title and stand
blocks.slice(2).forEach((block) => {
const lines = block
.split("\n")
.map((l) => l.trim())
.filter((l) => l !== "");
if (lines.length === 0) return;
const firstLine = lines[0];
if (/^\d+\./.test(firstLine)) {
// New section
if (currentSection) sections.push(currentSection);
currentSection = { title: firstLine, content: [] };
// If there are more lines in this block, they form the first paragraph(s)
if (lines.length > 1) {
// Join subsequent lines as they might be part of the same paragraph
// In this MD, we'll assume lines in the same block belong together
// unless they are clearly separate paragraphs (but we already split by double newline)
const remainingText = lines.slice(1).join(" ");
if (remainingText) currentSection.content.push(remainingText);
}
} else if (currentSection) {
// Continuation of current section
const blockText = lines.join(" ");
if (blockText) currentSection.content.push(blockText);
}
});
if (currentSection) sections.push(currentSection);
// The last block is the footer
const footer = blocks[blocks.length - 1];
if (sections.length > 0) {
const lastSection = sections[sections.length - 1];
if (lastSection.content.includes(footer) || lastSection.title === footer) {
lastSection.content = lastSection.content.filter((c) => c !== footer);
if (sections[sections.length - 1].title === footer) {
sections.pop();
}
}
}
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
<div className="container-custom">
<div className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
<div>
<h1 className="text-4xl font-extrabold text-primary mb-2">
{title}
</h1>
<p className="text-slate-500 font-medium">{stand}</p>
</div>
<a
href="/assets/AGB MB Grid 1-2026.pdf"
download
className="btn-primary !py-3 !px-6 flex items-center gap-2"
>
<Download size={18} />
Als PDF herunterladen
</a>
</div>
<div className="space-y-8 text-slate-600 leading-relaxed">
{sections.map((section, index) => (
<div key={index}>
<h2 className="text-2xl font-bold text-primary mb-4">
{section.title}
</h2>
<div className="space-y-4">
{section.content.map((paragraph, pIndex) => (
<p key={pIndex}>{paragraph}</p>
))}
</div>
</div>
))}
<div className="pt-8 border-t border-slate-100">
<p className="font-bold text-primary">{footer}</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
export default function Privacy() {
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20">
<div className="container-custom">
<div className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100">
<h1 className="text-4xl font-extrabold text-primary mb-8">
Datenschutzerklärung
</h1>
<div className="space-y-8 text-slate-600 leading-relaxed">
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
1. Datenschutz auf einen Blick
</h2>
<p>
Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir
behandeln Ihre personenbezogenen Daten vertraulich und
entsprechend der gesetzlichen Datenschutzvorschriften sowie
dieser Datenschutzerklärung.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
2. Hosting
</h2>
<p>
Unsere Website wird bei Hetzner Online GmbH gehostet. Der
Serverstandort ist Deutschland. Wir haben einen Vertrag über
Auftragsverarbeitung (AVV) mit Hetzner geschlossen.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
3. Kontaktformular
</h2>
<p>
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen,
werden Ihre Angaben aus dem Anfrageformular inklusive der von
Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der
Anfrage und für den Fall von Anschlussfragen bei uns
gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung
weiter.
</p>
</div>
<div>
<h2 className="text-2xl font-bold text-primary mb-4">
4. Server-Log-Dateien
</h2>
<p>
Der Provider der Seiten erhebt und speichert automatisch
Informationen in sogenannten Server-Log-Dateien, die Ihr Browser
automatisch an uns übermittelt. Dies sind: Browsertyp und
Browserversion, verwendetes Betriebssystem, Referrer URL,
Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage,
IP-Adresse.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

63
app/[locale]/error.tsx Normal file
View File

@@ -0,0 +1,63 @@
"use client";
import { useEffect } from "react";
import { motion } from "framer-motion";
import { RefreshCcw, Home } from "lucide-react";
import Link from "next/link";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="min-h-[80vh] flex items-center justify-center px-4">
<div className="text-center">
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="text-9xl font-extrabold text-slate-200 mb-8"
>
500
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.5 }}
>
<h1 className="text-4xl font-bold text-primary mb-4">
Etwas ist schiefgelaufen
</h1>
<p className="text-slate-600 text-lg mb-12 max-w-md mx-auto">
Es gab ein technisches Problem. Wir arbeiten bereits an der Lösung.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button
onClick={() => reset()}
className="btn-accent flex items-center gap-2"
>
<RefreshCcw size={18} />
Erneut versuchen
</button>
<Link
href="/"
className="btn-primary bg-slate-100 !text-primary hover:bg-slate-200 flex items-center gap-2"
>
<Home size={18} />
Zur Startseite
</Link>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,101 @@
"use client";
import { motion } from "framer-motion";
import { TechBackground } from "@/components/TechBackground";
export default function Legal() {
return (
<div className="bg-slate-50 min-h-screen pt-40 pb-20 relative overflow-hidden">
<TechBackground />
<div className="container-custom relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 50, damping: 20 }}
className="max-w-4xl mx-auto bg-white p-8 md:p-12 rounded-[2.5rem] shadow-sm border border-slate-100 relative overflow-hidden group"
>
<div className="tech-corner top-8 left-8 border-t-2 border-l-2 opacity-20" />
<div className="tech-corner bottom-8 right-8 border-b-2 border-r-2 opacity-20" />
<h1 className="text-4xl font-extrabold text-primary mb-8 relative z-10">
Impressum
</h1>
<div className="space-y-8 text-slate-600 leading-relaxed relative z-10">
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Angaben gemäß § 5 TMG
</h2>
<p>
MB Grid Solutions & Services GmbH
<br />
Raiffeisenstraße 22
<br />
73630 Remshalden
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Vertreten durch
</h2>
<p>
Michael Bodemer
<br />
Klaus Mintel
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">Kontakt</h2>
<p>
E-Mail:{" "}
<a
href="mailto:info@mb-grid-solutions.com"
className="text-accent hover:underline"
>
info@mb-grid-solutions.com
</a>
<br />
Web:{" "}
<a
href="https://www.mb-grid-solutions.com"
className="text-accent hover:underline"
>
www.mb-grid-solutions.com
</a>
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Registereintrag
</h2>
<p>
Eintragung im Handelsregister.
<br />
Registergericht: Amtsgericht Stuttgart
<br />
Registernummer: HRB 803379
</p>
</div>
<div>
<h2 className="text-xl font-bold text-primary mb-4">
Urheberrecht
</h2>
<p>
Alle auf der Website veröffentlichten Texte, Bilder und
sonstigen Informationen unterliegen sofern nicht anders
gekennzeichnet dem Urheberrecht. Jede Vervielfältigung,
Verbreitung, Speicherung, Übermittlung, Wiedergabe bzw.
Weitergabe der Inhalte ohne schriftliche Genehmigung ist
ausdrücklich untersagt.
</p>
</div>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { Metadata } from "next";
import ContactContent from "@/components/ContactContent";
export const metadata: Metadata = {
title: "Kontakt",
description:
"Haben Sie Fragen zu einem Projekt oder benötigen Sie technische Beratung? Wir freuen uns auf Ihre Nachricht.",
};
export default function Page() {
return <ContactContent />;
}

123
app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,123 @@
import Layout from "@/components/Layout";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "../globals.css";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
export const metadata: Metadata = {
metadataBase: new URL("https://www.mb-grid-solutions.com"),
title: {
default: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung",
template: "%s | MB Grid Solutions",
},
description:
"Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV. Expertise in Mittel- und Hochspannungsnetzen.",
keywords: [
"Energiekabel",
"Hochspannung",
"Mittelspannung",
"Kabelprojekte",
"Technische Beratung",
"Engineering",
"Energiewende",
"110 kV",
],
authors: [{ name: "MB Grid Solutions & Services GmbH" }],
creator: "MB Grid Solutions & Services GmbH",
publisher: "MB Grid Solutions & Services GmbH",
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
type: "website",
locale: "de_DE",
url: "https://www.mb-grid-solutions.com",
siteName: "MB Grid Solutions",
title: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung",
description:
"Spezialisierter Partner für Energiekabelprojekte bis 110 kV. Herstellerneutrale technische Beratung und Projektbegleitung.",
},
twitter: {
card: "summary_large_image",
title: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung",
description: "Spezialisierter Partner für Energiekabelprojekte bis 110 kV.",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Validate that the incoming `locale` is supported
if (locale !== "de") {
notFound();
}
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
name: "MB Grid Solutions & Services GmbH",
url: "https://www.mb-grid-solutions.com",
logo: "https://www.mb-grid-solutions.com/assets/logo.png",
description:
"Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.",
address: {
"@type": "PostalAddress",
streetAddress: "Raiffeisenstraße 22",
addressLocality: "Remshalden",
postalCode: "73630",
addressCountry: "DE",
},
contactPoint: {
"@type": "ContactPoint",
email: "info@mb-grid-solutions.com",
contactType: "customer service",
},
};
return (
<html lang={locale} className={`${inter.variable}`}>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body className="antialiased">
<NextIntlClientProvider messages={messages}>
<Layout>{children}</Layout>
</NextIntlClientProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { Home, ArrowLeft } from "lucide-react";
export default function NotFound() {
return (
<div className="min-h-[80vh] flex items-center justify-center px-4">
<div className="text-center">
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="text-9xl font-extrabold text-slate-200 mb-8"
>
404
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.5 }}
>
<h1 className="text-4xl font-bold text-primary mb-4">
Seite nicht gefunden
</h1>
<p className="text-slate-600 text-lg mb-12 max-w-md mx-auto">
Die von Ihnen gesuchte Seite scheint nicht zu existieren oder wurde
verschoben.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link href="/" className="btn-primary flex items-center gap-2">
<Home size={18} />
Zur Startseite
</Link>
<button
onClick={() => window.history.back()}
className="btn-primary bg-slate-100 !text-primary hover:bg-slate-200 flex items-center gap-2"
>
<ArrowLeft size={18} />
Zurück
</button>
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,175 @@
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt =
"MB Grid Solutions | Energiekabelprojekte & Technische Beratung";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default async function Image() {
return new ImageResponse(
<div
style={{
background: "#ffffff",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
position: "relative",
fontFamily: "sans-serif",
}}
>
{/* Grid Pattern Background - matching .grid-pattern in globals.css */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage:
"radial-gradient(circle, #e2e8f0 1.5px, transparent 1.5px)",
backgroundSize: "40px 40px",
zIndex: 0,
}}
/>
{/* Content Container - matching .card-modern / .glass-panel style */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(255, 255, 255, 0.95)",
padding: "60px 80px",
borderRadius: "48px",
border: "1px solid #e2e8f0",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.1)",
zIndex: 1,
position: "relative",
}}
>
{/* Engineering Excellence Badge */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
padding: "8px 20px",
backgroundColor: "rgba(16, 185, 129, 0.1)",
borderRadius: "100px",
marginBottom: "32px",
}}
>
<div
style={{
width: "10px",
height: "10px",
backgroundColor: "#10b981",
borderRadius: "50%",
}}
/>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
color: "#10b981",
textTransform: "uppercase",
letterSpacing: "0.1em",
}}
>
Engineering Excellence
</div>
</div>
{/* Brand Mark */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100px",
height: "100px",
backgroundColor: "#0f172a",
borderRadius: "24px",
marginBottom: "32px",
boxShadow: "0 10px 15px -3px rgba(15, 23, 42, 0.3)",
}}
>
<div
style={{
fontSize: "48px",
fontWeight: "bold",
color: "#10b981",
}}
>
MB
</div>
</div>
{/* Title */}
<div
style={{
fontSize: "72px",
fontWeight: "900",
color: "#0f172a",
marginBottom: "16px",
textAlign: "center",
letterSpacing: "-0.02em",
}}
>
MB Grid <span style={{ color: "#10b981" }}>Solutions</span>
</div>
{/* Subtitle */}
<div
style={{
fontSize: "32px",
fontWeight: "500",
color: "#64748b",
textAlign: "center",
maxWidth: "800px",
lineHeight: 1.4,
}}
>
Energiekabelprojekte & Technische Beratung
<br />
bis 110 kV
</div>
</div>
{/* Tech Lines - matching .tech-line style */}
<div
style={{
position: "absolute",
top: "10%",
left: 0,
width: "200px",
height: "1px",
backgroundColor: "rgba(16, 185, 129, 0.2)",
}}
/>
<div
style={{
position: "absolute",
bottom: "15%",
right: 0,
width: "300px",
height: "1px",
backgroundColor: "rgba(16, 185, 129, 0.2)",
}}
/>
</div>,
{
...size,
},
);
}

12
app/[locale]/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { Metadata } from "next";
import HomeContent from "@/components/HomeContent";
export const metadata: Metadata = {
title: "MB Grid Solutions | Energiekabelprojekte & Technische Beratung",
description:
"Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.",
};
export default function Page() {
return <HomeContent />;
}

View File

@@ -0,0 +1,175 @@
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt =
"MB Grid Solutions | Energiekabelprojekte & Technische Beratung";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default async function Image() {
return new ImageResponse(
<div
style={{
background: "#ffffff",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
position: "relative",
fontFamily: "sans-serif",
}}
>
{/* Grid Pattern Background */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage:
"radial-gradient(circle, #e2e8f0 1.5px, transparent 1.5px)",
backgroundSize: "40px 40px",
zIndex: 0,
}}
/>
{/* Content Container */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(255, 255, 255, 0.95)",
padding: "60px 80px",
borderRadius: "48px",
border: "1px solid #e2e8f0",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.1)",
zIndex: 1,
position: "relative",
}}
>
{/* Engineering Excellence Badge */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "12px",
padding: "8px 20px",
backgroundColor: "rgba(16, 185, 129, 0.1)",
borderRadius: "100px",
marginBottom: "32px",
}}
>
<div
style={{
width: "10px",
height: "10px",
backgroundColor: "#10b981",
borderRadius: "50%",
}}
/>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
color: "#10b981",
textTransform: "uppercase",
letterSpacing: "0.1em",
}}
>
Engineering Excellence
</div>
</div>
{/* Brand Mark */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100px",
height: "100px",
backgroundColor: "#0f172a",
borderRadius: "24px",
marginBottom: "32px",
boxShadow: "0 10px 15px -3px rgba(15, 23, 42, 0.3)",
}}
>
<div
style={{
fontSize: "48px",
fontWeight: "bold",
color: "#10b981",
}}
>
MB
</div>
</div>
{/* Title */}
<div
style={{
fontSize: "72px",
fontWeight: "900",
color: "#0f172a",
marginBottom: "16px",
textAlign: "center",
letterSpacing: "-0.02em",
}}
>
MB Grid <span style={{ color: "#10b981" }}>Solutions</span>
</div>
{/* Subtitle */}
<div
style={{
fontSize: "32px",
fontWeight: "500",
color: "#64748b",
textAlign: "center",
maxWidth: "800px",
lineHeight: 1.4,
}}
>
Energiekabelprojekte & Technische Beratung
<br />
bis 110 kV
</div>
</div>
{/* Tech Lines */}
<div
style={{
position: "absolute",
top: "10%",
left: 0,
width: "200px",
height: "1px",
backgroundColor: "rgba(16, 185, 129, 0.2)",
}}
/>
<div
style={{
position: "absolute",
bottom: "15%",
right: 0,
width: "300px",
height: "1px",
backgroundColor: "rgba(16, 185, 129, 0.2)",
}}
/>
</div>,
{
...size,
},
);
}

View File

@@ -0,0 +1,12 @@
import { Metadata } from "next";
import AboutContent from "@/components/AboutContent";
export const metadata: Metadata = {
title: "Über uns",
description:
"Erfahren Sie mehr über MB Grid Solutions, unsere Expertise und unser Manifest für technische Exzellenz.",
};
export default function Page() {
return <AboutContent />;
}