This commit is contained in:
Engin committed 2026-09-10 14:31:05 +02:00
1 parent cc2b84dcad
commit 3bda7d6eb6
28 files changed
+2330 -49

No files matched your search

@@ -0,0 +1,343 @@
.hero {
background: var(--color-background-linear);
padding-block: var(--space-16);
position: relative;
overflow: hidden;
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: left;
}
.decoration {
position: absolute;
top: var(--space-8);
right: var(--space-8);
display: none;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: var(--space-3);
pointer-events: none;
z-index: 0;
}
.dot {
width: 14px;
height: 14px;
border-radius: var(--radius-full);
background-color: var(--color-accent);
opacity: 0.8;
}
.heading {
color: var(--color-heading);
max-width: 20ch;
display: flex;
flex-direction: column;
gap: var(--space-1);
text-align: left;
align-self: left;
position: relative;
z-index: 2;
}
.headingSub {
font-size: var(--font-size-subhead);
font-weight: var(--font-weight-medium);
letter-spacing: var(--letter-spacing-tight);
}
.content {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.imageWrapper {
display: none;
position: relative;
width: 100%;
margin-inline: auto;
aspect-ratio: 21 / 9;
flex-shrink: 0;
z-index: 1;
}
.imageWrapper::before {
content: "";
position: absolute;
left: 8%;
right: 8%;
bottom: -4px;
height: 28%;
background: color-mix(in srgb, var(--color-accent) 55%, transparent);
filter: blur(22px);
opacity: 0.45;
border-radius: 999px;
z-index: -1;
pointer-events: none;
}
.imageFrame {
position: relative;
width: 100%;
height: 100%;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.08),
0 24px 60px rgba(0, 0, 0, 0.06);
}
.image {
object-fit: cover;
}
.body {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-6);
text-align: left;
}
.lead {
font-size: var(--font-size-base);
text-align: left;
width: min(100%, 48ch);
}
/* --- Challenges Section: 1:1 Optik von MaintenanceCleaningSection --- */
.challengesSection {
background: var(--color-background-linear);
padding-block: var(--space-16);
}
.challengesInner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.challengesHeading {
max-width: 24ch;
padding: 0 0 8px 0;
color: var(--color-heading);
}
.challengesLead {
margin-top: var(--space-3);
font-size: var(--font-size-base);
max-width: 48ch;
}
.challengesList {
margin-top: var(--space-6);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-5);
list-style: none;
padding: 0;
}
.challengesItem {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: var(--space-4);
background: var(--color-surface-muted, #e6ede9);
border-radius: 12px;
padding: var(--space-5);
}
.challengesIcon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
color: var(--color-accent);
}
.challengesText {
color: var(--font-heading-color);
font-size: var(--font-size-xs);
}
/* --- Related Services Section --- */
.servicesSection {
padding-block: var(--space-16);
}
.servicesInner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.servicesHeading {
max-width: 32ch;
color: var(--color-heading);
}
.servicesLead {
margin-top: var(--space-3);
max-width: 60ch;
font-size: var(--font-size-base);
}
.servicesGrid {
margin-top: var(--space-8);
display: grid;
grid-template-columns: 1fr;
gap: var(--space-6);
}
.serviceCard {
display: flex;
flex-direction: column;
border-radius: var(--radius-lg);
overflow: hidden;
background: #fff;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.06),
0 2px 8px rgba(0, 0, 0, 0.04);
text-decoration: none;
color: inherit;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.serviceCard:hover {
transform: translateY(-4px);
box-shadow:
0 16px 40px rgba(0, 0, 0, 0.1),
0 4px 12px rgba(0, 0, 0, 0.06);
}
.serviceCardImageWrapper {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
}
.serviceCardImage {
object-fit: cover;
}
.serviceCardBody {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.serviceCardTitle {
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
color: var(--color-heading);
}
.serviceCardLink {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--font-size-small, 0.875rem);
font-weight: var(--font-weight-medium);
color: var(--color-accent);
}
/* Tablet: 768px */
@media (min-width: 768px) {
.content {
flex-direction: row;
align-items: flex-start;
gap: var(--space-8);
}
.inner {
align-items: flex-start;
gap: var(--space-8);
}
.heading {
text-align: left;
align-self: flex-start;
}
.body {
align-items: flex-start;
text-align: left;
}
.lead {
text-align: left;
margin-inline: 0;
}
.decoration {
display: grid;
top: -24px;
}
.imageWrapper {
display: block;
width: 260px;
aspect-ratio: 4 / 3;
}
.dot {
width: 24px;
height: 24px;
}
.challengesSection {
padding-block: var(--space-24);
}
.challengesHeading,
.challengesLead {
text-align: left;
}
.challengesList {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.servicesSection {
padding-block: var(--space-24);
}
.servicesGrid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Desktop: 1024px */
@media (min-width: 1024px) {
.hero {
padding-block: var(--space-36);
}
.decoration {
top: -42px;
}
.challengesInner {
padding-inline: var(--space-6);
}
.challengesList {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
+213 -13
View File
@@ -1,34 +1,234 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { branchen } from "../data";
import { leistungen } from "../../leistungen/data";
import Image from "next/image"; import Image from "next/image";
import { branchenloesungen } from "../data"; import Link from "next/link";
import {
ArrowRightIcon,
CheckCircleIcon,
} from "@phosphor-icons/react/dist/ssr";
import styles from "./page.module.css";
import StaggeredGallery from "@/components/StaggeredGallery";
import FaqSection from "@/components/faq/FaqSection";
import { branchenFaq } from "@/components/faq/faqData";
import ContactCtaSection from "@/components/contact/ContactCtaSection";
import BentoGallery from "@/components/BentoGallery";
import type { Metadata } from "next";
const siteUrl = "https://basiclean.de";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const branche = branchen.find((item) => item.slug === slug);
if (!branche) {
return {
title: "Nicht gefunden",
robots: { index: false, follow: false },
};
}
const hasCustomSeo = Boolean(branche.metaTitle);
const title = hasCustomSeo
? { absolute: branche.metaTitle as string }
: branche.titleMain;
const flatTitle = hasCustomSeo
? (branche.metaTitle as string)
: branche.titleMain;
const description = branche.metaDescription ?? branche.text;
const url = `${siteUrl}/branchenloesungen/${branche.slug}`;
const ogImageUrl = branche.ogImage ?? branche.image;
return {
title,
description,
alternates: {
canonical: url,
},
openGraph: {
type: "website",
siteName: "Basiclean",
locale: "de_DE",
title: flatTitle,
description,
url,
images: [
{
url: ogImageUrl,
alt: branche.imageAlt,
width: 1200,
height: 628,
},
],
},
twitter: {
card: "summary_large_image",
site: "@basiclean",
title: flatTitle,
description,
images: [{ url: ogImageUrl, alt: branche.imageAlt }],
},
};
}
export function generateStaticParams() { export function generateStaticParams() {
return branchenloesungen.map((item) => ({ return branchen.map((item) => ({
slug: item.slug, slug: item.slug,
})); }));
} }
export default async function LeistungDetail({ export default async function BrancheDetail({
params, params,
}: { }: {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}) { }) {
const { slug } = await params; const { slug } = await params;
const leistung = branchenloesungen.find((item) => item.slug === slug);
if (!leistung) notFound(); const branche = branchen.find((item) => item.slug === slug);
const faqEntry = branchenFaq.find((item) => item.slug === branche?.faqSlug);
if (!branche) {
notFound();
}
const empfohleneLeistungen = branche.relatedServices.slugs
.map((leistungSlug) =>
leistungen.find((item) => item.slug === leistungSlug),
)
.filter((item): item is (typeof leistungen)[number] => Boolean(item));
return ( return (
<main> <main>
<h1>{leistung.title}</h1> <section className={styles.hero} aria-labelledby="hero-heading">
<p>{leistung.text}</p> <div className={styles.inner}>
<div className={styles.decoration} aria-hidden="true">
{Array.from({ length: 18 }).map((_, i) => (
<span key={i} className={styles.dot} />
))}
</div>
<Image <h1 id="hero-heading" className={styles.heading}>
src={leistung.image} {branche.titleMain}
alt={leistung.title} <span className={styles.headingSub}>{branche.titleSub}</span>
width={1200} </h1>
height={800}
/> <div className={styles.content}>
<div className={styles.imageWrapper}>
<div className={styles.imageFrame}>
<Image
src={branche.image}
alt={branche.imageAlt}
fill
className={styles.image}
sizes="(max-width: 640px) 100vw, 260px"
/>
</div>
</div>
<div className={styles.body}>
<p className={`lead ${styles.lead}`}>{branche.text}</p>
<Link href="/kontakt" className="btn btn--filled">
<span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={18} weight="bold" />
</span>
Jetzt Angebot anfragen
</Link>
</div>
</div>
</div>
</section>
<BentoGallery images={branche.gallery} />
<section
className={styles.challengesSection}
aria-labelledby="challenges-heading"
>
<div className={styles.challengesInner}>
<h2 id="challenges-heading" className={styles.challengesHeading}>
{branche.challengesSection.heading}
</h2>
<p className={`lead ${styles.challengesLead}`}>
{branche.challengesSection.lead}
</p>
<div className={styles.challengesList}>
{branche.challengesSection.points.map((point) => (
<article key={point} className={styles.challengesItem}>
<span className={styles.challengesIcon} aria-hidden="true">
<CheckCircleIcon size={24} weight="duotone" />
</span>
<p className={styles.challengesText}>{point}</p>
</article>
))}
</div>
</div>
</section>
{empfohleneLeistungen.length > 0 && (
<section className={styles.servicesSection}>
<div className={styles.servicesInner}>
<h2 className={styles.servicesHeading}>
{branche.relatedServices.heading}
</h2>
<p className={styles.servicesLead}>
{branche.relatedServices.lead}
</p>
<div className={styles.servicesGrid}>
{empfohleneLeistungen.map((leistung) => (
<Link
key={leistung.slug}
href={`/leistungen/${leistung.slug}`}
className={styles.serviceCard}
>
<div className={styles.serviceCardImageWrapper}>
<Image
src={leistung.image}
alt={leistung.imageAlt}
fill
className={styles.serviceCardImage}
sizes="(max-width: 640px) 100vw, 320px"
/>
</div>
<div className={styles.serviceCardBody}>
<h3 className={styles.serviceCardTitle}>
{leistung.titleMain}
</h3>
<span className={styles.serviceCardLink}>
Mehr erfahren
<ArrowRightIcon
size={16}
weight="bold"
aria-hidden="true"
/>
</span>
</div>
</Link>
))}
</div>
</div>
</section>
)}
<section>
<FaqSection
title={faqEntry?.title ?? "Häufige Fragen"}
items={faqEntry?.faq ?? []}
/>
</section>
<section>
<ContactCtaSection {...branche.contactCtaSection} />
</section>
</main> </main>
); );
} }
+256 -16
View File
@@ -1,26 +1,266 @@
export const branchenloesungen = [ export const branchen = [
{ {
slug: "bueros", slug: "bueros",
title: "Büros", titleMain: "Reinigung für Büros",
text: "Glas- und Fensterreinigung", titleSub: "in Hannover und Umgebung",
image: "/images/leistungen/webdesign.jpg", text: "Ein gepflegtes Büro prägt den Eindruck bei Kunden, Bewerbern und dem eigenen Team. Wir reinigen Arbeitsplätze, Besprechungsräume, Empfangsbereiche und Sanitäranlagen zuverlässig und abgestimmt auf Ihre Betriebsabläufe.",
}, image: "/images/branchen/buero.jpg",
{ imageAlt: "Gepflegter Büroarbeitsplatz nach der Reinigung",
slug: "kanzlei",
title: "Kanzlei", // --- SEO ---
text: "Unterhaltsreinigung", metaTitle: "Büroreinigung Hannover – für Unternehmen jeder Größe",
image: "/images/leistungen/seo.jpg", metaDescription:
"Professionelle Reinigung für Büros in Hannover: Arbeitsplätze, Empfang und Sanitärbereiche zuverlässig gepflegt. Individuelle Konzepte. Jetzt anfragen.",
ogImage: "/images/branchen/buero.jpg",
challengesSection: {
heading: "Typische Anforderungen in Büroumgebungen",
lead: "Büros werden täglich stark frequentiert – von Mitarbeitenden, Kunden und Besuchern. Das stellt besondere Anforderungen an Sauberkeit, Hygiene und diskrete Abstimmung mit dem laufenden Betrieb.",
points: [
"Hohe Publikumsfrequenz an Empfang und in Besprechungsräumen",
"Sensible Bereiche wie Serverräume oder Aktenlager erfordern besondere Sorgfalt",
"Reinigungszeiten müssen sich in bestehende Arbeitsabläufe einfügen",
"Regelmäßige Hygiene an Kontaktflächen wie Türgriffen und Tastaturen",
"Repräsentative Sauberkeit in Kunden- und Empfangsbereichen",
],
},
relatedServices: {
heading: "Passende Leistungen für Ihr Büro",
lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihr Bürogebäude.",
slugs: [
"unterhaltsreinigung",
"fensterreinigung",
"treppenhausreinigung",
],
},
faqSlug: "bueros",
contactCtaSection: {
heading: "Lassen Sie uns über die Reinigung Ihres Büros sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Büroräume in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
gallery: [
{
src: "/images/branchen/gallery/1.jpg",
alt: "Modernes Büro mit Arbeitsplätzen",
},
{
src: "/images/branchen/gallery/2.jpg",
alt: "Empfangsbereich eines Bürogebäudes",
},
{
src: "/images/branchen/gallery/3.jpg",
alt: "Besprechungsraum in einem Büro",
},
{
src: "/images/branchen/gallery/4.jpg",
alt: "Gemeinschaftsküche im Büro",
},
{
src: "/images/branchen/gallery/5.jpg",
alt: "Flur mit Glasfronten im Bürogebäude",
},
{
src: "/images/branchen/gallery/6.jpg",
alt: "Sanitärbereich in einem Bürogebäude",
},
],
}, },
{ {
slug: "hallen-und-lagerflaechen", slug: "hallen-und-lagerflaechen",
title: "Hallen und Lagerflächen", titleMain: "Reinigung für Hallen und Lagerflächen",
text: "Treppenhausreinigung", titleSub: "in Hannover und Umgebung",
image: "/images/leistungen/branding.jpg", text: "Große Flächen, hohe Belastung und industrielle Anforderungen: Wir reinigen Hallen, Lager und Produktionsflächen zuverlässig und mit dem passenden Equipment für großflächige und stark beanspruchte Böden.",
image: "/images/branchen/hallen-lagerflaechen.jpg",
imageAlt: "Saubere Lagerhalle nach professioneller Reinigung",
// --- SEO ---
metaTitle: "Hallenreinigung Hannover – Lager & Produktionsflächen",
metaDescription:
"Reinigung von Hallen und Lagerflächen in Hannover: großflächig, belastbar und termingerecht. Professionelles Equipment für Industrieböden. Jetzt anfragen.",
ogImage: "/images/branchen/hallen-lagerflaechen.jpg",
challengesSection: {
heading: "Typische Anforderungen in Hallen und Lagerflächen",
lead: "Hallen und Lagerflächen unterliegen anderen Bedingungen als klassische Büroräume – große Flächen, Staub, Verkehr durch Gabelstapler und wechselnde Verschmutzungsgrade erfordern ein angepasstes Konzept.",
points: [
"Großflächige Böden erfordern effizientes, maschinelles Vorgehen",
"Staub- und Schmutzbelastung durch Warenumschlag und Betriebsabläufe",
"Reinigungszeiten müssen sich an Schichtbetrieb und Lieferverkehr anpassen",
"Sicherheitsvorgaben bei laufendem Gabelstapler- und Warenverkehr",
"Unterschiedliche Bodenbeläge von Industrieboden bis Estrich",
],
},
relatedServices: {
heading: "Passende Leistungen für Ihre Halle",
lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihre Hallen- und Lagerflächen.",
slugs: ["grundreinigung", "unterhaltsreinigung", "hausmeisterdienste"],
},
faqSlug: "hallen-und-lagerflaechen",
contactCtaSection: {
heading: "Lassen Sie uns über die Reinigung Ihrer Halle sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Hallen- und Lagerflächen in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
gallery: [
{
src: "/images/branchen/gallery/1.jpg",
alt: "Lagerhalle mit Regalsystemen",
},
{
src: "/images/branchen/gallery/2.jpg",
alt: "Industrieboden in einer Produktionshalle",
},
{
src: "/images/branchen/gallery/3.jpg",
alt: "Gabelstapler in einer Lagerhalle",
},
{
src: "/images/branchen/gallery/4.jpg",
alt: "Verladebereich einer Lagerhalle",
},
{
src: "/images/branchen/gallery/5.jpg",
alt: "Hochregallager von innen",
},
{
src: "/images/branchen/gallery/halle-6.jpg",
alt: "Produktionsfläche in einer Industriehalle",
},
],
}, },
{ {
slug: "wohn-und-geschaeftshaeuser", slug: "wohn-und-geschaeftshaeuser",
title: "Wohn- und Geschäftshäuse", titleMain: "Reinigung für Wohn- und Geschäftshäuser",
text: "Performante E-Commerce-Lösungen.", titleSub: "in Hannover und Umgebung",
image: "/images/leistungen/shop.jpg", text: "Gepflegte Treppenhäuser, Eingangsbereiche und Gemeinschaftsflächen sind die Visitenkarte jeder Immobilie. Wir betreuen Wohn- und Geschäftshäuser zuverlässig und mit einem festen Ansprechpartner für Verwaltungen und Eigentümer.",
image: "/images/branchen/wohn-geschaeftshaeuser.jpg",
imageAlt: "Gepflegtes Treppenhaus in einem Wohn- und Geschäftshaus",
// --- SEO ---
metaTitle: "Hausreinigung Hannover – Wohn- & Geschäftshäuser",
metaDescription:
"Reinigung für Wohn- und Geschäftshäuser in Hannover: Treppenhäuser, Eingänge und Gemeinschaftsflächen gepflegt. Fester Ansprechpartner. Jetzt anfragen.",
ogImage: "/images/branchen/wohn-geschaeftshaeuser.jpg",
challengesSection: {
heading: "Typische Anforderungen bei Wohn- und Geschäftshäusern",
lead: "In Mehrparteienhäusern treffen unterschiedliche Interessen von Bewohnern, Mietern und Eigentümern aufeinander. Das erfordert verlässliche Abläufe und klare Kommunikation mit der Hausverwaltung.",
points: [
"Regelmäßige Betreuung von Treppenhäusern, Fluren und Eingangsbereichen",
"Abstimmung mit Hausverwaltungen und Eigentümergemeinschaften",
"Unterschiedliche Erwartungen von Mietern und Gewerbetreibenden im selben Objekt",
"Saisonale Aufgaben wie Laubentfernung oder Winterdienst im Außenbereich",
"Diskrete Reinigungszeiten mit möglichst geringer Störung der Bewohner",
],
},
relatedServices: {
heading: "Passende Leistungen für Ihre Immobilie",
lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihr Wohn- oder Geschäftshaus.",
slugs: ["treppenhausreinigung", "hausmeisterdienste", "fensterreinigung"],
},
faqSlug: "wohn-und-geschaeftshaeuser",
contactCtaSection: {
heading: "Lassen Sie uns über die Reinigung Ihrer Immobilie sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihr Wohn- oder Geschäftshaus in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
gallery: [
{
src: "/images/branchen/gallery/1.jpg",
alt: "Treppenhaus in einem Wohnhaus",
},
{
src: "/images/branchen/gallery/2.jpg",
alt: "Eingangsbereich eines Geschäftshauses",
},
{
src: "/images/branchen/gallery/3.jpg",
alt: "Fassade eines Wohn- und Geschäftshauses",
},
{
src: "/images/branchen/gallery/4.jpg",
alt: "Gemeinschaftsflur in einem Mehrfamilienhaus",
},
{
src: "/images/branchen/gallery/5.jpg",
alt: "Briefkastenanlage im Eingangsbereich",
},
{
src: "/images/branchen/gallery/6.jpg",
alt: "Außenanlage eines Wohnhauses",
},
],
},
{
slug: "kanzlei",
titleMain: "Reinigung für Kanzleien",
titleSub: "in Hannover und Umgebung",
text: "In Kanzleien zählen Diskretion, Sorgfalt und ein makelloses Erscheinungsbild. Wir reinigen Empfangsbereiche, Besprechungsräume und Büros zuverlässig – mit geschultem Personal und höchster Sorgfalt im Umgang mit vertraulichen Unterlagen.",
image: "/images/branchen/kanzlei.jpg",
imageAlt: "Gepflegter Empfangsbereich einer Kanzlei",
// --- SEO ---
metaTitle: "Kanzleireinigung Hannover – diskret & zuverlässig",
metaDescription:
"Reinigung für Kanzleien in Hannover: Empfang, Besprechungsräume und Büros diskret gepflegt. Geschultes Personal, feste Ansprechpartner. Jetzt anfragen.",
ogImage: "/images/branchen/kanzlei.jpg",
challengesSection: {
heading: "Typische Anforderungen in Kanzleien",
lead: "Kanzleien stellen besondere Anforderungen an Vertraulichkeit und Zuverlässigkeit. Reinigungskräfte bewegen sich in sensiblen Räumen mit vertraulichen Unterlagen und benötigen entsprechend geschultes Personal.",
points: [
"Vertrauliche Unterlagen erfordern besondere Sorgfalt und Diskretion",
"Repräsentatives Erscheinungsbild in Empfang und Besprechungsräumen",
"Feste, verlässliche Ansprechpartner für langfristige Zusammenarbeit",
"Reinigungszeiten außerhalb der Kanzleistunden zur Wahrung der Vertraulichkeit",
"Sorgfältiger Umgang mit hochwertiger Ausstattung und Mobiliar",
],
},
relatedServices: {
heading: "Passende Leistungen für Ihre Kanzlei",
lead: "Diese Leistungen kombinieren wir je nach Bedarf für Ihre Kanzleiräume.",
slugs: ["unterhaltsreinigung", "fensterreinigung", "grundreinigung"],
},
faqSlug: "kanzlei",
contactCtaSection: {
heading: "Lassen Sie uns über die Reinigung Ihrer Kanzlei sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Kanzleiräume in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
gallery: [
{
src: "/images/branchen/gallery/1.jpg",
alt: "Empfangsbereich einer Kanzlei",
},
{
src: "/images/branchen/gallery/2.jpg",
alt: "Besprechungsraum in einer Kanzlei",
},
{
src: "/images/branchen/gallery/3.jpg",
alt: "Büro mit Aktenregalen in einer Kanzlei",
},
{
src: "/images/branchen/gallery/4.jpg",
alt: "Wartebereich in einer Kanzlei",
},
{
src: "/images/branchen/gallery/5.jpg",
alt: "Schreibtisch in einem Kanzleibüro",
},
{
src: "/images/branchen/gallery/6.jpg",
alt: "Konferenzraum einer Kanzlei",
},
],
}, },
]; ];
+93
View File
@@ -0,0 +1,93 @@
"use server";
import { transporter } from "@/lib/mailer";
export interface ContactFormState {
success: boolean;
error?: string;
fieldErrors?: Partial<Record<
"unternehmen" | "vorname" | "nachname" | "email" | "telefon" | "nachricht" | "datenschutz",
string
>>;
}
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Erlaubt Ziffern, Leerzeichen, +, -, /, Klammern - deckt gängige Telefonformate ab.
const PHONE_REGEX = /^[0-9+\-/() ]{6,20}$/;
export async function sendContactMessage(
_prevState: ContactFormState,
formData: FormData,
): Promise<ContactFormState> {
const unternehmen = (formData.get("unternehmen") as string | null)?.trim() ?? "";
const vorname = (formData.get("vorname") as string | null)?.trim() ?? "";
const nachname = (formData.get("nachname") as string | null)?.trim() ?? "";
const email = (formData.get("email") as string | null)?.trim() ?? "";
const telefon = (formData.get("telefon") as string | null)?.trim() ?? "";
const nachricht = (formData.get("nachricht") as string | null)?.trim() ?? "";
const datenschutz = formData.get("datenschutz");
const fieldErrors: ContactFormState["fieldErrors"] = {};
if (!vorname) fieldErrors.vorname = "Bitte geben Sie Ihren Vornamen an.";
if (!nachname) fieldErrors.nachname = "Bitte geben Sie Ihren Nachnamen an.";
if (!email) {
fieldErrors.email = "Bitte geben Sie Ihre E-Mail-Adresse an.";
} else if (!EMAIL_REGEX.test(email)) {
fieldErrors.email = "Bitte geben Sie eine gültige E-Mail-Adresse an.";
}
if (!telefon) {
fieldErrors.telefon = "Bitte geben Sie Ihre Telefonnummer an.";
} else if (!PHONE_REGEX.test(telefon)) {
fieldErrors.telefon = "Bitte geben Sie eine gültige Telefonnummer an.";
}
if (!nachricht) {
fieldErrors.nachricht = "Bitte geben Sie eine Nachricht ein.";
}
if (datenschutz !== "on") {
fieldErrors.datenschutz = "Bitte bestätigen Sie die Datenschutzhinweise.";
}
if (Object.keys(fieldErrors).length > 0) {
return { success: false, fieldErrors };
}
try {
await transporter.sendMail({
from: `"Basiclean Kontaktformular" <${process.env.SMTP_USER}>`,
to: process.env.CONTACT_RECEIVER,
replyTo: email,
subject: `Neue Kontaktanfrage von ${vorname} ${nachname}`,
text: [
`Unternehmen: ${unternehmen || "-"}`,
`Name: ${vorname} ${nachname}`,
`E-Mail: ${email}`,
`Telefon: ${telefon}`,
"",
"Nachricht:",
nachricht,
].join("\n"),
html: `
<h2>Neue Kontaktanfrage</h2>
<p><strong>Unternehmen:</strong> ${unternehmen || "-"}</p>
<p><strong>Name:</strong> ${vorname} ${nachname}</p>
<p><strong>E-Mail:</strong> ${email}</p>
<p><strong>Telefon:</strong> ${telefon}</p>
<p><strong>Nachricht:</strong></p>
<p>${nachricht.replace(/\n/g, "<br />")}</p>
`,
});
return { success: true };
} catch (error) {
console.error("Fehler beim Senden der Kontaktanfrage:", error);
return {
success: false,
error: "Die Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.",
};
}
}
+189
View File
@@ -0,0 +1,189 @@
.hero {
background: var(--color-background-linear);
padding-block: var(--space-16);
position: relative;
overflow: hidden;
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
position: relative;
z-index: 1;
}
.decoration {
position: absolute;
top: var(--space-8);
right: var(--space-8);
display: none;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: var(--space-3);
pointer-events: none;
z-index: 0;
}
.dot {
width: 14px;
height: 14px;
border-radius: var(--radius-full);
background-color: var(--color-accent);
opacity: 0.8;
}
.heading {
color: var(--color-heading);
max-width: 24ch;
position: relative;
z-index: 2;
}
.lead {
margin-top: var(--space-3);
font-size: var(--font-size-base);
max-width: 60ch;
position: relative;
z-index: 2;
}
/* --- Form Section --- */
.formSection {
padding-block: var(--space-16);
}
.formInner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: var(--space-2);
padding: var(--space-2) var(--space-2) var(--space-2) 0;
}
/* Links: kein eigener Hintergrund - bleibt leer/weiß */
.formPanel {
padding: var(--space-8) var(--space-6) var(--space-6);
}
/*
Rechts: Hintergrundbild mit dunklem Verlauf zur Lesbarkeit.
Inhalt wird als Flex-Column mit space-between verteilt:
Überschrift oben links, Kontaktdaten unten links.
*/
.infoPanel {
position: relative;
background-image:
linear-gradient(
180deg,
rgb(0 0 0 / 35%) 0%,
rgb(0 0 0 / 20%) 45%,
rgb(0 0 0 / 65%) 100%
),
url("/images/contact.jpg");
background-size: cover;
background-position: center;
padding: var(--space-8) var(--space-6) var(--space-6);
color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 420px;
border-radius: var(--space-2);
}
.infoHeading {
color: #fff;
max-width: 20ch;
}
.infoList {
display: flex;
flex-direction: column;
gap: var(--space-3);
list-style: none;
padding: 0;
}
.infoItem {
display: flex;
flex-direction: column;
gap: 2px;
}
.infoLabel {
font-size: var(--font-size-small, 0.875rem);
font-weight: var(--font-weight-medium);
color: rgb(255 255 255 / 80%);
}
.infoValue {
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
color: #fff;
text-decoration: none;
}
a.infoValue:hover {
text-decoration: underline;
}
/* Tablet: 768px – 50/50 Aufteilung */
@media (min-width: 768px) {
.hero,
.formSection {
padding-block: var(--space-24);
}
.decoration {
display: grid;
top: -24px;
}
.dot {
width: 24px;
height: 24px;
}
.formInner {
flex-direction: row;
align-items: stretch;
background: var(--color-background-linear);
}
.formPanel {
width: 50%;
padding: var(--space-8);
}
.infoPanel {
width: 50%;
padding: var(--space-10) var(--space-8) var(--space-8);
min-height: 480px;
}
}
/* Desktop: 1024px – zurück auf 40/60 */
@media (min-width: 1024px) {
.hero {
padding-block: var(--space-36);
}
.decoration {
top: -42px;
}
.formPanel {
width: 40%;
padding: var(--space-10);
}
.infoPanel {
width: 60%;
padding: var(--space-12) var(--space-10) var(--space-10);
min-height: 560px;
}
}
+73 -9
View File
@@ -1,10 +1,74 @@
export default function Kontakt() { import type { Metadata } from "next";
return <>
<h1>Gebäudereinigung für ein makelloses Arbeitsumfeld</h1> import styles from "./page.module.css";
<h2>Gebäudereinigung für ein makelloses Arbeitsumfeld</h2> import ContactForm from "@/components/ContactForm";
<h3>Gebäudereinigung für ein makelloses Arbeitsumfeld</h3> import FaqSection from "@/components/faq/FaqSection";
<h4>Gebäudereinigung für ein makelloses Arbeitsumfeld</h4> import { homepageFaq } from "@/components/faq/faqData";
<h5>Gebäudereinigung für ein makelloses Arbeitsumfeld</h5>
<h6>Gebäudereinigung für ein makelloses Arbeitsumfeld</h6> export const metadata: Metadata = {
</>; title: "Kontakt",
description:
"Kontaktieren Sie Basiclean für ein individuelles Angebot zu Reinigung und Hausmeisterdiensten in Hannover und Umgebung.",
};
export default function Page() {
return (
<main>
<section className={styles.hero} aria-labelledby="hero-heading">
<div className={styles.inner}>
<div className={styles.decoration} aria-hidden="true">
{Array.from({ length: 18 }).map((_, i) => (
<span key={i} className={styles.dot} />
))}
</div>
<h1 id="hero-heading" className={styles.heading}>
Kontaktieren Sie uns
</h1>
<p className={`lead ${styles.lead}`}>
Sie haben Fragen oder möchten ein individuelles Angebot anfragen?
Füllen Sie das Formular aus – wir melden uns schnellstmöglich bei
Ihnen zurück.
</p>
</div>
</section>
<section className={styles.formSection}>
<div className={styles.formInner}>
<div className={styles.formPanel}>
<ContactForm />
</div>
<div className={styles.infoPanel}>
<h2 className={styles.infoHeading}>So erreichen Sie uns</h2>
<ul className={styles.infoList}>
<li className={styles.infoItem}>
<span className={styles.infoLabel}>Telefon</span>
<a href="tel:+4951112345678" className={styles.infoValue}>
0511 123 456 78
</a>
</li>
<li className={styles.infoItem}>
<span className={styles.infoLabel}>E-Mail</span>
<a href="mailto:info@basiclean.de" className={styles.infoValue}>
info@basiclean.de
</a>
</li>
<li className={styles.infoItem}>
<span className={styles.infoLabel}>Adresse</span>
<span className={styles.infoValue}>
Musterstraße 12, 30159 Hannover
</span>
</li>
</ul>
</div>
</div>
</section>
<section>
<FaqSection title="Häufige Fragen" items={homepageFaq} />
</section>
</main>
);
} }
+71 -3
View File
@@ -13,6 +13,74 @@ import FaqSection from "@/components/faq/FaqSection";
import { leistungenFaq } from "@/components/faq/faqData"; import { leistungenFaq } from "@/components/faq/faqData";
import ContactCtaSection from "@/components/contact/ContactCtaSection"; import ContactCtaSection from "@/components/contact/ContactCtaSection";
import type { Metadata } from "next";
const siteUrl = "https://basiclean.de";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const leistung = leistungen.find((item) => item.slug === slug);
if (!leistung) {
return {
title: "Nicht gefunden",
robots: { index: false, follow: false },
};
}
const hasCustomSeo = Boolean(leistung.metaTitle);
// Eigener metaTitle vorhanden -> Suffix "| Basiclean" unterdrücken (title.absolute).
// Kein metaTitle gesetzt -> Fallback auf titleMain, Template aus dem Layout greift normal.
const title = hasCustomSeo
? { absolute: leistung.metaTitle as string }
: leistung.titleMain;
const description = leistung.metaDescription ?? leistung.text;
const url = `${siteUrl}/leistungen/${leistung.slug}`;
const ogImageUrl = leistung.ogImage ?? leistung.image;
// Für OpenGraph/Twitter braucht es einen reinen String-Titel (kein title.absolute-Objekt).
const flatTitle = hasCustomSeo
? (leistung.metaTitle as string)
: leistung.titleMain;
return {
title,
description,
alternates: {
canonical: url,
},
openGraph: {
type: "website",
siteName: "Basiclean",
locale: "de_DE",
title: flatTitle,
description,
url,
images: [
{
url: ogImageUrl,
alt: leistung.imageAlt,
width: 1200,
height: 628,
},
],
},
twitter: {
card: "summary_large_image",
site: "@basiclean",
title: flatTitle,
description,
images: [{ url: ogImageUrl, alt: leistung.imageAlt }],
},
};
}
export function generateStaticParams() { export function generateStaticParams() {
return leistungen.map((item) => ({ return leistungen.map((item) => ({
slug: item.slug, slug: item.slug,
@@ -110,9 +178,9 @@ export default async function LeistungDetail({
/> />
</section> </section>
<section> <section>
<ContactCtaSection {...leistung.contactCtaSection} /> <ContactCtaSection {...leistung.contactCtaSection} />
</section> </section>
</main> </main>
); );
} }
+34
View File
@@ -8,6 +8,12 @@ export const leistungen = [
imageAlt: "Gereinigte Fensterfront eines Geschäftsgebäudes", imageAlt: "Gereinigte Fensterfront eines Geschäftsgebäudes",
titleSlides: "Warum eine professionelle Unterhaltsreinigung wichtig ist", titleSlides: "Warum eine professionelle Unterhaltsreinigung wichtig ist",
// --- SEO ---
metaTitle: "Fensterreinigung Hannover – streifenfrei & zuverlässig",
metaDescription:
"Professionelle Fensterreinigung in Hannover: Fenster, Rahmen und Glasfassaden streifenfrei gereinigt. Individuelle Konzepte, faire Preise. Jetzt anfragen.",
ogImage: "/images/services/Fensterreinigung.jpg",
slides: [ slides: [
{ {
heading: "Klare Sicht für Ihren guten ersten Eindruck", heading: "Klare Sicht für Ihren guten ersten Eindruck",
@@ -118,6 +124,13 @@ export const leistungen = [
image: "/images/services/unterhaltsreinigung.jpg", image: "/images/services/unterhaltsreinigung.jpg",
imageAlt: "Reinigungskraft bei der Unterhaltsreinigung in einem Büro", imageAlt: "Reinigungskraft bei der Unterhaltsreinigung in einem Büro",
titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist",
// --- SEO ---
metaTitle: "Unterhaltsreinigung Hannover – Büros, Praxen & Schulen",
metaDescription:
"Regelmäßige Unterhaltsreinigung in Hannover für Büros, Praxen und Schulen. Feste Termine, individuelle Intervalle, geschultes Personal. Jetzt anfragen.",
ogImage: "/images/slides/unterhaltsreinigung-1.jpg",
slides: [ slides: [
{ {
heading: "Konstante Hygiene im Alltag", heading: "Konstante Hygiene im Alltag",
@@ -228,6 +241,13 @@ export const leistungen = [
image: "/images/services/treppenhausreinigung.jpg", image: "/images/services/treppenhausreinigung.jpg",
imageAlt: "Sauberes Treppenhaus in einem Gewerbeobjekt", imageAlt: "Sauberes Treppenhaus in einem Gewerbeobjekt",
titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist",
// --- SEO ---
metaTitle: "Treppenhausreinigung Hannover – gepflegte Zugänge",
metaDescription:
"Treppenhausreinigung in Hannover für Wohn- und Gewerbeobjekte: Böden, Handläufe und Türen gepflegt in festen Intervallen. Jetzt Angebot anfragen.",
ogImage: "/images/slides/treppenhausreinigung-1.jpg",
slides: [ slides: [
{ {
heading: "Ein gepflegter Empfang beginnt im Treppenhaus", heading: "Ein gepflegter Empfang beginnt im Treppenhaus",
@@ -339,6 +359,13 @@ export const leistungen = [
image: "/images/services/grundreinigung.jpg", image: "/images/services/grundreinigung.jpg",
imageAlt: "Sauberer Büroarbeitsplatz nach der Grundreinigung", imageAlt: "Sauberer Büroarbeitsplatz nach der Grundreinigung",
titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist",
// --- SEO ---
metaTitle: "Grundreinigung Hannover – Tiefenreinigung vom Profi",
metaDescription:
"Professionelle Grundreinigung in Hannover: intensive Tiefenreinigung nach Umzug, Umbau oder Neueröffnung. Passendes Equipment, faire Preise. Jetzt anfragen.",
ogImage: "/images/slides/grundreinigung-1.jpg",
slides: [ slides: [
{ {
heading: "Gründliche Sauberkeit bis ins Detail", heading: "Gründliche Sauberkeit bis ins Detail",
@@ -450,6 +477,13 @@ export const leistungen = [
image: "/images/services/Hausmeisterdienste.jpg", image: "/images/services/Hausmeisterdienste.jpg",
imageAlt: "Hausmeister bei der Außenpflege eines Gebäudes", imageAlt: "Hausmeister bei der Außenpflege eines Gebäudes",
titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist", titleSlides: "Warum professionelle Unterhaltsreinigung wichtig ist",
// --- SEO ---
metaTitle: "Hausmeisterdienste Hannover – Objektbetreuung",
metaDescription:
"Hausmeisterdienste in Hannover: Winterdienst, Grünpflege, Kleinreparaturen und Kontrollgänge aus einer Hand. Fester Ansprechpartner. Jetzt anfragen.",
ogImage: "/images/slides/hausmeisterdienste-1.jpg",
slides: [ slides: [
{ {
heading: "Zuverlässige Betreuung für Ihr Objekt", heading: "Zuverlässige Betreuung für Ihr Objekt",
-7
View File
@@ -1,7 +0,0 @@
export default function Loading() {
return (
<main>
<p>Laden...</p>
</main>
);
}
+207
View File
@@ -0,0 +1,207 @@
.hero {
background: var(--color-background-linear);
padding-block: var(--space-16);
position: relative;
overflow: hidden;
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
position: relative;
z-index: 1;
}
.decoration {
position: absolute;
top: var(--space-8);
right: var(--space-8);
display: none;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: var(--space-3);
pointer-events: none;
z-index: 0;
}
.dot {
width: 14px;
height: 14px;
border-radius: var(--radius-full);
background-color: var(--color-accent);
opacity: 0.8;
}
.heading {
color: var(--color-heading);
max-width: 26ch;
position: relative;
z-index: 2;
}
.lead {
margin-top: var(--space-3);
font-size: var(--font-size-base);
max-width: 65ch;
position: relative;
z-index: 2;
}
/* --- Story Section --- */
.storySection {
padding-block: var(--space-16);
}
.storyInner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.storyText {
max-width: 70ch;
}
.storyHeading {
color: var(--color-heading);
margin-bottom: var(--space-4);
}
.storyLead {
font-size: var(--font-size-base);
margin-bottom: var(--space-4);
}
.storyLead:last-child {
margin-bottom: 0;
}
/* --- Werte Section (Kachel-Design wie MaintenanceCleaningSection) --- */
.werteSection {
background: var(--color-background-linear);
padding-block: var(--space-16);
}
.werteInner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.werteHeading {
max-width: 24ch;
color: var(--color-heading);
}
.werteLead {
margin-top: var(--space-3);
font-size: var(--font-size-base);
max-width: 48ch;
}
.werteGrid {
margin-top: var(--space-8);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-5);
}
.werteCard {
display: flex;
flex-direction: column;
gap: var(--space-3);
background: var(--color-surface-muted, #e6ede9);
border-radius: 12px;
padding: var(--space-5);
}
.werteIcon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: var(--color-accent);
}
.werteCardHeading {
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
color: var(--color-heading);
}
.werteCardText {
font-size: var(--font-size-xs);
color: var(--font-heading-color);
}
/* --- Philosophy Section --- */
.philosophySection {
padding-block: var(--space-16);
}
.philosophyInner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-8);
}
.philosophyCol {
flex: 1;
}
.philosophyHeading {
color: var(--color-heading);
margin-bottom: var(--space-3);
}
.philosophyText {
font-size: var(--font-size-base);
}
/* Tablet: 768px */
@media (min-width: 768px) {
.storySection,
.werteSection,
.philosophySection {
padding-block: var(--space-24);
}
.decoration {
display: grid;
top: -24px;
}
.dot {
width: 24px;
height: 24px;
}
.werteGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.philosophyInner {
flex-direction: row;
gap: var(--space-10);
}
}
/* Desktop: 1024px */
@media (min-width: 1024px) {
.hero {
padding-block: var(--space-36);
}
.decoration {
top: -42px;
}
.werteGrid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
+127 -1
View File
@@ -1,8 +1,134 @@
import type { Metadata } from "next";
import { CheckCircleIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./page.module.css";
import ContactCtaSection from "@/components/contact/ContactCtaSection";
export const metadata: Metadata = {
title: "Über uns",
description:
"Basiclean ist Ihr Partner für professionelle Gebäudereinigung in Hannover. Erfahren Sie mehr über unsere Werte, Ziele und unser Verständnis von Qualität.",
};
const werte = [
{
heading: "Zuverlässigkeit",
text: "Wir halten Termine ein und sorgen für gleichbleibende Qualität – Woche für Woche, ohne Ausnahmen.",
},
{
heading: "Transparenz",
text: "Faire, nachvollziehbare Preise ohne versteckte Kosten. Sie wissen jederzeit, wofür Sie bezahlen.",
},
{
heading: "Sorgfalt",
text: "Wir behandeln Ihr Objekt so, als wäre es unser eigenes – mit Aufmerksamkeit für Details.",
},
{
heading: "Nähe",
text: "Als regionales Unternehmen sind wir persönlich erreichbar und kennen die Anforderungen vor Ort.",
},
];
export default function UeberUns() { export default function UeberUns() {
return ( return (
<main> <main>
<h1>Über Uns</h1> <section className={styles.hero} aria-labelledby="hero-heading">
<div className={styles.inner}>
<div className={styles.decoration} aria-hidden="true">
{Array.from({ length: 18 }).map((_, i) => (
<span key={i} className={styles.dot} />
))}
</div>
<h1 id="hero-heading" className={styles.heading}>
Über Basiclean
</h1>
<p className={`lead ${styles.lead}`}>
Basiclean wurde mit dem Anspruch gegründet, Gebäudereinigung in
Hannover verlässlicher, transparenter und persönlicher zu gestalten.
Wir sind ein junges Unternehmen mit klarem Fokus: saubere Ergebnisse
und langfristige Zusammenarbeit statt austauschbarer
Standarddienstleistung.
</p>
</div>
</section>
<section className={styles.storySection}>
<div className={styles.storyInner}>
<div className={styles.storyText}>
<h2 className={styles.storyHeading}>Wie wir angefangen haben</h2>
<p className={styles.storyLead}>
Basiclean ist ein neu gegründetes Unternehmen für Gebäudereinigung
mit Sitz in Hannover. Der Anstoß war die Beobachtung, dass viele
Reinigungsdienstleistungen unpersönlich und wenig flexibel
organisiert sind. Wir wollten das anders machen: mit festen
Ansprechpartnern, klaren Absprachen und einem Team, das sich
wirklich für das Ergebnis verantwortlich fühlt.
</p>
<p className={styles.storyLead}>
Von Anfang an haben wir uns auf Unterhalts-, Fenster- und
Grundreinigung sowie Hausmeisterdienste für Büros, Wohn- und
Geschäftshäuser, Kanzleien und Gewerbeflächen spezialisiert –
statt alles anzubieten, konzentrieren wir uns auf das, was wir
wirklich gut können.
</p>
</div>
</div>
</section>
<section className={styles.werteSection}>
<div className={styles.werteInner}>
<h2 className={styles.werteHeading}>Was uns wichtig ist</h2>
<p className={`lead ${styles.werteLead}`}>
Diese Grundsätze bestimmen, wie wir arbeiten – bei jedem Auftrag,
unabhängig von Objektgröße oder Branche.
</p>
<div className={styles.werteGrid}>
{werte.map((wert) => (
<article key={wert.heading} className={styles.werteCard}>
<span className={styles.werteIcon} aria-hidden="true">
<CheckCircleIcon size={24} weight="duotone" />
</span>
<h3 className={styles.werteCardHeading}>{wert.heading}</h3>
<p className={styles.werteCardText}>{wert.text}</p>
</article>
))}
</div>
</div>
</section>
<section className={styles.philosophySection}>
<div className={styles.philosophyInner}>
<div className={styles.philosophyCol}>
<h2 className={styles.philosophyHeading}>Unsere Philosophie</h2>
<p className={styles.philosophyText}>
Wir verstehen Reinigung nicht als reine Dienstleistung nach
Checkliste, sondern als Teil der Wahrnehmung Ihres Unternehmens.
Ein gepflegtes Gebäude schafft Vertrauen – bei Kunden,
Mitarbeitenden und Besuchern gleichermaßen. Deshalb passen wir
unsere Konzepte an das jeweilige Objekt an, statt starre
Standardpakete zu verkaufen.
</p>
</div>
<div className={styles.philosophyCol}>
<h2 className={styles.philosophyHeading}>Unsere Ziele</h2>
<p className={styles.philosophyText}>
Kurzfristig wollen wir in Hannover und Umgebung als verlässlicher
Partner für Gebäudereinigung wahrgenommen werden. Langfristig
setzen wir auf nachhaltiges Wachstum durch zufriedene, langjährige
Kunden statt kurzfristiger Einzelaufträge – getragen von einem
Team, das fair behandelt wird und entsprechend motiviert arbeitet.
</p>
</div>
</div>
</section>
<section>
<ContactCtaSection
heading="Lernen Sie uns persönlich kennen"
lead="Sie möchten mehr über Basiclean erfahren oder direkt ein individuelles Angebot anfragen? Wir freuen uns auf Ihre Nachricht."
/>
</section>
</main> </main>
); );
} }
+105
View File
@@ -0,0 +1,105 @@
.gallerySection {
padding-block: var(--space-10);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
/* Mobile: einfache Stapelung */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
}
.item {
position: relative;
}
.frame {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.08),
0 24px 60px rgba(0, 0, 0, 0.06);
}
.image {
object-fit: cover;
transition: transform 0.4s ease;
}
.frame:hover .image {
transform: scale(1.04);
}
/*
Tablet & Desktop ab 768px: Bento-Grid mit benannten Areas.
6 Bilder, ein großes Hauptbild (a) oben links, Rest gestaffelt drumherum.
Jede Area erscheint genau einmal im Template -> kein Bild geht verloren.
*/
@media (min-width: 768px) {
.gallerySection {
padding-block: var(--space-16);
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, minmax(140px, 1fr));
grid-template-areas:
"a a b c"
"a a d d"
"e e f f";
gap: var(--space-4);
}
.item {
height: 100%;
}
.frame {
height: 100%;
aspect-ratio: auto;
}
.item[data-index="0"] {
grid-area: a;
}
.item[data-index="1"] {
grid-area: b;
}
.item[data-index="2"] {
grid-area: c;
}
.item[data-index="3"] {
grid-area: d;
}
.item[data-index="4"] {
grid-area: e;
}
.item[data-index="5"] {
grid-area: f;
}
}
/* Desktop: 1024px – großzügigere, breitere Variante desselben Musters */
@media (min-width: 1024px) {
.gallerySection {
padding-block: var(--space-20);
}
.grid {
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(2, minmax(200px, 1fr));
grid-template-areas:
"a a a b b c"
"a a a d e c";
}
}
+38
View File
@@ -0,0 +1,38 @@
import Image from "next/image";
import styles from "./BentoGallery.module.css";
interface GalleryImage {
src: string;
alt: string;
}
interface BentoGalleryProps {
images: GalleryImage[];
}
export default function BentoGallery({ images }: BentoGalleryProps) {
const items = images.slice(0, 6);
return (
<section className={styles.gallerySection} aria-label="Bildergalerie">
<div className={styles.inner}>
<div className={styles.grid}>
{items.map((image, index) => (
<div key={image.src} className={styles.item} data-index={index}>
<div className={styles.frame}>
<Image
src={image.src}
alt={image.alt}
fill
className={styles.image}
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 600px"
priority={index === 0}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
}
+100
View File
@@ -0,0 +1,100 @@
.form {
display: flex;
flex-direction: column;
gap: var(--space-5);
max-width: 640px;
}
.row {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
}
.field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.label {
font-size: var(--font-size-small, 0.9rem);
font-weight: var(--font-weight-medium);
color: var(--color-heading);
}
.optional {
font-weight: var(--font-weight-regular, 400);
color: var(--color-text-muted, #6b7280);
}
.required {
color: var(--color-accent);
}
.input,
.textarea {
border: 1px solid color-mix(in srgb, var(--color-heading) 15%, transparent);
border-radius: var(--radius-md, 8px);
padding: var(--space-3) var(--space-4);
font-size: var(--font-size-base);
font-family: inherit;
background: #fff;
transition: border-color 0.2s ease;
}
.input:focus,
.textarea:focus {
outline: none;
border-color: var(--color-accent);
}
.textarea {
resize: vertical;
min-height: 140px;
}
.checkboxField {
display: flex;
align-items: flex-start;
gap: var(--space-3);
}
.checkbox {
margin-top: 4px;
width: 18px;
height: 18px;
flex-shrink: 0;
accent-color: var(--color-accent);
}
.checkboxLabel {
font-size: var(--font-size-small, 0.9rem);
line-height: 1.5;
}
.checkboxLabel a {
color: var(--color-accent);
text-decoration: underline;
}
.errorText {
font-size: var(--font-size-small, 0.85rem);
color: #d92d20;
margin: 0;
}
.successBox {
max-width: 640px;
padding: var(--space-6);
border-radius: var(--radius-lg);
background: color-mix(in srgb, var(--color-accent) 12%, white);
color: var(--color-heading);
}
/* Tablet: 768px */
@media (min-width: 768px) {
.row {
grid-template-columns: repeat(2, 1fr);
}
}
+213
View File
@@ -0,0 +1,213 @@
"use client";
import { useActionState, useMemo, useState } from "react";
import { useFormStatus } from "react-dom";
import {
sendContactMessage,
type ContactFormState,
} from "@/app/kontakt/actions";
import styles from "./ContactForm.module.css";
const initialState: ContactFormState = { success: false };
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const PHONE_REGEX = /^[0-9+\-/() ]{6,20}$/;
function SubmitButton({ disabled }: { disabled: boolean }) {
const { pending } = useFormStatus();
return (
<button
type="submit"
className="btn btn--filled"
disabled={disabled || pending}
>
{pending ? "Wird gesendet…" : "Nachricht senden"}
</button>
);
}
export default function ContactForm() {
const [state, formAction] = useActionState(sendContactMessage, initialState);
const [values, setValues] = useState({
unternehmen: "",
vorname: "",
nachname: "",
email: "",
telefon: "",
nachricht: "",
datenschutz: false,
});
const isValid = useMemo(() => {
return (
values.vorname.trim().length > 0 &&
values.nachname.trim().length > 0 &&
EMAIL_REGEX.test(values.email.trim()) &&
PHONE_REGEX.test(values.telefon.trim()) &&
values.nachricht.trim().length > 0 &&
values.datenschutz
);
}, [values]);
if (state.success) {
return (
<div className={styles.successBox} role="status">
<p>
Vielen Dank für Ihre Nachricht! Wir melden uns schnellstmöglich bei
Ihnen zurück.
</p>
</div>
);
}
return (
<form action={formAction} className={styles.form} noValidate>
<div className={styles.field}>
<label htmlFor="unternehmen" className={styles.label}>
Unternehmen <span className={styles.optional}>(optional)</span>
</label>
<input
id="unternehmen"
name="unternehmen"
type="text"
className={styles.input}
value={values.unternehmen}
onChange={(e) =>
setValues((v) => ({ ...v, unternehmen: e.target.value }))
}
/>
</div>
<div className={styles.row}>
<div className={styles.field}>
<label htmlFor="vorname" className={styles.label}>
Vorname <span className={styles.required}>*</span>
</label>
<input
id="vorname"
name="vorname"
type="text"
required
className={styles.input}
value={values.vorname}
onChange={(e) =>
setValues((v) => ({ ...v, vorname: e.target.value }))
}
/>
{state.fieldErrors?.vorname && (
<p className={styles.errorText}>{state.fieldErrors.vorname}</p>
)}
</div>
<div className={styles.field}>
<label htmlFor="nachname" className={styles.label}>
Nachname <span className={styles.required}>*</span>
</label>
<input
id="nachname"
name="nachname"
type="text"
required
className={styles.input}
value={values.nachname}
onChange={(e) =>
setValues((v) => ({ ...v, nachname: e.target.value }))
}
/>
{state.fieldErrors?.nachname && (
<p className={styles.errorText}>{state.fieldErrors.nachname}</p>
)}
</div>
</div>
<div className={styles.field}>
<label htmlFor="email" className={styles.label}>
E-Mail <span className={styles.required}>*</span>
</label>
<input
id="email"
name="email"
type="email"
required
className={styles.input}
value={values.email}
onChange={(e) => setValues((v) => ({ ...v, email: e.target.value }))}
/>
{state.fieldErrors?.email && (
<p className={styles.errorText}>{state.fieldErrors.email}</p>
)}
</div>
<div className={styles.field}>
<label htmlFor="telefon" className={styles.label}>
Telefonnummer <span className={styles.required}>*</span>
</label>
<input
id="telefon"
name="telefon"
type="tel"
required
className={styles.input}
value={values.telefon}
onChange={(e) =>
setValues((v) => ({ ...v, telefon: e.target.value }))
}
/>
{state.fieldErrors?.telefon && (
<p className={styles.errorText}>{state.fieldErrors.telefon}</p>
)}
</div>
<div className={styles.field}>
<label htmlFor="nachricht" className={styles.label}>
Nachricht <span className={styles.required}>*</span>
</label>
<textarea
id="nachricht"
name="nachricht"
required
rows={6}
className={styles.textarea}
value={values.nachricht}
onChange={(e) =>
setValues((v) => ({ ...v, nachricht: e.target.value }))
}
/>
{state.fieldErrors?.nachricht && (
<p className={styles.errorText}>{state.fieldErrors.nachricht}</p>
)}
</div>
<div className={styles.checkboxField}>
<input
id="datenschutz"
name="datenschutz"
type="checkbox"
required
className={styles.checkbox}
checked={values.datenschutz}
onChange={(e) =>
setValues((v) => ({ ...v, datenschutz: e.target.checked }))
}
/>
<label htmlFor="datenschutz" className={styles.checkboxLabel}>
Ich habe die{" "}
<a href="/datenschutz" target="_blank" rel="noopener noreferrer">
Datenschutzhinweise
</a>{" "}
gelesen und stimme der Verarbeitung meiner Daten zu.{" "}
<span className={styles.required}>*</span>
</label>
</div>
{state.fieldErrors?.datenschutz && (
<p className={styles.errorText}>{state.fieldErrors.datenschutz}</p>
)}
{state.error && <p className={styles.errorText}>{state.error}</p>}
<SubmitButton disabled={!isValid} />
</form>
);
}
+95
View File
@@ -0,0 +1,95 @@
.gallerySection {
padding-block: var(--space-10);
overflow: hidden;
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-4);
}
.item {
position: relative;
}
.frame {
position: relative;
width: 100%;
aspect-ratio: 4 / 5;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.08),
0 24px 60px rgba(0, 0, 0, 0.06);
}
.image {
object-fit: cover;
transition: transform 0.4s ease;
}
.frame:hover .image {
transform: scale(1.04);
}
/* Tablet: 768px – 3 Spalten, 2 Zeilen, versetzter Look */
@media (min-width: 768px) {
.gallerySection {
padding-block: var(--space-16);
}
.grid {
grid-template-columns: repeat(3, 1fr);
gap: var(--space-6);
}
.frame {
aspect-ratio: 3 / 4;
}
/* Versatz-Muster: mittlere Spalte tiefer, äußere Spalten höher */
.item[data-index="0"],
.item[data-index="2"] {
margin-top: 0;
}
.item[data-index="1"] {
margin-top: var(--space-10);
}
.item[data-index="3"],
.item[data-index="5"] {
margin-top: calc(-1 * var(--space-6));
}
.item[data-index="4"] {
margin-top: var(--space-4);
}
}
/* Desktop: 1024px */
@media (min-width: 1024px) {
.gallerySection {
padding-block: var(--space-20);
}
.item[data-index="1"] {
margin-top: var(--space-14);
}
.item[data-index="3"],
.item[data-index="5"] {
margin-top: calc(-1 * var(--space-8));
}
.item[data-index="4"] {
margin-top: var(--space-6);
}
}
+38
View File
@@ -0,0 +1,38 @@
import Image from "next/image";
import styles from "./BentoGallery.module.css";
interface GalleryImage {
src: string;
alt: string;
}
interface BentoGalleryProps {
images: GalleryImage[];
}
export default function BentoGallery({ images }: BentoGalleryProps) {
const items = images.slice(0, 6);
return (
<section className={styles.gallerySection} aria-label="Bildergalerie">
<div className={styles.inner}>
<div className={styles.grid}>
{items.map((image, index) => (
<div key={image.src} className={styles.item} data-index={index}>
<div className={styles.frame}>
<Image
src={image.src}
alt={image.alt}
fill
className={styles.image}
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 600px"
priority={index === 0}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
}
+111
View File
@@ -151,3 +151,114 @@ export const leistungenFaq = [
], ],
}, },
]; ];
export const branchenFaq = [
{
slug: "buero",
title: "Häufige Fragen zur Büroreinigung",
faq: [
{
question: "Wie oft wird unser Büro gereinigt?",
answer:
"Das legen wir gemeinsam mit Ihnen fest – ob täglich, wöchentlich oder in individuellen Intervallen. Entscheidend sind Mitarbeiterzahl, Publikumsverkehr und Ihre Hygieneanforderungen.",
},
{
question: "Reinigen Sie auch außerhalb unserer Geschäftszeiten?",
answer:
"Ja, wir richten die Reinigungszeiten nach Ihrem Betriebsablauf – früh morgens, abends oder am Wochenende, damit der laufende Betrieb nicht gestört wird.",
},
{
question: "Werden auch sensible Bereiche wie Serverräume gereinigt?",
answer:
"Ja, für sensible Bereiche wie Serverräume oder Aktenlager stimmen wir vorab besondere Vorgaben und Zugangsregeln mit Ihnen ab.",
},
{
question: "Können Sie mehrere Standorte gleichzeitig betreuen?",
answer:
"Ja, wir betreuen auch Unternehmen mit mehreren Bürostandorten in Hannover und Umgebung mit einem festen Ansprechpartner für alle Objekte.",
},
],
},
{
slug: "hallen-lagerflaechen",
title: "Häufige Fragen zur Reinigung von Hallen und Lagerflächen",
faq: [
{
question: "Reinigen Sie auch großflächige Industrieböden?",
answer:
"Ja, mit professionellem Maschinenequipment reinigen wir großflächige Hallen-, Lager- und Produktionsböden effizient und materialschonend.",
},
{
question: "Ist eine Reinigung während des laufenden Betriebs möglich?",
answer:
"Ja, wir stimmen die Einsatzzeiten auf Schichtbetrieb und Lieferverkehr ab, damit Ihre Abläufe möglichst wenig beeinträchtigt werden.",
},
{
question: "Wie gehen Sie mit hoher Staub- und Schmutzbelastung um?",
answer:
"Wir passen Reinigungsintervalle und Verfahren an die tatsächliche Belastung durch Warenumschlag und Betriebsabläufe an, statt starr nach Schema zu arbeiten.",
},
{
question:
"Berücksichtigen Sie Sicherheitsvorgaben bei laufendem Gabelstaplerverkehr?",
answer:
"Ja, unser Personal ist auf die Zusammenarbeit in Bereichen mit Gabelstapler- und Warenverkehr geschult und hält geltende Sicherheitsregeln ein.",
},
],
},
{
slug: "wohn-geschaeftshaeuser",
title: "Häufige Fragen zur Reinigung von Wohn- und Geschäftshäusern",
faq: [
{
question: "Arbeiten Sie direkt mit unserer Hausverwaltung zusammen?",
answer:
"Ja, wir stimmen Reinigungspläne, Intervalle und Sonderaufgaben direkt mit Hausverwaltungen oder Eigentümergemeinschaften ab und sorgen für klare Kommunikation.",
},
{
question: "Übernehmen Sie auch den Winterdienst am Objekt?",
answer:
"Ja, Winterdienst und Grünpflege im Außenbereich lassen sich mit der laufenden Betreuung Ihres Wohn- oder Geschäftshauses kombinieren.",
},
{
question:
"Wie gehen Sie mit unterschiedlichen Erwartungen von Mietern und Gewerbe um?",
answer:
"Wir sprechen Reinigungsumfang und -zeiten so ab, dass sowohl private Mieter als auch gewerbliche Nutzer im selben Objekt zufriedenstellend berücksichtigt werden.",
},
{
question: "Wie störungsarm ist die Reinigung für die Bewohner?",
answer:
"Die Reinigung erfolgt zu festen, mit der Hausverwaltung abgestimmten Zeiten, die den Alltag der Bewohner so wenig wie möglich beeinträchtigen.",
},
],
},
{
slug: "kanzlei",
title: "Häufige Fragen zur Kanzleireinigung",
faq: [
{
question:
"Wie gehen Sie mit vertraulichen Unterlagen in der Kanzlei um?",
answer:
"Unser Personal ist im diskreten Umgang mit vertraulichen Unterlagen geschult und hält vereinbarte Zugangs- und Sicherheitsregeln konsequent ein.",
},
{
question: "Reinigen Sie außerhalb unserer Kanzleistunden?",
answer:
"Ja, die Reinigung erfolgt in der Regel außerhalb der Kanzleistunden, um die Vertraulichkeit zu wahren und den Beratungsbetrieb nicht zu stören.",
},
{
question: "Haben wir einen festen Ansprechpartner?",
answer:
"Ja, Sie erhalten einen festen Ansprechpartner, der die Anforderungen Ihrer Kanzlei kennt und für eine verlässliche, langfristige Zusammenarbeit sorgt.",
},
{
question:
"Gehen Sie besonders sorgfältig mit hochwertiger Ausstattung um?",
answer:
"Ja, wir reinigen Mobiliar, Böden und Oberflächen in Empfangs- und Besprechungsräumen materialschonend und mit besonderer Sorgfalt.",
},
],
},
];
+13
View File
@@ -0,0 +1,13 @@
import nodemailer from "nodemailer";
// Transporter wird einmalig erstellt und bei jedem Funktionsaufruf wiederverwendet
// (Next.js Server Actions laufen in einer Node-Umgebung, kein Cold-Start-Problem wie bei Edge Functions).
export const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT ?? 465),
secure: process.env.SMTP_SECURE === "true", // true für Port 465, false für 587 (STARTTLS)
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
+10
View File
@@ -11,6 +11,7 @@
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.10",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"next": "^16.3.4", "next": "^16.3.4",
"nodemailer": "^10.0.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"vanilla-cookieconsent": "^3.1.0" "vanilla-cookieconsent": "^3.1.0"
@@ -4556,6 +4557,15 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/nodemailer": {
"version": "10.0.3",
"resolved": "https://cicd.mecklenburgische.de/repository/npm-public/nodemailer/-/nodemailer-10.0.3.tgz",
"integrity": "sha512-h5c8ypcMoOIh44bIcVROvTDbjRdOJX8AXTkxMht2C92HlSjkG+/8kjoqmNPypfjQaVNaUHWyZ3tqs5I3K91F3g==",
"license": "MIT-0",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+1
View File
@@ -12,6 +12,7 @@
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.10",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"next": "^16.3.4", "next": "^16.3.4",
"nodemailer": "^10.0.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"vanilla-cookieconsent": "^3.1.0" "vanilla-cookieconsent": "^3.1.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB