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

View File

@@ -1,44 +1,50 @@
import { Download } from 'lucide-react';
import fs from 'fs';
import path from 'path';
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');
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 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 !== '');
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(' ');
const remainingText = lines.slice(1).join(" ");
if (remainingText) currentSection.content.push(remainingText);
}
} else if (currentSection) {
// Continuation of current section
const blockText = lines.join(' ');
const blockText = lines.join(" ");
if (blockText) currentSection.content.push(blockText);
}
});
@@ -49,7 +55,7 @@ export default function AGB() {
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);
lastSection.content = lastSection.content.filter((c) => c !== footer);
if (sections[sections.length - 1].title === footer) {
sections.pop();
}
@@ -57,12 +63,14 @@ export default function AGB() {
}
return (
<div className="bg-slate-50 min-h-screen pt-28 pb-20">
<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>
<h1 className="text-4xl font-extrabold text-primary mb-2">
{title}
</h1>
<p className="text-slate-500 font-medium">{stand}</p>
</div>
<a
@@ -74,11 +82,13 @@ export default function AGB() {
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>
<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>

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>
);
}

View File

@@ -1,9 +1,9 @@
'use client';
"use client";
import { useEffect } from 'react';
import { motion } from 'framer-motion';
import { RefreshCcw, Home } from 'lucide-react';
import Link from 'next/link';
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,
@@ -27,17 +27,19 @@ export default function Error({
>
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>
<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()}
@@ -46,7 +48,10 @@ export default function Error({
<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">
<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>

View File

@@ -1,11 +1,11 @@
'use client';
"use client";
import { motion } from 'framer-motion';
import { TechBackground } from '@/components/TechBackground';
import { motion } from "framer-motion";
import { TechBackground } from "@/components/TechBackground";
export default function Legal() {
return (
<div className="bg-slate-50 min-h-screen pt-28 pb-20 relative overflow-hidden">
<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
@@ -16,23 +16,32 @@ export default function Legal() {
>
<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>
<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>
<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 />
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>
<h2 className="text-xl font-bold text-primary mb-4">
Vertreten durch
</h2>
<p>
Michael Bodemer<br />
Michael Bodemer
<br />
Klaus Mintel
</p>
</div>
@@ -40,24 +49,48 @@ export default function Legal() {
<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>
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>
<h2 className="text-xl font-bold text-primary mb-4">
Registereintrag
</h2>
<p>
Eintragung im Handelsregister.<br />
Registergericht: Amtsgericht Stuttgart<br />
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>
<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.
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>

View File

@@ -3,7 +3,8 @@ 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.",
description:
"Haben Sie Fragen zu einem Projekt oder benötigen Sie technische Beratung? Wir freuen uns auf Ihre Nachricht.",
};
export default function Page() {

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

@@ -1,8 +1,8 @@
'use client';
"use client";
import Link from 'next/link';
import { motion } from 'framer-motion';
import { Home, ArrowLeft } from 'lucide-react';
import Link from "next/link";
import { motion } from "framer-motion";
import { Home, ArrowLeft } from "lucide-react";
export default function NotFound() {
return (
@@ -16,23 +16,26 @@ export default function NotFound() {
>
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>
<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.
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
<button
onClick={() => window.history.back()}
className="btn-primary bg-slate-100 !text-primary hover:bg-slate-200 flex items-center gap-2"
>

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,
},
);
}

View File

@@ -3,7 +3,8 @@ 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.",
description:
"Ihr spezialisierter Partner für herstellerneutrale technische Beratung und Projektbegleitung bei Energiekabelprojekten bis 110 kV.",
};
export default function Page() {

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

@@ -3,7 +3,8 @@ 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.",
description:
"Erfahren Sie mehr über MB Grid Solutions, unsere Expertise und unser Manifest für technische Exzellenz.",
};
export default function Page() {

View File

@@ -1,56 +1,122 @@
import { NextResponse } from 'next/server';
import * as nodemailer from 'nodemailer';
import { NextResponse } from "next/server";
import * as nodemailer from "nodemailer";
import directus, { ensureAuthenticated } from "@/lib/directus";
import { createItem } from "@directus/sdk";
import { getServerAppServices } from "@/lib/services/create-services.server";
export async function POST(req: Request) {
const services = getServerAppServices();
const logger = services.logger.child({ action: "contact_submission" });
try {
const { name, email, company, message, website } = await req.json();
// Honeypot check
if (website) {
console.log('Spam detected (honeypot)');
return NextResponse.json({ message: 'Ok' });
logger.info("Spam detected (honeypot)");
return NextResponse.json({ message: "Ok" });
}
// Validation
if (!name || name.length < 2 || name.length > 100) {
return NextResponse.json({ error: 'Ungültiger Name' }, { status: 400 });
return NextResponse.json({ error: "Ungültiger Name" }, { status: 400 });
}
if (!email || !/^\S+@\S+\.\S+$/.test(email)) {
return NextResponse.json({ error: 'Ungültige E-Mail' }, { status: 400 });
return NextResponse.json({ error: "Ungültige E-Mail" }, { status: 400 });
}
if (!message || message.length < 20 || message.length > 4000) {
return NextResponse.json({ error: 'Nachricht zu kurz oder zu lang' }, { status: 400 });
if (!message || message.length < 20) {
return NextResponse.json({ error: "message_too_short" }, { status: 400 });
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
if (message.length > 4000) {
return NextResponse.json({ error: "message_too_long" }, { status: 400 });
}
await transporter.sendMail({
from: process.env.SMTP_FROM,
to: process.env.CONTACT_RECIPIENT,
replyTo: email,
subject: `Kontaktanfrage von ${name}`,
text: `
// 1. Directus save
let directusSaved = false;
try {
await ensureAuthenticated();
await directus.request(
createItem("contact_submissions", {
name,
email,
company: company || "Nicht angegeben",
message,
}),
);
logger.info("Contact submission saved to Directus");
directusSaved = true;
} catch (directusError) {
logger.error("Failed to save to Directus", { error: directusError });
services.errors.captureException(directusError, {
phase: "directus_save",
});
// We still try to send the email even if Directus fails
}
// 2. Email sending
try {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || "587"),
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
await transporter.sendMail({
from: process.env.SMTP_FROM,
to: process.env.CONTACT_RECIPIENT || "info@mb-grid-solutions.com",
replyTo: email,
subject: `Kontaktanfrage von ${name}`,
text: `
Name: ${name}
Firma: ${company || 'Nicht angegeben'}
Firma: ${company || "Nicht angegeben"}
E-Mail: ${email}
Zeitpunkt: ${new Date().toISOString()}
Nachricht:
${message}
`,
});
`,
});
return NextResponse.json({ message: 'Ok' });
logger.info("Email sent successfully");
// Notify success for important leads
await services.notifications.notify({
title: "📩 Neue Kontaktanfrage",
message: `Anfrage von ${name} (${email}) erhalten.\nFirma: ${company || "Nicht angegeben"}`,
priority: 5,
});
} catch (smtpError) {
logger.error("SMTP Error", { error: smtpError });
services.errors.captureException(smtpError, { phase: "smtp_send" });
// If Directus failed AND SMTP failed, then we really have a problem
if (!directusSaved) {
return NextResponse.json(
{ error: "Systemfehler (Speicherung und Versand fehlgeschlagen)" },
{ status: 500 },
);
}
// If Directus was successful, we tell the user "Ok" but we know internally it was a partial failure
await services.notifications.notify({
title: "🚨 SMTP Fehler (Kontaktformular)",
message: `Anfrage von ${name} (${email}) in Directus gespeichert, aber E-Mail-Versand fehlgeschlagen: ${smtpError instanceof Error ? smtpError.message : String(smtpError)}`,
priority: 8,
});
}
return NextResponse.json({ message: "Ok" });
} catch (error) {
console.error('SMTP Error:', error);
return NextResponse.json({ error: 'Interner Serverfehler' }, { status: 500 });
logger.error("Global API Error", { error });
services.errors.captureException(error, { phase: "api_global" });
return NextResponse.json(
{ error: "Interner Serverfehler" },
{ status: 500 },
);
}
}

View File

@@ -1,33 +0,0 @@
export default function Privacy() {
return (
<div className="bg-slate-50 min-h-screen pt-28 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>
);
}

View File

@@ -10,14 +10,18 @@
--color-text-main: #0f172a;
--color-text-muted: #64748b;
--color-border: #e2e8f0;
--font-sans: var(--font-inter), -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-sans:
var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--shadow-soft: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
--shadow-card: 0 10px 15px -3px rgb(0 0 0 / 0.03), 0 4px 6px -4px rgb(0 0 0 / 0.03);
--shadow-soft:
0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
--shadow-card:
0 10px 15px -3px rgb(0 0 0 / 0.03), 0 4px 6px -4px rgb(0 0 0 / 0.03);
}
:root {
@@ -43,7 +47,11 @@
}
.grid-pattern {
background-image: radial-gradient(circle, var(--color-border) 1px, transparent 1px);
background-image: radial-gradient(
circle,
var(--color-border) 1px,
transparent 1px
);
background-size: 40px 40px;
}
@@ -56,7 +64,11 @@
background-image:
radial-gradient(at 0% 0%, rgba(16, 185, 129, 0.05) 0px, transparent 50%),
radial-gradient(at 100% 0%, rgba(15, 23, 42, 0.05) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(16, 185, 129, 0.05) 0px, transparent 50%),
radial-gradient(
at 100% 100%,
rgba(16, 185, 129, 0.05) 0px,
transparent 50%
),
radial-gradient(at 0% 100%, rgba(15, 23, 42, 0.05) 0px, transparent 50%);
}
@@ -78,7 +90,7 @@
}
.tech-card-border::before {
content: '';
content: "";
@apply absolute -inset-px bg-gradient-to-br from-accent/20 via-transparent to-accent/20 rounded-[inherit] opacity-0 transition-opacity duration-500;
}
@@ -86,11 +98,20 @@
@apply opacity-100;
}
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-bold tracking-tight text-primary;
text-wrap: balance;
}
button {
@apply cursor-pointer;
}
section {
@apply py-20 md:py-32;
}
@@ -102,11 +123,11 @@
}
.btn-primary {
@apply inline-flex items-center justify-center px-6 py-3 rounded-lg bg-primary text-white font-semibold transition-all hover:bg-primary-light hover:shadow-lg active:scale-[0.98] disabled:opacity-50;
@apply inline-flex items-center justify-center px-6 py-3 rounded-lg bg-primary text-white font-semibold transition-all hover:bg-primary-light hover:shadow-lg active:scale-[0.98] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-accent {
@apply inline-flex items-center justify-center px-6 py-3 rounded-lg bg-accent text-white font-semibold transition-all hover:bg-accent-hover hover:shadow-lg active:scale-[0.98] disabled:opacity-50;
@apply inline-flex items-center justify-center px-6 py-3 rounded-lg bg-accent text-white font-semibold transition-all hover:bg-accent-hover hover:shadow-lg active:scale-[0.98] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed;
}
.glass-panel {

View File

@@ -1,95 +0,0 @@
import Layout from "@/components/Layout";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
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 function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
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="de" className={`${inter.variable}`}>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body className="antialiased">
<Layout>
{children}
</Layout>
</body>
</html>
);
}

View File

@@ -1,175 +0,0 @@
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,
}
);
}

View File

@@ -1,175 +0,0 @@
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,
}
);
}