Compare commits

..
3 Commits
Author SHA1 Message Date
Engin 3bda7d6eb6 update 2026-09-10 14:31:05 +02:00
egnbkts cc2b84dcad update 2026-09-08 09:49:27 +02:00
egnbkts 574e07603d Backup eingespielt 2026-08-12 11:06:10 +02:00
59 changed files with 5001 additions and 373 deletions

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));
}
}
+234
View File
@@ -0,0 +1,234 @@
import { notFound } from "next/navigation";
import { branchen } from "../data";
import { leistungen } from "../../leistungen/data";
import Image from "next/image";
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() {
return branchen.map((item) => ({
slug: item.slug,
}));
}
export default async function BrancheDetail({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
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 (
<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}>
{branche.titleMain}
<span className={styles.headingSub}>{branche.titleSub}</span>
</h1>
<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>
);
}
+266
View File
@@ -0,0 +1,266 @@
export const branchen = [
{
slug: "bueros",
titleMain: "Reinigung für Büros",
titleSub: "in Hannover und Umgebung",
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",
// --- SEO ---
metaTitle: "Büroreinigung Hannover – für Unternehmen jeder Größe",
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",
titleMain: "Reinigung für Hallen und Lagerflächen",
titleSub: "in Hannover und Umgebung",
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",
titleMain: "Reinigung für Wohn- und Geschäftshäuser",
titleSub: "in Hannover und Umgebung",
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",
},
],
},
];
+3
View File
@@ -0,0 +1,3 @@
export default function Branchenloesungen() {
return <h1>Branchenlösungen</h1>;
}
+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;
}
}
+74
View File
@@ -0,0 +1,74 @@
import type { Metadata } from "next";
import styles from "./page.module.css";
import ContactForm from "@/components/ContactForm";
import FaqSection from "@/components/faq/FaqSection";
import { homepageFaq } from "@/components/faq/faqData";
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>
);
}
+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;
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);
/* color: var(--color-text-muted); */
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);
align-items: left;
text-align: left;
}
.lead {
font-size: var(--font-size-base);
text-align: left;
width: min(100%, 48ch);
}
.sliderSection {
padding-block: var(--space-16);
}
.slidesHeading {
max-width: 1024px;
margin-inline: auto;
padding-inline: var(--space-4);
text-align: center;
margin-bottom: var(--space-8);
}
/* Tablet: 768px */
@media (min-width: 768px) {
.sliderSection {
padding-block: var(--space-24);
}
.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;
}
}
/* Desktop: 1024px */
@media (min-width: 1024px) {
.hero {
padding-block: var(--space-36);
}
.decoration {
top: -42px;
}
}
+186
View File
@@ -0,0 +1,186 @@
import { notFound } from "next/navigation";
import { leistungen } from "../data";
import Image from "next/image";
import Link from "next/link";
import { ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
import { Slider } from "@/components/slider/Slider";
import styles from "./page.module.css";
import MaintenanceCleaningSection from "@/components/MaintenanceCleaningSection";
import QualityClaimSection from "@/components/QualityClaimSection";
import ConceptSection from "@/components/ConceptSection";
import ProcessSection from "@/components/ProcessSection";
import FaqSection from "@/components/faq/FaqSection";
import { leistungenFaq } from "@/components/faq/faqData";
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() {
return leistungen.map((item) => ({
slug: item.slug,
}));
}
export default async function LeistungDetail({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const leistung = leistungen.find((item) => item.slug === slug);
const faqEntry = leistungenFaq.find((item) => item.slug === slug);
if (!leistung) {
notFound();
}
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}>
{leistung.titleMain}
<span className={styles.headingSub}>{leistung.titleSub}</span>
</h1>
<div className={styles.content}>
<div className={styles.imageWrapper}>
<div className={styles.imageFrame}>
<Image
src={leistung.image}
alt={leistung.imageAlt}
fill
className={styles.image}
sizes="(max-width: 640px) 100vw, 260px"
/>
</div>
</div>
<div className={styles.body}>
<p className={`lead ${styles.lead}`}>{leistung.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>
{leistung.slides.length > 0 && (
<section
className={styles.sliderSection}
aria-label={`Weitere Informationen zur ${leistung.titleMain}`}
>
<h2 className={styles.slidesHeading}>{leistung.titleSlides}</h2>
<div className={styles.inner}>
<Slider slides={leistung.slides} imageAlt={leistung.imageAlt} />
</div>
</section>
)}
<section>
<MaintenanceCleaningSection {...leistung.maintenanceSection} />
</section>
<section>
<QualityClaimSection {...leistung.qualityClaimSection} />
</section>
<section>
<ConceptSection {...leistung.conceptSection} />
</section>
<section>
<ProcessSection {...leistung.processSection} />
</section>
<section>
<FaqSection
title={faqEntry?.title ?? "Häufige Fragen"}
items={faqEntry?.faq ?? []}
/>
</section>
<section>
<ContactCtaSection {...leistung.contactCtaSection} />
</section>
</main>
);
}
+589
View File
@@ -0,0 +1,589 @@
export const leistungen = [
{
slug: "fensterreinigung",
titleMain: "Professionelle Fensterreinigung",
titleSub: "in Hannover und Umgebung",
text: "Streifenfreie Fenster und Glasfronten sind das Aushängeschild Ihres Unternehmens. Wir reinigen Fenster, Rahmen, Glasfassaden und Eingangsbereiche zuverlässig – für einen professionellen ersten Eindruck bei Kunden, Mitarbeitenden und Besuchern.",
image: "/images/services/Fensterreinigung.jpg",
imageAlt: "Gereinigte Fensterfront eines Geschäftsgebäudes",
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: [
{
heading: "Klare Sicht für Ihren guten ersten Eindruck",
text: "Saubere Fenster und Glasflächen unterstreichen den professionellen Auftritt Ihres Unternehmens. Wir reinigen zuverlässig Fenster, Rahmen und Glasfronten.",
image: "/images/services/Fensterreinigung.jpg",
},
{
heading: "Glasflächen streifenfrei gepflegt",
text: "Mit abgestimmten Reinigungsintervallen bleiben Eingangsbereiche, Schaufenster und Glasfassaden dauerhaft sauber und einladend.",
image: "/images/services/unterhaltsreinigung.jpg",
},
{
heading: "Reinigung passend zu Ihrem Objekt",
text: "Ob Büro, Praxis oder Gewerbefläche: Wir planen die Fensterreinigung nach Größe, Nutzung und erreichbaren Reinigungszeiten.",
image: "/images/services/Fensterreinigung.jpg",
},
],
maintenanceSection: {
heading: "Regelmäßige Fensterreinigung für dauerhaft klare Sicht",
lead: "Damit Glasflächen und Fensterfronten jederzeit streifenfrei und einladend wirken, ist eine regelmäßige Fensterreinigung entscheidend. Durch planbare Reinigungsintervalle bleiben Fenster, Rahmen und Eingangsbereiche dauerhaft in einem einwandfreien Zustand.",
requirements: [
"Die Reinigung kann täglich, wöchentlich oder bedarfsorientiert stattfinden.",
"Die Lage und Ausrichtung der Fenster bestimmen, wie schnell Flächen wieder verschmutzen.",
"Die Art der Verglasung und Rahmenmaterialien beeinflusst Mittel, Methoden und Aufwand.",
"Die Erreichbarkeit der Fensterflächen legt fest, welches Equipment nötig ist.",
"Die Reinigungszeiten richten sich nach Ihren Betriebsabläufen und dem Publikumsverkehr.",
"Zugangs- und Sicherheitsregeln müssen berücksichtigt werden, damit alles reibungslos läuft.",
],
},
qualityClaimSection: {
heading: "Unser Qualitätsanspruch in der Fensterreinigung",
textLeft:
"Jedes Gebäude und jede Fensterfront bringt eigene Anforderungen mit sich – deshalb entwickeln wir ein Reinigungskonzept, das genau zu Ihren Glasflächen und Erwartungen passt. Entscheidend ist das Zusammenspiel aus Qualität, Service und Sorgfalt, damit die Fensterreinigung nicht nur kurzfristig gut wirkt, sondern dauerhaft gleichbleibende Ergebnisse liefert.",
textRight:
"Genau diese Balance schaffen wir mit klar definierten Prozessen, transparenter Abstimmung und einem eingespielten Team, das zuverlässig und sorgfältig arbeitet.",
image: "/images/services/Fensterreinigung.jpg",
imageAlt: "Reinigungskraft bei der professionellen Fensterreinigung",
},
conceptSection: {
heading: "Unser Konzept für Fensterreinigung",
lead: "Mit Basiclean erhalten Sie eine planbare Fensterreinigung, die sich an Ihrem Gebäude anpasst und durch klare Abläufe, Dokumentation und regelmäßige Qualitätskontrollen konstant bleibt.",
pillars: [
{
number: "01",
title: "Qualität",
points: [
"Klare Reinigungspläne",
"Regelmäßige Qualitätschecks",
"Dokumentierte Ergebnisse",
],
},
{
number: "02",
title: "Service",
points: [
"Fester Ansprechpartner",
"Flexible Einsatzzeiten",
"Schnelle Reaktion bei Sonderfällen",
],
},
{
number: "03",
title: "Sicherheit",
points: [
"Geschultes Personal für Höhenarbeiten",
"Fokus auf sensible Glasflächen",
"Sauberkeit mit System",
],
},
],
results: {
heading: "Ergebnisse die überzeugen",
intro: "Mit der Fensterreinigung in Hannover profitieren Sie von:",
points: [
"Individuelle Reinigungskonzepte",
"Faire und transparente Preise",
"Qualifiziertes Personal",
"Eingespielte Teams für gleichbleibende Qualität",
"Flexible Reinigungsintervalle nach Bedarf und Wunsch",
],
},
},
processSection: {
heading: "4-Stufen-Prozess für Ihre Fensterreinigung",
lead: "Damit Ihre Fensterreinigung dauerhaft zuverlässig, planbar und nachvollziehbar bleibt, arbeiten wir bei Basiclean nach einem klaren Ablauf – von der Bedarfsklärung bis zur laufenden Qualitätskontrolle.",
steps: [
{ number: "01", title: "Individuelle Analyse" },
{ number: "02", title: "Leistungsplan erstellen" },
{ number: "03", title: "Regelmäßige Durchführung" },
{ number: "04", title: "Feedback und Qualitätskontrolle" },
],
},
contactCtaSection: {
heading: "Lassen Sie uns über Ihre Fensterreinigung sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihre Glasflächen in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
},
{
slug: "unterhaltsreinigung",
titleMain: "Professionelle Unterhaltsreinigung",
titleSub: "in Hannover und Umgebung",
text: "Regelmäßige Unterhaltsreinigung für Büros, Praxen und Schulen – zuverlässig, gründlich und zu festen Terminen. Wir stimmen die Reinigungsintervalle individuell auf Ihre Räumlichkeiten ab, damit Ihre Flächen dauerhaft gepflegt und einladend wirken.",
image: "/images/services/unterhaltsreinigung.jpg",
imageAlt: "Reinigungskraft bei der Unterhaltsreinigung in einem Büro",
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: [
{
heading: "Konstante Hygiene im Alltag",
text: "Eine professionelle Unterhaltsreinigung sorgt täglich für verlässliche Sauberkeit und hygienische Standards in Ihren Räumen. Regelmäßige Reinigung reduziert Staub, Schmutz und Keimbelastung und schafft ein angenehmes Umfeld für Mitarbeitende und Besucher.",
image: "/images/slides/unterhaltsreinigung-1.jpg",
},
{
heading: "Saubere Arbeitsplätze für jeden Tag",
text: "Büros, Besprechungsräume und Gemeinschaftsflächen bleiben ordentlich und gepflegt, ohne dass Ihr Betriebsablauf beeinträchtigt wird.",
image: "/images/slides/unterhaltsreinigung-2.jpg",
},
{
heading: "Reinigungsintervalle nach Bedarf",
text: "Wir stimmen Umfang, Zeiten und Häufigkeit der Reinigung auf Ihre Nutzung, Ihre Flächen und Ihre Anforderungen ab.",
image: "/images/slides/unterhaltsreinigung-3.jpg",
},
],
maintenanceSection: {
heading: "Regelmäßige Unterhaltsreinigung für dauerhaft saubere Gebäude",
lead: "Damit Ihr Gebäude jederzeit sauber, hygienisch und repräsentativ bleibt, ist eine regelmäßige Unterhaltsreinigung entscheidend. Durch planbare Reinigungsintervalle und abgestimmte Leistungen in der Gebäudereinigung werden Räume und Flächen laufend gepflegt und dauerhaft in einem einwandfreien Zustand gehalten.",
requirements: [
"Die Reinigung kann täglich, wöchentlich oder bedarfsorientiert stattfinden.",
"Die Nutzung und Besucherfrequenz bestimmen, wie schnell Flächen wieder verschmutzen.",
"Die Art der Oberflächen und Materialien beeinflusst Mittel, Methoden und Aufwand.",
"Die geforderten Hygienestandards legen fest, welche Bereiche wie intensiv gereinigt werden.",
"Die Reinigungszeiten richten sich nach Ihren Betriebsabläufen und dem Publikumsverkehr.",
"Zugangs- und Sicherheitsregeln müssen berücksichtigt werden, damit alles reibungslos läuft.",
],
},
qualityClaimSection: {
heading: "Unser Qualitätsanspruch in der Unterhaltsreinigung",
textLeft:
"Jedes Gebäude und jede Nutzung bringt eigene Anforderungen mit sich – deshalb entwickeln wir ein Reinigungskonzept, das genau zu Ihren Abläufen und Erwartungen passt. Entscheidend ist das Zusammenspiel aus Qualität, Service und Hygiene, damit die Unterhaltsreinigung nicht nur kurzfristig gut wirkt, sondern dauerhaft gleichbleibende Ergebnisse liefert.",
textRight:
"Genau diese Balance schaffen wir mit klar definierten Prozessen, transparenter Abstimmung und einem eingespielten Team, das zuverlässig und sorgfältig arbeitet.",
image: "/images/slides/unterhaltsreinigung-1.jpg",
imageAlt: "Reinigungskraft bei der professionellen Unterhaltsreinigung",
},
conceptSection: {
heading: "Unser Konzept für Unterhaltsreinigung",
lead: "Mit Basiclean erhalten Sie eine planbare Unterhaltsreinigung, die sich an Ihrem Betrieb anpasst und durch klare Abläufe, Dokumentation und regelmäßige Qualitätskontrollen konstant bleibt.",
pillars: [
{
number: "01",
title: "Qualität",
points: [
"Klare Reinigungspläne",
"Regelmäßige Qualitätschecks",
"Dokumentierte Ergebnisse",
],
},
{
number: "02",
title: "Service",
points: [
"Fester Ansprechpartner",
"Flexible Einsatzzeiten",
"Schnelle Reaktion bei Sonderfällen",
],
},
{
number: "03",
title: "Hygiene",
points: [
"Definierte Hygienestandards",
"Fokus auf sensible Bereiche",
"Sauberkeit mit System",
],
},
],
results: {
heading: "Ergebnisse die überzeugen",
intro: "Mit der Unterhaltsreinigung in Hannover profitieren Sie von:",
points: [
"Individuell Reinigungskonzepte",
"Faire und transparente Preise",
"Qualifiziertes Personal",
"Eingespielte Teams für gleichbleibende Qualität",
"Flexible Reinigungsintervalle nach Bedarf und Wunsch",
],
},
},
processSection: {
heading: "4-Stufen-Prozess für Ihre Unterhaltsreinigung",
lead: "Damit Ihre Unterhaltsreinigung dauerhaft zuverlässig, planbar und nachvollziehbar bleibt, arbeiten wir bei Basiclean nach einem klaren Ablauf – von der Bedarfsklärung bis zur laufenden Qualitätskontrolle.",
steps: [
{ number: "01", title: "Individuelle Analyse" },
{ number: "02", title: "Leistungsplan erstellen" },
{ number: "03", title: "Regelmäßige Durchführung" },
{ number: "04", title: "Feedback und Qualitätskontrolle" },
],
},
contactCtaSection: {
heading: "Lassen Sie uns über Ihre Unterhaltsreinigung sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihr Unternehmen in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
},
{
slug: "treppenhausreinigung",
titleMain: "Professionelle Treppenhausreinigung",
titleSub: "in Hannover und Umgebung",
text: "Gepflegte Treppenhäuser und Eingangsbereiche hinterlassen bei Mitarbeitenden, Kundschaft und Besuchern einen guten ersten Eindruck. Wir übernehmen die regelmäßige Reinigung zuverlässig und termingerecht – für eine saubere Visitenkarte Ihres Gebäudes.",
image: "/images/services/treppenhausreinigung.jpg",
imageAlt: "Sauberes Treppenhaus in einem Gewerbeobjekt",
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: [
{
heading: "Ein gepflegter Empfang beginnt im Treppenhaus",
text: "Treppenhäuser und Eingangsbereiche prägen den ersten Eindruck Ihres Gebäudes. Wir halten diese Bereiche zuverlässig sauber.",
image: "/images/slides/treppenhausreinigung-1.jpg",
},
{
heading: "Sauberkeit auf jeder Etage",
text: "Regelmäßige Reinigung von Böden, Handläufen, Türen und Eingangsflächen sorgt für ein dauerhaft gepflegtes Erscheinungsbild.",
image: "/images/slides/treppenhausreinigung-2.jpg",
},
{
heading: "Planbare Termine, verlässliche Abläufe",
text: "Die Reinigung erfolgt in festen Intervallen und wird so organisiert, dass Bewohner, Mitarbeitende und Besucher möglichst wenig gestört werden.",
image: "/images/slides/treppenhausreinigung-3.jpg",
},
],
maintenanceSection: {
heading:
"Regelmäßige Treppenhausreinigung für dauerhaft gepflegte Zugänge",
lead: "Damit Treppenhäuser und Eingangsbereiche jederzeit sauber und einladend bleiben, ist eine regelmäßige Treppenhausreinigung entscheidend. Durch planbare Reinigungsintervalle bleiben Böden, Handläufe und Türen laufend gepflegt und dauerhaft in einem einwandfreien Zustand.",
requirements: [
"Die Reinigung kann täglich, wöchentlich oder bedarfsorientiert stattfinden.",
"Die Anzahl der Bewohner und Besucherfrequenz bestimmen, wie schnell Flächen wieder verschmutzen.",
"Die Art der Boden- und Handlaufmaterialien beeinflusst Mittel, Methoden und Aufwand.",
"Die geforderten Hygienestandards legen fest, welche Bereiche wie intensiv gereinigt werden.",
"Die Reinigungszeiten richten sich nach den Abläufen der Bewohner und Mitarbeitenden.",
"Zugangs- und Sicherheitsregeln müssen berücksichtigt werden, damit alles reibungslos läuft.",
],
},
qualityClaimSection: {
heading: "Unser Qualitätsanspruch in der Treppenhausreinigung",
textLeft:
"Jedes Treppenhaus und jede Nutzung bringt eigene Anforderungen mit sich – deshalb entwickeln wir ein Reinigungskonzept, das genau zu Ihrem Gebäude und Erwartungen passt. Entscheidend ist das Zusammenspiel aus Qualität, Service und Hygiene, damit die Treppenhausreinigung nicht nur kurzfristig gut wirkt, sondern dauerhaft gleichbleibende Ergebnisse liefert.",
textRight:
"Genau diese Balance schaffen wir mit klar definierten Prozessen, transparenter Abstimmung und einem eingespielten Team, das zuverlässig und sorgfältig arbeitet.",
image: "/images/slides/treppenhausreinigung-1.jpg",
imageAlt: "Gepflegtes Treppenhaus nach professioneller Reinigung",
},
conceptSection: {
heading: "Unser Konzept für Treppenhausreinigung",
lead: "Mit Basiclean erhalten Sie eine planbare Treppenhausreinigung, die sich an Ihrem Gebäude anpasst und durch klare Abläufe, Dokumentation und regelmäßige Qualitätskontrollen konstant bleibt.",
pillars: [
{
number: "01",
title: "Qualität",
points: [
"Klare Reinigungspläne",
"Regelmäßige Qualitätschecks",
"Dokumentierte Ergebnisse",
],
},
{
number: "02",
title: "Service",
points: [
"Fester Ansprechpartner",
"Flexible Einsatzzeiten",
"Schnelle Reaktion bei Sonderfällen",
],
},
{
number: "03",
title: "Hygiene",
points: [
"Definierte Hygienestandards",
"Fokus auf Handläufe und Kontaktflächen",
"Sauberkeit mit System",
],
},
],
results: {
heading: "Ergebnisse die überzeugen",
intro: "Mit der Treppenhausreinigung in Hannover profitieren Sie von:",
points: [
"Individuelle Reinigungskonzepte",
"Faire und transparente Preise",
"Qualifiziertes Personal",
"Eingespielte Teams für gleichbleibende Qualität",
"Flexible Reinigungsintervalle nach Bedarf und Wunsch",
],
},
},
processSection: {
heading: "4-Stufen-Prozess für Ihre Treppenhausreinigung",
lead: "Damit Ihre Treppenhausreinigung dauerhaft zuverlässig, planbar und nachvollziehbar bleibt, arbeiten wir bei Basiclean nach einem klaren Ablauf – von der Bedarfsklärung bis zur laufenden Qualitätskontrolle.",
steps: [
{ number: "01", title: "Individuelle Analyse" },
{ number: "02", title: "Leistungsplan erstellen" },
{ number: "03", title: "Regelmäßige Durchführung" },
{ number: "04", title: "Feedback und Qualitätskontrolle" },
],
},
contactCtaSection: {
heading: "Lassen Sie uns über Ihre Treppenhausreinigung sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihr Treppenhaus in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
},
{
slug: "grundreinigung",
titleMain: "Professionelle Grundreinigung",
titleSub: "in Hannover und Umgebung",
text: "Intensive Tiefenreinigung für Räume mit besonderem Pflegebedarf – gründlich, termingerecht und mit professionellem Equipment. Ob nach Umzug, vor Neueröffnung oder als saisonale Maßnahme: Wir sorgen für einen makellosen Ausgangszustand.",
image: "/images/services/grundreinigung.jpg",
imageAlt: "Sauberer Büroarbeitsplatz nach der Grundreinigung",
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: [
{
heading: "Gründliche Sauberkeit bis ins Detail",
text: "Bei einer professionellen Grundreinigung entfernen wir hartnäckige Verschmutzungen und schaffen einen gepflegten Ausgangszustand.",
image: "/images/slides/grundreinigung-1.jpg",
},
{
heading: "Die passende Lösung bei besonderem Bedarf",
text: "Nach Umbau, Umzug oder vor der Neueröffnung bringen wir Böden, Oberflächen und Nebenräume systematisch auf Vordermann.",
image: "/images/slides/grundreinigung-2.jpg",
},
{
heading: "Professionelles Equipment für starke Ergebnisse",
text: "Mit passenden Verfahren und Reinigungsmitteln behandeln wir unterschiedliche Materialien fachgerecht und materialschonend.",
image: "/images/slides/grundreinigung-3.jpg",
},
],
maintenanceSection: {
heading:
"Regelmäßige Grundreinigung für einen dauerhaft gepflegten Zustand",
lead: "Damit Räume und Flächen auch nach besonderer Beanspruchung wieder einen einwandfreien Zustand erreichen, ist eine professionelle Grundreinigung entscheidend. Durch abgestimmte Reinigungsmethoden werden Böden, Oberflächen und Nebenräume gründlich aufbereitet.",
requirements: [
"Die Reinigung kann einmalig, saisonal oder bedarfsorientiert stattfinden.",
"Der Grad der Verschmutzung bestimmt, welches Verfahren notwendig ist.",
"Die Art der Oberflächen und Materialien beeinflusst Mittel, Methoden und Aufwand.",
"Die geforderten Hygienestandards legen fest, welche Bereiche wie intensiv gereinigt werden.",
"Die Reinigungszeiten richten sich nach Umbau-, Umzugs- oder Eröffnungsterminen.",
"Zugangs- und Sicherheitsregeln müssen berücksichtigt werden, damit alles reibungslos läuft.",
],
},
qualityClaimSection: {
heading: "Unser Qualitätsanspruch in der Grundreinigung",
textLeft:
"Jeder Raum und jeder Verschmutzungsgrad bringt eigene Anforderungen mit sich – deshalb entwickeln wir ein Reinigungskonzept, das genau zu Ihrem Objekt und Erwartungen passt. Entscheidend ist das Zusammenspiel aus Qualität, Service und Equipment, damit die Grundreinigung nicht nur kurzfristig gut wirkt, sondern einen dauerhaft gepflegten Ausgangszustand liefert.",
textRight:
"Genau diese Balance schaffen wir mit klar definierten Prozessen, transparenter Abstimmung und einem eingespielten Team, das zuverlässig und sorgfältig arbeitet.",
image: "/images/slides/grundreinigung-1.jpg",
imageAlt: "Gründlich gereinigter Raum nach der Grundreinigung",
},
conceptSection: {
heading: "Unser Konzept für Grundreinigung",
lead: "Mit Basiclean erhalten Sie eine gründliche Grundreinigung, die sich an Ihrem Objekt anpasst und durch klare Abläufe, Dokumentation und regelmäßige Qualitätskontrollen überzeugt.",
pillars: [
{
number: "01",
title: "Qualität",
points: [
"Klare Reinigungspläne",
"Regelmäßige Qualitätschecks",
"Dokumentierte Ergebnisse",
],
},
{
number: "02",
title: "Service",
points: [
"Fester Ansprechpartner",
"Flexible Einsatzzeiten",
"Schnelle Reaktion bei Sonderfällen",
],
},
{
number: "03",
title: "Equipment",
points: [
"Professionelle Maschinen und Mittel",
"Fokus auf hartnäckige Verschmutzungen",
"Sauberkeit mit System",
],
},
],
results: {
heading: "Ergebnisse die überzeugen",
intro: "Mit der Grundreinigung in Hannover profitieren Sie von:",
points: [
"Individuelle Reinigungskonzepte",
"Faire und transparente Preise",
"Qualifiziertes Personal",
"Eingespielte Teams für gleichbleibende Qualität",
"Flexible Termine nach Bedarf und Wunsch",
],
},
},
processSection: {
heading: "4-Stufen-Prozess für Ihre Grundreinigung",
lead: "Damit Ihre Grundreinigung dauerhaft zuverlässig, planbar und nachvollziehbar bleibt, arbeiten wir bei Basiclean nach einem klaren Ablauf – von der Bedarfsklärung bis zur laufenden Qualitätskontrolle.",
steps: [
{ number: "01", title: "Individuelle Analyse" },
{ number: "02", title: "Leistungsplan erstellen" },
{ number: "03", title: "Durchführung vor Ort" },
{ number: "04", title: "Abnahme und Qualitätskontrolle" },
],
},
contactCtaSection: {
heading: "Lassen Sie uns über Ihre Grundreinigung sprechen",
lead: "Wir erstellen ein individuelles Reinigungskonzept für Ihr Objekt in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
},
{
slug: "hausmeisterdienste",
titleMain: "Professionelle Hausmeisterdienste",
titleSub: "in Hannover und Umgebung",
text: "Vom Winterdienst bis zur Grünpflege – wir übernehmen zuverlässig alle anfallenden Hausmeisteraufgaben. Ob Kleinreparaturen, Objektbetreuung oder regelmäßige Kontrollgänge: Ein fester Ansprechpartner sorgt für reibungslose Abläufe in Ihrem Objekt.",
image: "/images/services/Hausmeisterdienste.jpg",
imageAlt: "Hausmeister bei der Außenpflege eines Gebäudes",
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: [
{
heading: "Zuverlässige Betreuung für Ihr Objekt",
text: "Wir übernehmen regelmäßige Kontrollgänge und kümmern uns um die Aufgaben, die im Alltag rund um Ihre Immobilie anfallen.",
image: "/images/slides/hausmeisterdienste-1.jpg",
},
{
heading: "Ein Ansprechpartner für viele Aufgaben",
text: "Von kleinen Reparaturen über Außenpflege bis zur Objektbetreuung koordinieren wir Arbeiten zuverlässig und transparent.",
image: "/images/slides/hausmeisterdienste-2.jpg",
},
{
heading: "Außenanlagen und Wege im Blick",
text: "Grünpflege, Winterdienst und die Kontrolle von Außenbereichen sorgen für ein gepflegtes und sicheres Umfeld.",
image: "/images/slides/hausmeisterdienste-3.jpg",
},
],
maintenanceSection: {
heading: "Regelmäßige Hausmeisterdienste für dauerhaft gepflegte Objekte",
lead: "Damit Ihr Objekt jederzeit gepflegt, sicher und funktionsfähig bleibt, sind regelmäßige Hausmeisterdienste entscheidend. Durch planbare Kontrollgänge und abgestimmte Leistungen werden Außenanlagen, Wege und technische Anlagen laufend betreut.",
requirements: [
"Die Kontrollgänge können täglich, wöchentlich oder bedarfsorientiert stattfinden.",
"Die Größe und Nutzung des Objekts bestimmen, wie viele Aufgaben anfallen.",
"Die Jahreszeit beeinflusst, ob Grünpflege oder Winterdienst im Vordergrund steht.",
"Die geforderten Sicherheitsstandards legen fest, welche Kontrollen nötig sind.",
"Die Einsatzzeiten richten sich nach Ihren Betriebsabläufen und dem Publikumsverkehr.",
"Zugangs- und Sicherheitsregeln müssen berücksichtigt werden, damit alles reibungslos läuft.",
],
},
qualityClaimSection: {
heading: "Unser Qualitätsanspruch in den Hausmeisterdiensten",
textLeft:
"Jedes Objekt und jede Außenanlage bringt eigene Anforderungen mit sich – deshalb entwickeln wir ein Betreuungskonzept, das genau zu Ihrem Gebäude und Erwartungen passt. Entscheidend ist das Zusammenspiel aus Zuverlässigkeit, Service und Sorgfalt, damit die Hausmeisterdienste nicht nur kurzfristig gut wirken, sondern dauerhaft gleichbleibende Ergebnisse liefern.",
textRight:
"Genau diese Balance schaffen wir mit klar definierten Prozessen, transparenter Abstimmung und einem eingespielten Team, das zuverlässig und sorgfältig arbeitet.",
image: "/images/slides/hausmeisterdienste-1.jpg",
imageAlt: "Hausmeister bei der zuverlässigen Objektbetreuung",
},
conceptSection: {
heading: "Unser Konzept für Hausmeisterdienste",
lead: "Mit Basiclean erhalten Sie eine planbare Objektbetreuung, die sich an Ihrem Gebäude anpasst und durch klare Abläufe, Dokumentation und regelmäßige Kontrollen konstant bleibt.",
pillars: [
{
number: "01",
title: "Zuverlässigkeit",
points: [
"Klare Einsatzpläne",
"Regelmäßige Kontrollgänge",
"Dokumentierte Ergebnisse",
],
},
{
number: "02",
title: "Service",
points: [
"Fester Ansprechpartner",
"Flexible Einsatzzeiten",
"Schnelle Reaktion bei Sonderfällen",
],
},
{
number: "03",
title: "Sicherheit",
points: [
"Definierte Sicherheitsstandards",
"Fokus auf Außenanlagen und Wege",
"Betreuung mit System",
],
},
],
results: {
heading: "Ergebnisse die überzeugen",
intro: "Mit den Hausmeisterdiensten in Hannover profitieren Sie von:",
points: [
"Individuelle Betreuungskonzepte",
"Faire und transparente Preise",
"Qualifiziertes Personal",
"Eingespielte Teams für gleichbleibende Qualität",
"Flexible Einsatzintervalle nach Bedarf und Wunsch",
],
},
},
processSection: {
heading: "4-Stufen-Prozess für Ihre Hausmeisterdienste",
lead: "Damit Ihre Objektbetreuung dauerhaft zuverlässig, planbar und nachvollziehbar bleibt, arbeiten wir bei Basiclean nach einem klaren Ablauf – von der Bedarfsklärung bis zur laufenden Qualitätskontrolle.",
steps: [
{ number: "01", title: "Individuelle Analyse" },
{ number: "02", title: "Einsatzplan erstellen" },
{ number: "03", title: "Regelmäßige Durchführung" },
{ number: "04", title: "Feedback und Qualitätskontrolle" },
],
},
contactCtaSection: {
heading: "Lassen Sie uns über Ihre Hausmeisterdienste sprechen",
lead: "Wir erstellen ein individuelles Betreuungskonzept für Ihr Objekt in Hannover und Umgebung. Unverbindlich, transparent und exakt auf Ihre Anforderung abgestimmt.",
},
},
];
+3
View File
@@ -0,0 +1,3 @@
export default function Leistungen() {
return <h1>Leistungen</h1>;
}
+2
View File
@@ -1,6 +1,7 @@
import AboutUsSection from "@/components/AboutUsSection";
import BenefitsSection from "@/components/benefitsSection";
import ContactCtaSection from "@/components/contact/ContactCtaSection";
import CtaSection from "@/components/CtaSection";
import { homepageFaq } from "@/components/faq/faqData";
import FaqSection from "@/components/faq/FaqSection";
import HeroSection from "@/components/HeroSection";
@@ -13,6 +14,7 @@ export default function Home() {
<HeroSection />
<ServiceSection />
<BenefitsSection />
<CtaSection />
<IndustrySolutionsSection />
<FaqSection
title="In drei Schritten zu planbarer Sauberkeit"
+8
View File
@@ -0,0 +1,8 @@
export default function Datenschutz() {
return (
<main>
<h1>Datenschutz</h1>
</main>
);
}
+8
View File
@@ -0,0 +1,8 @@
export default function Impressum() {
return (
<main>
<h1>impressum</h1>
</main>
);
}
+55 -13
View File
@@ -25,29 +25,71 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: 1.0,
},
{
url: `${BASE_URL}/about`,
url: `${BASE_URL}/leistungen`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/leistungen/fensterreinigung`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/leistungen/grundreinigung`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/leistungen/unterhaltsreinigung`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/leistungen/hausmeisterdienste`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/leistungen/treppenhausreinigung`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${BASE_URL}/branchenloesungen/bueros`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.6,
},
{
url: `${BASE_URL}/branchenloesungen/kanzlei`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.6,
},
{
url: `${BASE_URL}/branchenloesungen/hallen-und-lagerflaechen`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.6,
},
{
url: `${BASE_URL}/branchenloesungen/wohn-und-geschaeftshaeuser`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.6,
},
{
url: `${BASE_URL}/kontakt`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.6,
},
{
url: `${BASE_URL}/datenschutz`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${BASE_URL}/impressum`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.3,
},
];
// ─── Dynamische Blog-Seiten (auskommentiert bis Blog vorhanden) ────────────
-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() {
return (
<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>
);
}
+2 -2
View File
@@ -55,7 +55,7 @@
.textBlock {
display: flex;
flex-direction: column;
gap: var(--space-5);
gap: var(--space-4);
align-items: center;
}
@@ -107,7 +107,7 @@
@media (min-width: 1024px) {
.section {
padding-block: var(--space-32);
padding-block: var(--space-24);
}
.inner {
+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>
);
}
+75
View File
@@ -0,0 +1,75 @@
import { CheckCircleIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./conceptSection.module.css";
interface ConceptPillar {
number: string;
title: string;
points: string[];
}
interface ConceptResults {
heading: string;
intro: string;
points: string[];
}
interface ConceptSectionProps {
heading: string;
lead: string;
pillars: ConceptPillar[];
results: ConceptResults;
}
export default function ConceptSection({
heading,
lead,
pillars,
results,
}: ConceptSectionProps) {
return (
<section className={styles.concept} aria-labelledby="concept-heading">
<div className={styles.inner}>
<div className={styles.intro}>
<h2 id="concept-heading" className={styles.heading}>
{heading}
</h2>
<p className={styles.lead}>{lead}</p>
</div>
<div className={styles.bento}>
{pillars.map((pillar) => (
<article key={pillar.number} className={styles.pillarCard}>
<span className={styles.number}>{pillar.number}</span>
<h3 className={styles.pillarTitle}>{pillar.title}</h3>
<ul className={styles.pointList}>
{pillar.points.map((point) => (
<li key={point} className={styles.point}>
<span className={styles.pointIcon} aria-hidden="true">
<CheckCircleIcon size={16} weight="duotone" />
</span>
{point}
</li>
))}
</ul>
</article>
))}
<article className={styles.resultsCard}>
<h3 className={styles.resultsHeading}>{results.heading}</h3>
<p className={styles.resultsIntro}>{results.intro}</p>
<ul className={styles.pointList}>
{results.points.map((point) => (
<li key={point} className={styles.point}>
<span className={styles.pointIcon} aria-hidden="true">
<CheckCircleIcon size={16} weight="duotone" />
</span>
{point}
</li>
))}
</ul>
</article>
</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>
);
}
+53
View File
@@ -0,0 +1,53 @@
.cta {
background: linear-gradient(135deg, #267361 0%, #1f5f50 100%);
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin: 0 auto;
padding-inline: var(--space-4);
}
.content {
max-width: 48rem;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-6);
text-align: center;
}
.heading {
color: #ffffff;
max-width: 22ch;
}
.actions {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.actions :global(.btn) {
font-weight: var(--font-weight-semibold);
}
.actions :global(.btn--tertiary) {
padding-top: 0;
padding-bottom: 0;
color: #ffffff;
}
@media (min-width: 768px) {
.cta {
padding-block: var(--space-20);
}
.actions {
flex-direction: row;
justify-content: center;
}
}
+37
View File
@@ -0,0 +1,37 @@
import { ArrowRightIcon, PhoneIcon } from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import { companyData } from "@/data/companyData";
import styles from "./CtaSection.module.css";
export default function CtaSection() {
return (
<section className={styles.cta} aria-labelledby="cta-heading">
<div className={styles.inner}>
<div className={styles.content}>
<h2 id="cta-heading" className={styles.heading}>
Saubere Räume für einen starken ersten Eindruck
</h2>
<div className={styles.actions}>
<Link href="/kontakt" className="btn btn btn--filled-inverse">
<span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={18} weight="bold" />
</span>
Jetzt Angebot anfragen
</Link>
<a
href={`tel:${companyData.contact.phone.value}`}
className="btn btn--tertiary"
>
<span className="btn-icon" aria-hidden="true">
<PhoneIcon size={18} weight="bold" />
</span>
{companyData.contact.phone.display}
</a>
</div>
</div>
</div>
</section>
);
}
+2 -2
View File
@@ -53,10 +53,10 @@ export default function Footer() {
<span className={styles["footer__nav-title"]}>Rechtliches</span>
<ul>
<li>
<Link href="/impressum">Impressum</Link>
<Link href="/rechtliches/impressum">Impressum</Link>
</li>
<li>
<Link href="/datenschutz">Datenschutz</Link>
<Link href="/rechtliches/datenschutz">Datenschutz</Link>
</li>
</ul>
</div>
+41
View File
@@ -0,0 +1,41 @@
import { CheckCircleIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./maintenanceCleaningSection.module.css";
interface MaintenanceCleaningSectionProps {
heading: string;
lead: string;
requirements: string[];
}
export default function MaintenanceCleaningSection({
heading,
lead,
requirements,
}: MaintenanceCleaningSectionProps) {
return (
<section
className={styles.maintenanceCleaning}
aria-labelledby="maintenance-cleaning-heading"
>
<div className={styles.inner}>
<div className={styles.intro}>
<h3 id="maintenance-cleaning-heading" className={styles.heading}>
{heading}
</h3>
<p className={`lead ${styles.lead}`}>{lead}</p>
</div>
<div className={styles.grid}>
{requirements.map((requirement) => (
<article key={requirement} className={styles.card}>
<span className={styles.iconWrap} aria-hidden="true">
<CheckCircleIcon size={24} weight="duotone" />
</span>
<p className={styles.cardText}>{requirement}</p>
</article>
))}
</div>
</div>
</section>
);
}
+6 -6
View File
@@ -105,19 +105,19 @@
opacity: 0;
transform: translateY(-8px);
transition:
max-height 300ms ease,
opacity 300ms ease,
transform 300ms ease,
visibility 0s linear 300ms;
max-height 200ms ease,
opacity 200ms ease,
transform 200ms ease,
visibility 0s linear 200ms;
background-color: var(--nav-bg);
}
.nav-submenu.open {
transition:
max-height 300ms ease,
max-height 200ms ease,
opacity 300ms ease,
transform 300ms ease,
visibility 0s linear 0s;
visibility 0s linear 0;
}
.nav-submenu-list li a:focus-visible {
+7 -2
View File
@@ -37,13 +37,18 @@ export const NavbarSubMenu: FC<Props> = ({
}) => {
const pathname = usePathname();
const subMenuRef = useRef<HTMLUListElement | null>(null);
const subMenuInnerRef = useRef<HTMLLIElement | null>(null);
const isDesktop = useIsDesktop();
const openMenu = () => setOpenSubmenuId(id);
const closeMenu = () => setOpenSubmenuId(null);
const toggleMenu = () => setOpenSubmenuId(open ? null : id);
useHeightVar(subMenuRef, "--submenu-h", "scroll", open);
// Measure the inner (unclipped) wrapper, not the ul.nav-submenu that
// gets its max-height animated — observing the animated element itself
// causes the ResizeObserver to refire on every step of the transition,
// which makes the animation stutter in Firefox.
useHeightVar(subMenuInnerRef, "--submenu-h", "scroll", open, subMenuRef);
const handleSubmenuClick = (e: MouseEvent<HTMLUListElement>) => {
const target = e.target as HTMLElement;
@@ -79,7 +84,7 @@ export const NavbarSubMenu: FC<Props> = ({
].join(" ")}
onClick={handleSubmenuClick}
>
<li className={styles["nav-submenu-inner"]}>
<li ref={subMenuInnerRef} className={styles["nav-submenu-inner"]}>
<ul className={styles["nav-submenu-list"]}>
{items.map((item) => {
const isActive =
+42
View File
@@ -0,0 +1,42 @@
import styles from "./processSection.module.css";
interface ProcessStep {
number: string;
title: string;
}
interface ProcessSectionProps {
heading: string;
lead: string;
steps: ProcessStep[];
}
export default function ProcessSection({
heading,
lead,
steps,
}: ProcessSectionProps) {
return (
<section className={styles.process} aria-labelledby="process-heading">
<div className={styles.inner}>
<div className={styles.intro}>
<h2 id="process-heading" className={styles.heading}>
{heading}
</h2>
<p className={`lead ${styles.lead}`}>{lead}</p>
</div>
<div className={styles.steps}>
{steps.map((step) => (
<div key={step.number} className={styles.step}>
<span className={styles.stepNumber} aria-hidden="true">
{step.number}
</span>
<span className={styles.stepTitle}>{step.title}</span>
</div>
))}
</div>
</div>
</section>
);
}
+70
View File
@@ -0,0 +1,70 @@
import Image from "next/image";
import Link from "next/link";
import { PhoneIcon, ArrowRightIcon } from "@phosphor-icons/react/dist/ssr";
import { companyData } from "@/data/companyData";
import styles from "./qualityClaimSection.module.css";
interface QualityClaimSectionProps {
heading: string;
textLeft: string;
textRight: string;
image: string;
imageAlt: string;
}
export default function QualityClaimSection({
heading,
textLeft,
textRight,
image,
imageAlt,
}: QualityClaimSectionProps) {
return (
<section
className={styles.qualityClaim}
aria-labelledby="quality-claim-heading"
>
<div className={styles.inner}>
<div className={styles.imageWrapper}>
<Image
src={image}
alt={imageAlt}
fill
className={styles.image}
sizes="(max-width: 1024px) 100vw, 320px"
/>
</div>
<div className={styles.content}>
<h2 id="quality-claim-heading" className={styles.heading}>
{heading}
</h2>
<div className={styles.textGrid}>
<p className={styles.text}>{textLeft}</p>
<p className={styles.text}>{textRight}</p>
</div>
<div className={styles.actions}>
<a
href={`tel:${companyData.contact.phone.value}`}
className="btn btn--tertiary"
>
<span className="btn-icon" aria-hidden="true">
<PhoneIcon size={18} weight="bold" />
</span>
{companyData.contact.phone.display}
</a>
<Link href="/kontakt" className="btn btn--filled">
<span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={18} weight="bold" />
</span>
Kostenloses Angebot sichern
</Link>
</div>
</div>
</div>
</section>
);
}
+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>
);
}
+1 -1
View File
@@ -95,7 +95,7 @@
/* Desktop: Intro links, Grid rechts — nebeneinander */
@media (min-width: 1024px) {
.benefits {
padding-block: var(--space-36);
padding-block: var(--space-24);
}
.inner {
+116
View File
@@ -0,0 +1,116 @@
.concept {
background: var(--color-background-linear);
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-8);
}
.intro {
display: flex;
flex-direction: column;
gap: var(--space-4);
max-width: 60ch;
}
.lead strong {
font-weight: var(--font-weight-semibold);
}
.bento {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
}
.pillarCard,
.resultsCard {
background: var(--color-surface-muted, #e6ede9);
border-radius: 12px;
padding: var(--space-5);
}
.number {
display: block;
color: var(--color-accent);
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
}
.pillarTitle {
color: var(--color-accent);
font-size: var(--font-size-m);
padding: 0 0 8px 0;
}
.resultsHeading {
color: var(--color-accent);
font-size: var(--font-size-h2);
padding: 0 0 8px 0;
}
.resultsIntro {
color: var(--color-text-muted);
color: var(--color-accent);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-m);
padding: 0 0 8px 0;
}
.resultsIntro strong {
font-weight: var(--font-weight-semibold);
color: var(--font-heading-color);
}
.pointList {
display: flex;
flex-direction: column;
gap: var(--space-2);
list-style: none;
padding: 0;
margin: 0;
}
.point {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
.pointIcon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--color-accent);
}
/* Tablet */
@media (min-width: 768px) {
.concept {
padding-block: var(--space-24);
}
.bento {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.resultsCard {
grid-column: 1 / -1;
}
}
/* Desktop */
@media (min-width: 1024px) {
.concept {
padding-block: var(--space-24);
}
}
@@ -1,6 +1,6 @@
.section {
background: #ffffff;
padding-bottom: var(--space-16);
padding-block: var(--space-24);
}
.inner {
+24 -8
View File
@@ -4,6 +4,13 @@ import { companyData } from "@/data/companyData";
import styles from "./ContactCtaSection.module.css";
import { contactCtaData, type ContactInfo } from "./ContactCtaData";
interface ContactCtaSectionProps {
heading?: string;
lead?: string;
buttonLabel?: string;
buttonHref?: string;
}
function resolveCompanyContact(
item: Extract<ContactInfo, { type: "company" }>,
) {
@@ -32,8 +39,17 @@ function resolveCompanyContact(
return null;
}
export default function ContactCtaSection() {
const { heading, lead, buttonLabel, buttonHref, contacts } = contactCtaData;
export default function ContactCtaSection({
heading,
lead,
buttonLabel,
buttonHref,
}: ContactCtaSectionProps = {}) {
const resolvedHeading = heading ?? contactCtaData.heading;
const resolvedLead = lead ?? contactCtaData.lead;
const resolvedButtonLabel = buttonLabel ?? contactCtaData.buttonLabel;
const resolvedButtonHref = buttonHref ?? contactCtaData.buttonHref;
const { contacts } = contactCtaData;
return (
<section className={styles.section} aria-labelledby="contact-cta-heading">
@@ -42,19 +58,19 @@ export default function ContactCtaSection() {
<div className={styles.top}>
<div className={styles.text}>
<h2 id="contact-cta-heading" className={styles.heading}>
{heading}
{resolvedHeading}
</h2>
<p className={styles.lead}>{lead}</p>
<p className={styles.lead}>{resolvedLead}</p>
</div>
<Link href={buttonHref} className="btn btn--filled-inverse">
</div>
<div>
<Link href={resolvedButtonHref} className="btn btn--filled-inverse">
<span className="btn-icon animate" aria-hidden="true">
<ArrowRightIcon size={16} weight="bold" />
</span>
{buttonLabel}
{resolvedButtonLabel}
</Link>
</div>
<hr className={styles.divider} aria-hidden="true" />
<div className={styles.contactRow}>
+5 -3
View File
@@ -17,10 +17,11 @@
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
}
.heading {
max-width: 11ch;
max-width: 24ch;
text-align: left;
color: var(--color-accent);
}
@@ -29,6 +30,7 @@
display: flex;
flex-direction: column;
gap: var(--space-3);
min-width: 0;
}
.item {
@@ -93,7 +95,7 @@
.answer {
max-width: 52ch;
color: var(--color-text-muted);
font-size: var(----font-size-xs);
font-size: var(--font-size-xs);
line-height: 1.5;
}
@@ -103,7 +105,7 @@
}
.inner {
grid-template-columns: 280px 1fr;
grid-template-columns: minmax(0, 40%) minmax(0, 60%);
gap: var(--space-16);
}
+247
View File
@@ -15,3 +15,250 @@ export const homepageFaq = [
"Wir planen flexibel und reinigen dann, wenn es Ihren Betrieb am wenigsten stört. Auf Wunsch auch früh morgens, abends oder am Wochenende.",
},
];
export const leistungenFaq = [
{
slug: "unterhaltsreinigung",
title: "Häufige Fragen zur Unterhaltsreinigung",
faq: [
{
question: "Wie oft findet die Unterhaltsreinigung statt?",
answer:
"Das legen wir gemeinsam mit Ihnen fest – ob täglich, wöchentlich oder in individuellen Intervallen. Entscheidend sind Nutzung, Besucherfrequenz und Ihre Hygieneanforderungen.",
},
{
question:
"Welche Bereiche werden bei der Unterhaltsreinigung gereinigt?",
answer:
"Wir reinigen Büros, Sanitärbereiche, Gemeinschaftsflächen und weitere vereinbarte Räume nach einem festen Reinigungsplan, der auf Ihr Objekt abgestimmt ist.",
},
{
question: "Kann ich die Reinigungszeiten selbst bestimmen?",
answer:
"Ja, wir richten uns nach Ihrem Betriebsablauf und reinigen dann, wenn es am wenigsten stört – auch früh morgens, abends oder am Wochenende.",
},
{
question:
"Wie wird die Qualität der Unterhaltsreinigung sichergestellt?",
answer:
"Durch regelmäßige Objektkontrollen, feste Ansprechpartner und ein dokumentiertes Qualitätsmanagement stellen wir dauerhaft gleichbleibende Ergebnisse sicher.",
},
],
},
{
slug: "fensterreinigung",
title: "Häufige Fragen zur Fensterreinigung",
faq: [
{
question: "Wie oft sollten Fenster professionell gereinigt werden?",
answer:
"Das hängt von Lage, Ausrichtung und Nutzung Ihres Gebäudes ab. Wir empfehlen ein individuelles Intervall, das wir gemeinsam mit Ihnen festlegen.",
},
{
question: "Reinigen Sie auch schwer zugängliche Glasflächen?",
answer:
"Ja, mit geschultem Personal und passendem Equipment reinigen wir auch höhere und schwer erreichbare Fensterfronten sicher und zuverlässig.",
},
{
question: "Werden Rahmen und Fensterbänke mitgereinigt?",
answer:
"Auf Wunsch beziehen wir Rahmen, Fensterbänke und angrenzende Glasflächen wie Eingangsbereiche in die Reinigung mit ein.",
},
{
question: "Was kostet eine professionelle Fensterreinigung?",
answer:
"Die Kosten richten sich nach Fensteranzahl, Erreichbarkeit und Reinigungsintervall. Nach einer kurzen Besichtigung erhalten Sie ein transparentes Angebot ohne versteckte Nebenkosten.",
},
],
},
{
slug: "grundreinigung",
title: "Häufige Fragen zur Grundreinigung",
faq: [
{
question: "Wann ist eine Grundreinigung sinnvoll?",
answer:
"Zum Beispiel nach einem Umzug, vor einer Neueröffnung, nach Umbauarbeiten oder als saisonale Tiefenreinigung bei besonderem Pflegebedarf.",
},
{
question:
"Wie unterscheidet sich die Grundreinigung von der Unterhaltsreinigung?",
answer:
"Die Grundreinigung ist eine intensive, meist einmalige Tiefenreinigung mit professionellem Equipment, während die Unterhaltsreinigung regelmäßig und planbar erfolgt.",
},
{
question: "Welche Materialien können gereinigt werden?",
answer:
"Wir behandeln unterschiedliche Böden, Oberflächen und Nebenräume fachgerecht und materialschonend, abgestimmt auf den jeweiligen Belag.",
},
{
question: "Wie schnell kann eine Grundreinigung durchgeführt werden?",
answer:
"Je nach Objektgröße und Verschmutzungsgrad planen wir kurzfristige Termine, etwa nach Umzug oder vor einer Eröffnung, um Ihren Zeitrahmen einzuhalten.",
},
],
},
{
slug: "hausmeisterdienste",
title: "Häufige Fragen zu den Hausmeisterdiensten",
faq: [
{
question: "Welche Aufgaben übernehmen Ihre Hausmeisterdienste?",
answer:
"Von Kontrollgängen über Kleinreparaturen bis zu Grünpflege und Winterdienst – wir kümmern uns zuverlässig um die anfallenden Aufgaben rund um Ihr Objekt.",
},
{
question: "Gibt es einen festen Ansprechpartner?",
answer:
"Ja, Sie erhalten einen festen Ansprechpartner, der die Abläufe in Ihrem Objekt kennt und für reibungslose Koordination sorgt.",
},
{
question: "Übernehmen Sie auch den Winterdienst?",
answer:
"Ja, der Winterdienst gehört zu unseren Hausmeisterdiensten und wird je nach Witterung und Vereinbarung zuverlässig durchgeführt.",
},
{
question: "Wie flexibel sind die Einsatzzeiten?",
answer:
"Die Einsatzzeiten richten sich nach Ihrem Betriebsablauf und dem Publikumsverkehr, damit Kontrollgänge und Arbeiten möglichst wenig stören.",
},
],
},
{
slug: "treppenhausreinigung",
title: "Häufige Fragen zur Treppenhausreinigung",
faq: [
{
question: "Wie oft wird das Treppenhaus gereinigt?",
answer:
"Die Häufigkeit richten wir nach Bewohnerzahl und Besucherfrequenz aus – von täglich bis zu individuell vereinbarten Intervallen.",
},
{
question: "Werden auch Handläufe und Türen mitgereinigt?",
answer:
"Ja, neben Böden reinigen wir auch Handläufe, Türen und Eingangsflächen, um ein durchgängig gepflegtes Erscheinungsbild sicherzustellen.",
},
{
question: "Wie wird die Störung für Bewohner minimiert?",
answer:
"Die Reinigung erfolgt in festen, abgestimmten Intervallen zu Zeiten, die den Alltag von Bewohnern und Mitarbeitenden möglichst wenig beeinträchtigen.",
},
{
question: "Gibt es feste Ansprechpartner für die Treppenhausreinigung?",
answer:
"Ja, Sie haben durchgehend denselben Ansprechpartner, sodass Absprachen unkompliziert und Zuständigkeiten klar bleiben.",
},
],
},
];
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.",
},
],
},
];
@@ -0,0 +1,89 @@
.maintenanceCleaning {
background: var(--color-background-linear);
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.intro {
margin-bottom: var(--space-6);
}
.heading {
max-width: 24ch;
padding: 0 0 8px 0;
}
.lead {
font-size: var(--font-size-base);
max-width: 48ch;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-5);
}
.card {
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);
}
.iconWrap {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
color: var(--color-accent);
}
.cardText {
color: var(--font-heading-color);
font-size: var(--font-size-xs);
}
/* Tablet */
@media (min-width: 768px) {
.maintenanceCleaning {
padding-block: var(--space-24);
}
.heading {
text-align: left;
}
.lead {
text-align: left;
}
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Desktop: Grid dreispaltig, Intro bleibt volle Breite oben */
@media (min-width: 1024px) {
.maintenanceCleaning {
padding-block: var(--space-24);
}
.inner {
padding-inline: var(--space-6);
}
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
+106
View File
@@ -0,0 +1,106 @@
.process {
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
}
.intro {
display: flex;
flex-direction: column;
gap: var(--space-4);
margin-bottom: var(--space-8);
}
.heading {
max-width: 24ch;
}
.lead {
max-width: 40ch;
}
.steps {
display: flex;
flex-direction: column;
gap: var(--space-8);
min-width: 0;
}
.step {
display: flex;
align-items: center;
gap: var(--space-4);
min-width: 0;
}
.stepNumber {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--color-accent);
color: var(--color-surface);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-base);
box-shadow: 0 0 0 8px var(--color-surface-muted, #e6ede9);
}
.stepTitle {
color: var(--font-heading-color);
font-size: var(--font-size-h5);
font-weight: var(--font-weight-bold);
min-width: 0;
padding-left: 16px;
}
/* Tablet */
@media (min-width: 768px) {
.process {
padding-block: var(--space-24);
}
.stepNumber {
width: 48px;
height: 48px;
}
.steps {
gap: var(--space-10);
}
}
/* Desktop: 60/40 Grid, Intro links, Steps rechts, beide vertikal zentriert */
@media (min-width: 1024px) {
.process {
padding-block: var(--space-24);
}
.steps {
gap: var(--space-16);
}
.stepNumber {
width: 64px;
height: 64px;
box-shadow: 0 0 0 12px var(--color-surface-muted, #e6ede9);
}
.inner {
display: grid;
grid-template-columns: minmax(0, 6fr) minmax(0, 4fr);
gap: var(--space-16);
align-items: center;
padding-inline: var(--space-6);
}
.intro {
margin-bottom: 0;
}
}
+103
View File
@@ -0,0 +1,103 @@
.qualityClaim {
padding-block: var(--space-16);
}
.inner {
max-width: var(--container-width);
margin-inline: auto;
padding-inline: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.imageWrapper {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
border-radius: 24px;
overflow: hidden;
display: none;
}
.image {
object-fit: cover;
}
.content {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.heading {
position: relative;
padding-left: var(--space-4);
max-width: 20ch;
}
.heading::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
background: var(--color-accent);
}
.textGrid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-2);
max-width: 48ch;
}
.text {
color: var(--color-text-muted);
}
.actions {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
}
/* Tablet */
@media (min-width: 768px) {
.qualityClaim {
padding-block: var(--space-24);
}
.imageWrapper {
aspect-ratio: 16 / 9;
display: block;
max-height: 320px;
max-width: 320px;
}
.textGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
max-width: 100%;
gap: var(--space-8);
}
}
/* Desktop: Bild feste Breite links, Content rechts */
@media (min-width: 1024px) {
.inner {
flex-direction: row;
align-items: center;
gap: var(--space-10);
}
.imageWrapper {
width: 320px;
max-height: 100%;
flex-shrink: 0;
aspect-ratio: 2 / 3;
}
.content {
flex: 1;
}
}
+197
View File
@@ -0,0 +1,197 @@
/* ==========================================================
Mobile First Basis-Styles (< 768px)
========================================================== */
.slider {
/* Slide-Abstand zentral als Variable, pro Breakpoint überschreibbar */
--slide-spacing: 1rem;
width: 100%;
margin: 0 auto;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.viewport {
overflow: hidden;
border-radius: 24px;
}
.container {
display: flex;
touch-action: pan-y pinch-zoom;
/* Kompensiert das padding-left des ersten Slides, damit dieser
bündig am linken Rand des Viewports beginnt. */
margin-left: calc(var(--slide-spacing) * -1);
}
.slide {
flex: 0 0 100%;
min-width: 0;
/* Erzeugt den sichtbaren Abstand zum vorherigen Slide.
Padding statt margin, weil Embla SSR nur bei padding-basiertem
Spacing zuverlässig unterstützt. */
padding-left: var(--slide-spacing);
}
/* Mobil: gestapelt, Bild oben, Text darunter */
.split {
display: grid;
grid-template-columns: 1fr;
height: auto;
overflow: hidden;
}
.imagePanel {
position: relative;
min-width: 0;
height: 230px;
order: -1;
overflow: hidden;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
}
.image {
object-fit: cover;
object-position: center;
}
.textPanel {
display: flex;
flex-direction: column;
justify-content: start;
min-width: 0;
min-height: 220px;
padding: 1.5rem;
color: #fff;
background:
radial-gradient(circle at 5% 0%, rgb(117 211 193 / 58%), transparent 49%),
linear-gradient(135deg, #42ac99 0%, var(--color-accent) 80%);
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px;
}
.heading {
color: #f9f9f9;
padding: 0 0 8px 0;
}
.text {
margin: 0;
line-height: var(--line-height-card);
}
.controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
.arrow {
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 50%;
background: #000;
color: #fff;
cursor: pointer;
}
.arrow:hover:not(:disabled) {
background: #242424;
}
.arrow:disabled {
cursor: not-allowed;
opacity: 0.35;
}
.dots {
display: flex;
align-items: center;
gap: 0.7rem;
}
.dot {
width: 16px;
height: 16px;
padding: 0;
border: 0;
border-radius: 50%;
background: #b8b8b8;
cursor: pointer;
}
.dotActive {
background: #000;
}
.arrow:focus-visible,
.dot:focus-visible {
outline: 3px solid #177f73;
outline-offset: 3px;
}
/* ==========================================================
Tablet (>= 768px): zurück zum Split-Layout, Text links / Bild rechts
========================================================== */
@media (min-width: 768px) {
.slider {
--slide-spacing: 1.5rem;
}
.split {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
height: 360px;
}
.imagePanel {
height: 360px;
order: 0;
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
border-top-left-radius: 0;
}
.textPanel {
min-height: 0;
padding: clamp(1.5rem, 3vw, 3rem);
border-top-left-radius: 24px;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 0;
}
}
/* ==========================================================
Desktop (>= 1024px): finale Maße wie im Ursprungsdesign
========================================================== */
@media (min-width: 1024px) {
.slider {
width: 992px;
--slide-spacing: 2rem;
}
.viewport {
border-radius: 24px;
}
.split {
height: 400px;
}
.imagePanel {
height: 400px;
}
.textPanel {
padding: clamp(2rem, 4vw, 4.75rem);
}
}
+163
View File
@@ -0,0 +1,163 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import Image from "next/image";
import useEmblaCarousel from "embla-carousel-react";
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react/dist/ssr";
import styles from "./Slider.module.css";
type Slide = {
heading: string;
text: string;
image: string;
};
type SliderProps = {
slides: Slide[];
imageAlt: string;
};
export function Slider({ slides, imageAlt }: SliderProps) {
const [emblaRef, emblaApi] = useEmblaCarousel({
align: "start",
loop: false,
duration: 28,
});
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const scrollPrevious = useCallback(() => {
emblaApi?.scrollPrev();
}, [emblaApi]);
const scrollNext = useCallback(() => {
emblaApi?.scrollNext();
}, [emblaApi]);
const scrollTo = useCallback(
(index: number) => {
emblaApi?.scrollTo(index);
},
[emblaApi],
);
const updateSelectedSlide = useCallback(() => {
if (!emblaApi) {
return;
}
setSelectedIndex(emblaApi.selectedScrollSnap());
setCanScrollPrev(emblaApi.canScrollPrev());
setCanScrollNext(emblaApi.canScrollNext());
}, [emblaApi]);
const initializeNavigation = useCallback(() => {
if (!emblaApi) {
return;
}
setScrollSnaps(emblaApi.scrollSnapList());
updateSelectedSlide();
}, [emblaApi, updateSelectedSlide]);
useEffect(() => {
if (!emblaApi) {
return;
}
initializeNavigation();
emblaApi.on("reInit", initializeNavigation);
emblaApi.on("select", updateSelectedSlide);
return () => {
emblaApi.off("reInit", initializeNavigation);
emblaApi.off("select", updateSelectedSlide);
};
}, [emblaApi, initializeNavigation, updateSelectedSlide]);
if (slides.length === 0) {
return null;
}
return (
<section
className={styles.slider}
aria-roledescription="carousel"
aria-label="Weitere Informationen"
>
<div className={styles.viewport} ref={emblaRef}>
<div className={styles.container}>
{slides.map((slide, index) => (
<article
key={`${slide.heading}-${index}`}
className={styles.slide}
aria-hidden={selectedIndex !== index}
>
<div className={styles.split}>
<div className={styles.textPanel}>
<h3 className={styles.heading}>{slide.heading}</h3>
<p className={styles.text}>{slide.text}</p>
</div>
<div className={styles.imagePanel}>
<Image
src={slide.image}
alt={imageAlt}
fill
sizes="(max-width: 700px) 100vw, 50vw"
className={styles.image}
/>
</div>
</div>
</article>
))}
</div>
</div>
{slides.length > 1 && (
<div className={styles.controls}>
<button
type="button"
className={styles.arrow}
onClick={scrollPrevious}
disabled={!canScrollPrev}
suppressHydrationWarning
aria-label="Vorheriger Slide"
>
<CaretLeftIcon size={20} weight="bold" aria-hidden="true" />
</button>
<div className={styles.dots} aria-label="Slide auswählen">
{scrollSnaps.map((_, index) => (
<button
key={index}
type="button"
className={`${styles.dot} ${
selectedIndex === index ? styles.dotActive : ""
}`}
onClick={() => scrollTo(index)}
aria-label={`Slide ${index + 1} von ${slides.length}`}
aria-current={selectedIndex === index ? "true" : undefined}
/>
))}
</div>
<button
type="button"
className={styles.arrow}
onClick={scrollNext}
disabled={!canScrollNext}
suppressHydrationWarning
aria-label="Nächster Slide"
>
<CaretRightIcon size={20} weight="bold" aria-hidden="true" />
</button>
</div>
)}
</section>
);
}
+7 -10
View File
@@ -18,23 +18,23 @@ export const subMenus: SubMenu[] = [
items: [
{
label: "Unterhaltsreinigung",
to: "/dienstleistungen/unterhaltsreinigung",
to: "/leistungen/unterhaltsreinigung",
},
{
label: "Treppenhausreinigung",
to: "/dienstleistungen/treppenhausreinigung",
to: "/leistungen/treppenhausreinigung",
},
{
label: "Fensterreinigung",
to: "/dienstleistungen/fensterreinigung",
to: "/leistungen/fensterreinigung",
},
{
label: "Grundreinigung",
to: "/dienstleistungen/grundreinigung",
to: "/leistungen/grundreinigung",
},
{
label: "Hausmeisterdienste",
to: "/dienstleistungen/hausmeisterdienste",
to: "/leistungen/hausmeisterdienste",
},
],
},
@@ -49,14 +49,11 @@ export const subMenus: SubMenu[] = [
},
{
label: "Wohn- und Geschäftshäuser",
to: "/branchenloesungen/wohn--und-geschaeftshaeuser",
to: "/branchenloesungen/wohn-und-geschaeftshaeuser",
},
{ label: "Kanzlei", to: "/branchenloesungen/kanzlei" },
],
},
];
export const endLinks: NavItem[] = [
{ label: "Unternehmen", to: "/unternehmen" },
{ label: "Kontakt", to: "/kontakt" },
];
export const endLinks: NavItem[] = [{ label: "Kontakt", to: "/kontakt" }];
+10 -3
View File
@@ -9,21 +9,28 @@ export function useHeightVar(
cssVarName: string,
mode: Mode = "scroll",
trigger?: unknown,
targetRef?: React.RefObject<HTMLElement | null>,
) {
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const target = targetRef?.current ?? el;
const measure = () => {
const h = mode === "scroll" ? el.scrollHeight : el.offsetHeight;
el.style.setProperty(cssVarName, `${h}px`);
target.style.setProperty(cssVarName, `${h}px`);
};
measure();
// Observe el (the unclipped content), not target: if target is the
// element whose max-height is being CSS-transitioned, observing it
// directly re-triggers measure() on every layout step of the
// transition, fighting the animation (visible as stutter in Firefox).
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, [ref, cssVarName, mode, trigger]);
}
}, [ref, targetRef, cssVarName, mode, trigger]);
}
+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,
},
});
+401 -309
View File
File diff suppressed because it is too large. Load diff
+5 -3
View File
@@ -10,9 +10,11 @@
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"next": "16.2.4",
"react": "19.2.4",
"react-dom": "19.2.4",
"embla-carousel-react": "^8.6.0",
"next": "^16.3.4",
"nodemailer": "^10.0.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"vanilla-cookieconsent": "^3.1.0"
},
"devDependencies": {
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

+2 -1
View File
@@ -199,10 +199,11 @@
/* Tertiary */
.btn--tertiary {
background-color: transparent;
color: var(--btn-text-dark);
color: var(--color-text);
border-color: transparent;
box-shadow: var(--btn-shadow-none);
padding-inline: var(--btn-tertiary-px);
font-weight: 600;
}
.btn--tertiary .btn-icon {
+11 -1
View File
@@ -13,9 +13,12 @@
--font-size-h4: clamp(1.266rem, 3.17vw, 1.802rem);
--font-size-h5: clamp(1.125rem, 2.82vw, 1.602rem);
--font-size-subhead: clamp(1.125rem, 3.02vw, 1.802rem);
--line-height-body: 1.5;
--line-height-card: 1.2;
--line-height-heading: 1.1;
--line-height-lead: 1.2;
--line-height-lead: 1.4;
--letter-spacing-tight: -0.02em;
--letter-spacing-normal: 0;
@@ -87,3 +90,10 @@ h4 {
h5 {
font-size: var(--font-size-h5);
}
@media (min-width: 768px) {
:root {
--line-height-body: 1.4;
--line-height-card: 1.2;
}
}