Compare commits

...

8 Commits

Author SHA1 Message Date
243978bfdd Resolve merge conflicts from Canva redesign; update configuration and component files for consistency and alignment with new design. 2026-05-05 22:46:11 +02:00
aac9e52bfb Merge branch 'codex/canva-redesign'
# Conflicts:
#	backlog/config.yml
#	src/components/cta.tsx
#	src/components/faq7.tsx
#	src/components/feature284.tsx
#	src/pages/index.astro
#	src/styles/global.css
2026-05-05 22:41:58 +02:00
e57678a68d Cleanly separate hero color fields 2026-05-05 22:37:20 +02:00
55a189e78e Use German umlauts in landing copy 2026-05-05 22:35:43 +02:00
e2ca8074b8 Redesign landing page from Canva brief 2026-05-05 22:33:46 +02:00
0d8b56864a Prepare Canva redesign worktree 2026-05-05 22:25:34 +02:00
73299fd18a Initialize redesign task tracking 2026-05-05 22:21:21 +02:00
117839058b Professionalize landing page design 2026-05-05 22:20:09 +02:00
19 changed files with 948 additions and 585 deletions

3
.gitignore vendored
View File

@@ -5,6 +5,7 @@ dist/
# dependencies
node_modules/
.worktrees/
# logs
npm-debug.log*
@@ -23,4 +24,4 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
.kilo
.env.local
.env.local

14
backlog/config.yml Normal file
View File

@@ -0,0 +1,14 @@
project_name: "Dev Landing"
default_status: "To Do"
statuses: ["To Do", "In Progress", "Done"]
labels: []
date_format: yyyy-mm-dd
max_column_width: 20
auto_open_browser: true
default_port: 6420
remote_operations: false
auto_commit: false
bypass_git_hooks: false
check_active_branches: false
active_branch_days: 30
task_prefix: "task"

View File

@@ -0,0 +1,42 @@
---
id: TASK-1
title: Professionalize landing page design
status: In Progress
assignee: []
created_date: '2026-05-05 19:55'
updated_date: '2026-05-05 20:13'
labels: []
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Refresh the existing landing page so it feels more professional while preserving the current regional KMU positioning and current content structure.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Visual hierarchy is clearer across the first viewport and content sections
- [x] #2 Styling feels cohesive and professional without generic AI visual patterns
- [x] #3 Responsive layout remains usable on mobile and desktop
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Audit current structure and design context
2. Tighten theme tokens and page rhythm
3. Refresh hero/trust/features/pricing/contact styling
4. Build and visual-check responsive behavior
5. Mark acceptance criteria verified, leave task In Progress for user confirmation
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Backlog was missing in the repo, so I initialized it with CLI defaults and disabled remote branch checks after sandbox git fetch failed. Design context exists in .impeccable.md: regional KMU, calm/direct/human, warm light theme.
Implemented the professionalization pass: warm OKLCH theme, full-width page shell, redesigned hero with real workspace image, calmer trust/features/stats sections, clearer pricing tabs, refined FAQ/about/contact/footer. Verified with pnpm build, git diff --check, and in-app browser checks for hero visibility and pricing tab interaction. Dev-server hydration errors in browser logs were from the initial Vite dependency optimization at 20:10:49 and did not recur after reload.
<!-- SECTION:NOTES:END -->

View File

@@ -0,0 +1,41 @@
# Canva Redesign Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rework the dev landing page into a bold black, red, and white editorial site inspired by the Canva creative brief deck.
**Architecture:** Replace the current stacked section imports on the homepage with one focused React landing component. Keep existing UI primitives available but avoid broad refactors of old sections so the redesign remains easy to review or revert.
**Tech Stack:** Astro 6, React 19, Tailwind CSS 4, lucide-react, Node test runner.
---
### Task 1: Landing Smoke Test
**Files:**
- Create: `tests/landing-content.test.mjs`
- Modify: `package.json`
- [x] Add a Node smoke test that checks the new component source for key content anchors: `Projektbrief`, `01`, `Website`, `Kontakt`.
- [x] Run `node --test tests/landing-content.test.mjs` and confirm it fails before the component exists.
### Task 2: Canva-Inspired Landing Page
**Files:**
- Create: `src/components/canva-landing.tsx`
- Modify: `src/pages/index.astro`
- Modify: `src/styles/global.css`
- [ ] Build a single-page layout with a dark editorial shell, red accent panels, large German headline, numbered sections, pricing/service strips, and a contact brief section.
- [ ] Replace the existing homepage component stack with the new `CanvaLanding` component.
- [ ] Update global tokens for the dark, high-contrast Canva reference style.
### Task 3: Verification
**Files:**
- Modify: `backlog/tasks/task-1 - Redesign-dev-website-from-Canva-reference.md`
- [ ] Run `node --test tests/landing-content.test.mjs` and confirm it passes.
- [ ] Run `CI=true pnpm run build` and confirm Astro builds the page.
- [ ] Start the local dev server and visually review the page in a browser/screenshot.
- [ ] Check off remaining acceptance criteria that have direct evidence.

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
allowBuilds:
esbuild: true
msw: true
sharp: true

11
solo.yml Normal file
View File

@@ -0,0 +1,11 @@
# Visit https://soloterm.com to learn more
name: Dev-Landing
icon: null
processes:
Astro:
command: pnpm run dev
working_dir: null
auto_start: false
auto_restart: true
restart_when_changed: []
env: {}

View File

@@ -7,32 +7,48 @@ interface About19Props {
const About19 = ({ className }: About19Props) => {
return (
<section className={cn("py-32", className)}>
<div className="container">
<div className="grid grid-cols-1 gap-15 lg:grid-cols-7 lg:gap-1">
<div className="col-span-4 h-120">
<section className={cn("px-4 py-20 sm:px-6 lg:px-8 lg:py-28", className)}>
<div className="mx-auto max-w-6xl">
<div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,0.88fr)] lg:items-start lg:gap-14">
<div className="overflow-hidden rounded-lg border border-border bg-card">
<img
src="/about.jpg"
alt=""
className="h-full w-full object-cover rounded-xl shadow-md"
alt="Matthias Meister bei der Webentwicklung"
className="h-[20rem] w-full object-cover sm:h-[28rem] lg:h-[34rem]"
/>
</div>
<div className="col-span-3 ml-auto max-w-4xl space-y-15 lg:pl-15">
<h1 className="text-2xl font-medium tracking-tight">
Hallo, ich bin Matthias.
</h1>
<p className="text-base text-foreground/40 lg:text-lg">
Ich bin in der Region aufgewachsen, war durch die Bundeswehr viele Jahre weg und bin jetzt zurück. Und ich plane zu bleiben.
<div className="max-w-xl">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Über die Zusammenarbeit
</p>
<p className="text-base text-foreground/40 lg:text-lg">
Seit über 15 Jahren beschäftige ich mich mit Webentwicklung und Software. Einen Großteil davon intern für die Bundeswehr Projekte die ich Ihnen leider nicht zeigen kann. Was ich Ihnen zeigen kann: Wie ich arbeite. Zuverlässig, präzise und ohne unnötigen Schnickschnack.
</p>
<p className="text-base text-foreground/40 lg:text-lg">
Neben Websites für regionale Unternehmen entwickle ich eigene Software und Apps. Das bedeutet: Wenn Ihre Anforderungen irgendwann über eine einfache Website hinausgehen, bin ich noch immer der richtige Ansprechpartner.
</p>
<p className="text-base text-foreground/40 lg:text-lg">
Mein Ziel ist es, Unternehmen aus der Region Handwerker, Friseure, Ärzte mit dem auszustatten, was Großstadtagenturen ihren Kunden für viel mehr Geld verkaufen. Eine Website die funktioniert, gefunden wird und Ihnen keine Kopfschmerzen macht.
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Hallo, ich bin Matthias. Zurück in der Region und hier, um zu
bleiben.
</h2>
<div className="mt-8 space-y-5 text-base leading-8 text-muted-foreground">
<p>
Ich bin in der Region aufgewachsen, war durch die Bundeswehr
viele Jahre weg und bin jetzt zurück.
</p>
<p>
Seit über 15 Jahren beschäftige ich mich mit Webentwicklung und
Software. Einen Großteil davon intern für die Bundeswehr:
Projekte, die ich Ihnen leider nicht zeigen kann. Was ich Ihnen
zeigen kann: wie ich arbeite. Zuverlässig, präzise und ohne
unnötigen Schnickschnack.
</p>
<p>
Neben Websites für regionale Unternehmen entwickle ich eigene
Software und Apps. Wenn Ihre Anforderungen irgendwann über eine
einfache Website hinausgehen, bleibt der Ansprechpartner also
derselbe.
</p>
</div>
<p className="mt-8 rounded-lg border border-border bg-card p-5 text-base font-medium leading-7 text-foreground">
Mein Ziel: Unternehmen aus der Region mit einer Website
ausstatten, die funktioniert, gefunden wird und Ihnen keine
Kopfschmerzen macht.
</p>
</div>
</div>

View File

@@ -0,0 +1,249 @@
import {
ArrowUpRight,
Check,
CornerDownRight,
Mail,
MapPin,
Phone,
} from "lucide-react";
const services = [
{
number: "01",
title: "Website",
text: "Eine klare Startseite oder ein kompletter Auftritt, der sofort zeigt, warum man Ihnen vertrauen kann.",
},
{
number: "02",
title: "Struktur",
text: "Angebot, Beweise, Ablauf und Kontakt werden so sortiert, dass Besucher nicht suchen müssen.",
},
{
number: "03",
title: "Technik",
text: "Schnell, mobil sauber, DSGVO-arm und so gebaut, dass spätere Änderungen nicht zum Projekt werden.",
},
];
const deliverables = [
"Strategie und Seitenstruktur",
"Individuelles Screen-Design",
"Astro/React Umsetzung",
"Kontaktformular und Datenschutz",
"Hosting, Wartung und Analytics",
];
const packages = [
{
name: "Basis",
price: "799 EUR",
detail: "Eine starke Seite für ein klares Angebot.",
},
{
name: "Profi",
price: "1.499 EUR",
detail: "Mehrere Seiten für Betriebe mit erklärungsbedürftigem Angebot.",
},
{
name: "Maßarbeit",
price: "2.499 EUR+",
detail: "Individuelle Struktur, CMS und besondere Anforderungen.",
},
];
const CanvaLanding = () => {
return (
<main className="min-h-screen overflow-hidden bg-background text-foreground">
<section className="relative grid min-h-screen grid-cols-1 border-b border-border lg:grid-cols-[1.08fr_0.92fr]">
<div className="flex min-h-[640px] flex-col justify-between px-5 py-5 sm:px-8 lg:px-12">
<header className="grid gap-4 border-b border-border pb-5 text-xs uppercase tracking-[0.28em] text-muted-foreground sm:grid-cols-[1fr_auto]">
<a href="/" className="font-semibold text-foreground">
Matthias Meister
</a>
<nav className="flex flex-wrap gap-5">
<a href="#leistungen">Leistungen</a>
<a href="#pakete">Pakete</a>
<a href="#kontakt">Kontakt</a>
</nav>
</header>
<div className="max-w-5xl py-16 sm:py-20 lg:py-24">
<p className="mb-6 max-w-sm text-sm uppercase tracking-[0.32em] text-primary">
Projektbrief für regionale Unternehmen
</p>
<h1 className="max-w-[11ch] text-[clamp(4.25rem,13vw,11.5rem)] font-black uppercase leading-[0.78] tracking-normal text-foreground">
Website ohne Umweg
</h1>
<div className="mt-8 grid gap-7 border-t border-border pt-7 lg:grid-cols-[0.72fr_1fr]">
<p className="text-sm uppercase tracking-[0.24em] text-muted-foreground">
Strategie trifft Umsetzung
</p>
<p className="max-w-2xl text-lg leading-8 text-foreground/78">
Ich baue Websites für Handwerk, Praxen, Salons und
Dienstleister aus der Region. Direkt, glaubwürdig und so
reduziert, dass der nächste Kontakt naheliegt.
</p>
</div>
</div>
<div className="grid gap-4 border-t border-border pt-5 text-sm uppercase tracking-[0.2em] text-muted-foreground sm:grid-cols-3">
<span>Antwort in 24h</span>
<span>DSGVO-arm</span>
<span>Hosting aus DE</span>
</div>
</div>
<aside className="relative flex min-h-[520px] flex-col justify-between border-t border-primary-foreground/25 bg-primary px-5 py-5 text-primary-foreground sm:px-8 lg:border-l lg:border-t-0 lg:px-12">
<div className="flex items-start justify-between border-b border-primary-foreground/30 pb-5 text-xs uppercase tracking-[0.28em]">
<span>Creative Direction</span>
<span>2026</span>
</div>
<div className="absolute bottom-32 right-10 h-36 w-36 border border-primary-foreground/35" />
<div className="relative mt-28 max-w-xl">
<p className="text-[clamp(4rem,10vw,9rem)] font-black uppercase leading-[0.75] tracking-normal">
01
</p>
<p className="mt-7 max-w-sm text-2xl font-semibold uppercase leading-none">
Klarer Auftritt. Harte Kante. Weniger Agenturlärm.
</p>
</div>
<a
href="#kontakt"
className="group inline-flex w-fit items-center gap-3 border border-primary-foreground px-5 py-4 text-sm font-semibold uppercase tracking-[0.18em] transition hover:bg-primary-foreground hover:text-primary"
>
Projekt anfragen
<ArrowUpRight className="size-5 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
</a>
</aside>
</section>
<section
id="leistungen"
className="grid border-b border-border lg:grid-cols-[0.36fr_0.64fr]"
>
<div className="border-b border-border px-5 py-12 sm:px-8 lg:border-b-0 lg:border-r lg:px-12 lg:py-20">
<p className="text-sm uppercase tracking-[0.3em] text-primary">
Leistungen (02)
</p>
<h2 className="mt-6 max-w-[9ch] text-5xl font-black uppercase leading-[0.86] sm:text-7xl">
Vom Brief zur Seite
</h2>
</div>
<div className="divide-y divide-border">
{services.map((service) => (
<article
key={service.number}
className="grid gap-8 px-5 py-10 sm:px-8 md:grid-cols-[7rem_0.4fr_1fr] lg:px-12"
>
<span className="text-5xl font-black text-primary">
{service.number}
</span>
<h3 className="text-3xl font-black uppercase leading-none">
{service.title}
</h3>
<p className="max-w-2xl text-lg leading-8 text-muted-foreground">
{service.text}
</p>
</article>
))}
</div>
</section>
<section className="grid border-b border-border lg:grid-cols-2">
<div className="bg-foreground px-5 py-14 text-background sm:px-8 lg:px-12 lg:py-24">
<p className="text-sm uppercase tracking-[0.3em] text-primary">
Deliverables (03)
</p>
<h2 className="mt-8 max-w-[10ch] text-5xl font-black uppercase leading-[0.86] sm:text-7xl">
Was am Ende steht
</h2>
</div>
<div className="grid content-center gap-4 px-5 py-12 sm:px-8 lg:px-12">
{deliverables.map((item) => (
<div
key={item}
className="flex items-center gap-4 border-b border-border pb-4 text-lg font-semibold uppercase tracking-[0.08em]"
>
<Check className="size-5 text-primary" />
<span>{item}</span>
</div>
))}
</div>
</section>
<section id="pakete" className="border-b border-border px-5 py-14 sm:px-8 lg:px-12 lg:py-24">
<div className="grid gap-8 lg:grid-cols-[0.45fr_0.55fr]">
<div>
<p className="text-sm uppercase tracking-[0.3em] text-primary">
Pakete (04)
</p>
<h2 className="mt-8 max-w-[9ch] text-5xl font-black uppercase leading-[0.86] sm:text-7xl">
Kosten ohne Nebel
</h2>
</div>
<div className="grid gap-4">
{packages.map((item) => (
<article
key={item.name}
className="grid gap-4 border border-border p-5 sm:grid-cols-[0.5fr_0.5fr] sm:p-6"
>
<div>
<p className="text-sm uppercase tracking-[0.24em] text-primary">
{item.name}
</p>
<p className="mt-4 text-4xl font-black uppercase">
{item.price}
</p>
</div>
<p className="self-end text-lg leading-7 text-muted-foreground">
{item.detail}
</p>
</article>
))}
</div>
</div>
</section>
<section
id="kontakt"
className="grid min-h-[620px] lg:grid-cols-[0.72fr_0.28fr]"
>
<div className="px-5 py-14 sm:px-8 lg:px-12 lg:py-24">
<p className="text-sm uppercase tracking-[0.3em] text-primary">
Kontakt (05)
</p>
<h2 className="mt-8 max-w-[12ch] text-5xl font-black uppercase leading-[0.86] sm:text-7xl lg:text-8xl">
Erzählen Sie mir kurz vom Projekt
</h2>
<p className="mt-8 max-w-2xl text-xl leading-8 text-muted-foreground">
Ein paar Sätze reichen: Was bieten Sie an, was soll die Website
leisten, und wann soll sie online sein?
</p>
<a
href="mailto:hallo@matthias-meister.com"
className="mt-10 inline-flex items-center gap-3 bg-primary px-6 py-5 text-sm font-black uppercase tracking-[0.18em] text-primary-foreground transition hover:bg-foreground hover:text-background"
>
<CornerDownRight className="size-5" />
Anfrage per Mail senden
</a>
</div>
<div className="flex flex-col justify-end gap-6 bg-primary px-5 py-10 text-primary-foreground sm:px-8 lg:px-10">
<div className="flex gap-3">
<Mail className="size-5" />
<span>hallo@matthias-meister.com</span>
</div>
<div className="flex gap-3">
<Phone className="size-5" />
<span>Rückmeldung innerhalb von 24 Stunden</span>
</div>
<div className="flex gap-3">
<MapPin className="size-5" />
<span>Regionale KMU in Deutschland</span>
</div>
</div>
</section>
</main>
);
};
export { CanvaLanding };

View File

@@ -21,7 +21,7 @@ const contactFormSchema = z.object({
email: z
.string()
.min(1, "Bitte geben Sie Ihre E-Mail ein")
.email("Bitte geben Sie eine gueltige E-Mail ein"),
.email("Bitte geben Sie eine gültige E-Mail ein"),
message: z.string().min(1, "Bitte beschreiben Sie kurz Ihr Anliegen"),
});
@@ -68,19 +68,29 @@ const Contact21 = ({ className, onSubmit }: Contact21Props) => {
};
return (
<section id="kontakt" className={cn("py-32", className)}>
<div className="container">
<div className="mt-20 flex flex-col justify-between gap-15 md:gap-10 lg:flex-row">
<section
id="kontakt"
className={cn("px-4 py-20 sm:px-6 lg:px-8 lg:py-28", className)}
>
<div className="mx-auto max-w-6xl">
<div className="grid gap-10 rounded-lg border border-border bg-card p-5 sm:p-8 lg:grid-cols-[minmax(0,0.82fr)_minmax(0,1.18fr)] lg:gap-14 lg:p-10">
<div className="flex w-full max-w-lg flex-col justify-between gap-10">
<p className="indent-[22%] text-3xl font-medium tracking-tight text-muted-foreground/50 lg:text-4xl">
Erzählen Sie mir kurz von Ihrem Unternehmen ich melde mich innerhalb von 24 Stunden mit einem unverbindlichen Angebot.
</p>
<div className="mt-5 flex items-center gap-4 lg:mt-20">
<img
src="https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/avatar3.png"
className="size-12"
alt="Matthias Meister"
/>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Kontakt
</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Erzählen Sie kurz, worum es geht.
</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">
Ich melde mich innerhalb von 24 Stunden mit einer ersten
Einschätzung und dem passenden nächsten Schritt.
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex size-12 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
MM
</div>
<div>
<h3 className="text-lg font-medium tracking-tight">
Matthias Meister
@@ -91,26 +101,22 @@ const Contact21 = ({ className, onSubmit }: Contact21Props) => {
</div>
</div>
</div>
<div className="col-span-4 flex w-full flex-col gap-2 lg:pl-10">
<h2 className="mb-7 text-6xl font-semibold tracking-tight lg:text-5xl">
Jetzt Website anfordern
</h2>
{isSubmitted && (
<div
className={cn(
"mb-4 rounded-lg border border-green-500/20 bg-green-500/10 p-4 text-center transition-opacity duration-500",
showSuccess ? "opacity-100" : "opacity-0",
)}
>
<p className="text-sm font-medium text-green-600 dark:text-green-400">
Vielen Dank! Ich melde mich in Kürze bei Ihnen.
</p>
</div>
<div className="w-full">
{isSubmitted && (
<div
className={cn(
"mb-4 rounded-lg border border-primary/20 bg-primary/10 p-4 text-center transition-opacity duration-500",
showSuccess ? "opacity-100" : "opacity-0",
)}
>
<p className="text-sm font-medium text-primary">
Vielen Dank! Ich melde mich in Kürze bei Ihnen.
</p>
</div>
)}
<form onSubmit={form.handleSubmit(handleFormSubmit)}>
<FieldGroup className="gap-0">
<form onSubmit={form.handleSubmit(handleFormSubmit)}>
<FieldGroup className="gap-0">
<Controller
control={form.control}
name="name"
@@ -124,7 +130,7 @@ const Contact21 = ({ className, onSubmit }: Contact21Props) => {
id={field.name}
aria-invalid={fieldState.invalid}
placeholder="Ihr Name*"
className="h-15 rounded-none border-0 border-b border-b-foreground/25 bg-transparent! shadow-none placeholder:text-foreground/20 focus-visible:ring-0 lg:text-base"
className="h-14 rounded-none border-0 border-b border-b-border bg-transparent! px-0 shadow-none placeholder:text-muted-foreground/65 focus-visible:border-b-primary focus-visible:ring-0 lg:text-base"
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
@@ -147,7 +153,7 @@ const Contact21 = ({ className, onSubmit }: Contact21Props) => {
type="email"
aria-invalid={fieldState.invalid}
placeholder="Ihre E-Mail*"
className="h-15 rounded-none border-0 border-b border-b-foreground/25 bg-transparent! shadow-none placeholder:text-foreground/20 focus-visible:ring-0 lg:text-base"
className="h-14 rounded-none border-0 border-b border-b-border bg-transparent! px-0 shadow-none placeholder:text-muted-foreground/65 focus-visible:border-b-primary focus-visible:ring-0 lg:text-base"
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
@@ -170,7 +176,7 @@ const Contact21 = ({ className, onSubmit }: Contact21Props) => {
aria-invalid={fieldState.invalid}
placeholder="Nachricht: Worum geht es bei Ihrem Projekt?"
rows={4}
className="min-h-32 w-full rounded-none border-0 border-b border-b-foreground/25 bg-transparent px-0 py-3 text-base text-foreground shadow-none outline-none placeholder:text-foreground/20 focus-visible:border-ring focus-visible:ring-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive lg:text-base"
className="min-h-36 w-full rounded-none border-0 border-b border-b-border bg-transparent px-0 py-4 text-base text-foreground shadow-none outline-none placeholder:text-muted-foreground/65 focus-visible:border-b-primary focus-visible:ring-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive lg:text-base"
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
@@ -186,7 +192,7 @@ const Contact21 = ({ className, onSubmit }: Contact21Props) => {
)}
<Button
className="mt-15 flex items-center justify-start gap-2 rounded-none px-8! lg:h-12 lg:text-base"
className="mt-8 flex h-11 w-full items-center justify-center gap-2 rounded-md px-6 lg:w-fit lg:text-base"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting ? (

View File

@@ -23,19 +23,18 @@ const trustAnchors = [
export default function CTASection() {
return (
<section className="px-4 pb-16 pt-4 sm:px-6 lg:px-8 lg:pb-24">
<div className="mx-auto max-w-6xl border-y border-border/80 py-8 lg:grid lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.45fr)] lg:gap-12 lg:py-10">
<div className="max-w-md space-y-4">
<section className="px-4 pb-14 sm:px-6 lg:px-8 lg:pb-20">
<div className="mx-auto max-w-6xl border-y border-border/80 py-9 lg:grid lg:grid-cols-[minmax(0,0.82fr)_minmax(0,1.55fr)] lg:gap-14 lg:py-11">
<div className="max-w-md space-y-4 lg:pt-1">
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
Vertrauensanker
Vor dem Angebot
</p>
<h2 className="text-2xl font-semibold tracking-tight text-balance lg:text-3xl">
Ein gemeinsamer Startpunkt statt leerer Versprechen.
Erst verstehen, dann bauen.
</h2>
<p className="text-base leading-7 text-muted-foreground">
Noch bevor es um Pakete oder Features geht, soll direkt klar sein,
warum diese Zusammenarbeit für regionale Unternehmen greifbar und
verlaesslich wirkt.
Die Zusammenarbeit ist bewusst direkt gehalten: ein Gespräch, eine
klare Empfehlung und ein Vorschlag, der zu Ihrem Betrieb passt.
</p>
</div>
<dl className="mt-8 grid gap-6 sm:grid-cols-3 lg:mt-0 lg:gap-0">
@@ -46,14 +45,14 @@ export default function CTASection() {
"space-y-3",
index === 0
? ""
: "border-t border-border/70 pt-4 sm:border-t-0 sm:border-l sm:pl-6 sm:pt-0 lg:pl-8",
: "border-t border-border/70 pt-5 sm:border-t-0 sm:border-l sm:pl-6 sm:pt-0 lg:pl-8",
)}
>
<dt className="text-sm font-medium text-foreground">
<dt className="text-sm font-semibold text-foreground">
{item.title}
</dt>
<dd className="space-y-2">
<p className="text-lg font-semibold leading-7 text-balance text-foreground">
<p className="text-base font-medium leading-7 text-balance text-foreground lg:text-lg">
{item.description}
</p>
<p className="text-sm leading-6 text-muted-foreground">

View File

@@ -41,35 +41,39 @@ interface Faq7Props {
const Faq7 = ({ className }: Faq7Props) => {
return (
<section className={cn("py-32", className)}>
<div className="container">
<div className="mx-auto grid max-w-7xl gap-10 md:grid-cols-2">
<div className="flex flex-col gap-6">
<h2 className="text-4xl font-semibold">
Fragen vor dem Start?
<br />
<span className="text-muted-foreground/70">
Hier finden Sie schnelle Antworten.
</span>
<section className={cn("px-4 py-20 sm:px-6 lg:px-8 lg:py-28", className)}>
<div className="mx-auto max-w-6xl">
<div className="grid gap-10 border-t border-border/80 pt-10 md:grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)] lg:gap-16">
<div className="flex max-w-md flex-col gap-6">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Häufige Fragen
</p>
<h2 className="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Vor dem Start soll nichts schwammig bleiben.
</h2>
<p className="text-lg text-muted-foreground md:text-xl">
Falls noch etwas offen ist, schreiben Sie mir gern ueber das
<a href="#" className="mx-1 whitespace-nowrap underline">
<p className="text-base leading-7 text-muted-foreground">
Falls noch etwas offen ist, schreiben Sie mir gern über das
<a
href="#kontakt"
className="mx-1 whitespace-nowrap underline underline-offset-4 transition-colors hover:text-foreground"
>
Kontaktformular
</a>
.
</p>
<Button size="lg" variant="outline" className="w-fit">
Alle Fragen ansehen
<Button asChild size="lg" variant="outline" className="w-fit rounded-md">
<a href="#kontakt">Frage stellen</a>
</Button>
</div>
<Accordion type="multiple">
<Accordion type="multiple" className="rounded-lg border border-border bg-card px-4">
{faqs.map((faq, index) => (
<AccordionItem key={index} value={`item-${index}`}>
<AccordionTrigger className="text-left">
<AccordionTrigger className="text-left text-base font-semibold">
{faq.question}
</AccordionTrigger>
<AccordionContent>{faq.answer}</AccordionContent>
<AccordionContent className="text-muted-foreground">
{faq.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>

View File

@@ -1,44 +1,43 @@
import { HelpCircleIcon } from "lucide-react";
import React from "react";
import {
Gauge,
Handshake,
MapPinned,
Search,
Smartphone,
} from "lucide-react";
import { GlowingEffect } from "@/components/ui/glowing-effect";
import { cn } from "@/lib/utils";
const featureData = [
{
desc: "Ihre Website erklaert in wenigen Sekunden, fuer wen Sie arbeiten und was Sie konkret anbieten.",
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img1.jpeg",
desc: "Die Startseite sagt schnell, für wen Sie arbeiten, was Sie anbieten und wie Interessenten Kontakt aufnehmen.",
title: "Klare Positionierung",
badgeTitle: "Vorteil 01",
gridClass: "md:col-span-1",
badgeTitle: "01",
icon: MapPinned,
},
{
desc: "Ein zeitgemaesses Design sorgt fuer einen starken ersten Eindruck und passt zu Ihrem Unternehmen.",
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img7.jpeg",
title: "Modernes Erscheinungsbild",
badgeTitle: "Vorteil 02",
gridClass: "lg:col-span-2",
desc: "Gestaltung, Texte und Struktur wirken seriös, ohne den Charakter eines regionalen Betriebs glattzubügeln.",
title: "Glaubwürdiger Auftritt",
badgeTitle: "02",
icon: Handshake,
},
{
desc: "Ihre Inhalte funktionieren sauber auf Smartphone, Tablet und Desktop - ohne Umwege fuer Besucher.",
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img11.jpeg",
title: "Mobil optimiert",
badgeTitle: "Vorteil 03",
gridClass: "md:col-span-1 lg:row-span-2 ",
desc: "Telefonnummer, Formular und zentrale Informationen bleiben auf Smartphone und Desktop leicht erreichbar.",
title: "Mobil sauber geführt",
badgeTitle: "03",
icon: Smartphone,
},
{
desc: "Klare Kontaktwege mit gut sichtbaren Handlungsaufforderungen machen den naechsten Schritt leicht.",
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img2.jpeg",
title: "Anfragen ohne Huerden",
badgeTitle: "Vorteil 04",
gridClass: "lg:col-span-2",
desc: "Technik, Bilder und Inhalte werden so umgesetzt, dass die Seite schnell lädt und stabil bleibt.",
title: "Schnell und robust",
badgeTitle: "04",
icon: Gauge,
},
{
desc: "Die Seite bleibt wartbar aufgebaut, damit Inhalte spaeter schnell angepasst oder erweitert werden koennen.",
img: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/guri3/img4.jpeg",
title: "Pflegeleicht aufgebaut",
badgeTitle: "Vorteil 05",
gridClass: "md:col-span-1",
desc: "Google findet die richtigen Inhalte: Leistungen, Region, Kontakt und die wichtigsten Suchbegriffe.",
title: "Für Suche vorbereitet",
badgeTitle: "05",
icon: Search,
},
];
@@ -48,45 +47,43 @@ interface Feature284Props {
const Feature284 = ({ className }: Feature284Props) => {
return (
<section className={cn("h-full overflow-hidden py-32", className)}>
<div className="container flex h-full w-full items-center justify-center">
<div className="grid w-full max-w-6xl grid-cols-1 grid-rows-2 gap-4 md:grid-cols-2 lg:h-[800px] lg:grid-cols-4">
<section className={cn("px-4 py-20 sm:px-6 lg:px-8 lg:py-28", className)}>
<div className="mx-auto max-w-6xl">
<div className="grid gap-8 border-t border-border/80 pt-10 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.4fr)] lg:gap-16">
<div className="max-w-md">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Was die Seite leisten muss
</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Professionell heißt hier: verständlich, erreichbar, belastbar.
</h2>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{featureData.map((feature, index) => (
<div
key={index}
className={cn(
"relative flex flex-col gap-2 rounded-3xl border p-4",
feature.gridClass,
)}
className="group flex min-h-52 flex-col justify-between rounded-lg border border-border bg-card p-5 transition-colors hover:border-primary/40"
>
<GlowingEffect
spread={40}
glow={true}
disabled={false}
proximity={64}
inactiveZone={0.01}
/>
<div className="flex w-full items-center justify-between">
<p className="text-muted-foreground">{feature.badgeTitle}</p>
<HelpCircleIcon className="size-4 text-muted-foreground" />
</div>
<div
className={cn(
"w-full flex-1 overflow-hidden rounded-3xl bg-muted",
)}
>
<img
src={feature.img}
alt={feature.title}
className="pointer-events-none h-full w-full object-cover"
<div className="flex items-center justify-between">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{feature.badgeTitle}
</p>
<feature.icon
className="size-5 text-primary transition-transform group-hover:-translate-y-0.5"
aria-hidden
/>
</div>
<h3 className="mt-4 text-2xl font-semibold tracking-tight">
{feature.title}
</h3>
<p className="text-muted-foreground">{feature.desc}</p>
<div className="mt-10 space-y-3">
<h3 className="text-xl font-semibold tracking-tight">
{feature.title}
</h3>
<p className="text-sm leading-6 text-muted-foreground">
{feature.desc}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>

View File

@@ -1,10 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { ArrowUpRight } from "lucide-react";
import { ArrowRight, Mail, Phone } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
interface Footer27Props {
@@ -12,137 +8,65 @@ interface Footer27Props {
}
const Footer27 = ({ className }: Footer27Props) => {
const socialLinks = [
{ name: "E-Mail", href: "#" },
{ name: "LinkedIn", href: "#" },
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
duration: 0.6,
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
};
return (
<section className={cn("py-32", className)}>
<div className="container">
<footer>
<footer className={cn("px-4 pb-10 sm:px-6 lg:px-8", className)}>
<div className="mx-auto max-w-6xl border-t border-border/80 pt-8">
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-start">
<div>
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="flex flex-col justify-between md:flex-row md:items-center"
>
<div className="space-y-8">
<motion.div variants={itemVariants} className="space-y-6">
<h2 className="text-4xl leading-tight font-bold text-foreground lg:text-5xl">
Bereit für eine Website, die Kunden bringt?
</h2>
<p className="max-w-md text-lg leading-relaxed text-muted-foreground">
Erzählen Sie mir kurz von Ihrem Unternehmen ich melde mich innerhalb von 24 Stunden mit einem unverbindlichen Angebot.
</p>
</motion.div>
<motion.div variants={itemVariants}>
<Button size="lg">Kostenloses Angebot anfordern</Button>
</motion.div>
</div>
<div className="mt-5 space-y-8 md:mt-0">
<motion.div variants={itemVariants}>
<div className="space-y-6">
{socialLinks.map((link) => (
<motion.div
key={link.name}
variants={itemVariants}
whileHover={{ x: 4 }}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
}}
>
<a
href={link.href}
className="group flex items-center gap-2 py-2 text-foreground transition-colors hover:text-foreground/80"
>
<span className="text-xl font-medium">
{link.name}
</span>
<ArrowUpRight className="h-6 w-6 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
</motion.div>
))}
</div>
</motion.div>
</div>
</motion.div>
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
className="mt-16"
>
<motion.div variants={itemVariants}>
<Separator className="mb-8" />
</motion.div>
<motion.div
variants={itemVariants}
className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center"
>
<p className="text-sm text-muted-foreground">
© 2025 Matthias Meister Webdesign Crimmitschau
</p>
<div className="flex items-center gap-6 text-sm">
<span className="text-muted-foreground">
Kontakt:{" "}
<a href="mailto:info@matthias-meister-webdesign.de" className="underline underline-offset-4 transition-colors hover:text-foreground">
info@matthias-meister-webdesign.de
</a>
</span>
<span className="text-muted-foreground">
Tel:{" "}
<a href="tel:037627984400" className="underline underline-offset-4 transition-colors hover:text-foreground">
03762 798 4400
</a>
</span>
<span className="text-muted-foreground">
<a href="/impressum" className="underline underline-offset-4 transition-colors hover:text-foreground">
Impressum
</a>
</span>
<span className="text-muted-foreground">
<a href="/datenschutz" className="underline underline-offset-4 transition-colors hover:text-foreground">
Datenschutz
</a>
</span>
</div>
</motion.div>
</motion.div>
<h2 className="max-w-2xl text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Bereit für eine Website, die Ihr Unternehmen klarer erklärt?
</h2>
<p className="mt-4 max-w-xl text-base leading-7 text-muted-foreground">
Eine kurze Nachricht reicht. Ich prüfe, welcher Weg sinnvoll ist,
und melde mich mit einer ehrlichen Einschätzung.
</p>
<Button asChild size="lg" className="mt-6 h-11 rounded-md px-5">
<a href="#kontakt">
Kostenloses Angebot anfordern
<ArrowRight className="shrink-0" aria-hidden />
</a>
</Button>
</div>
</footer>
<address className="not-italic">
<div className="space-y-3 text-sm text-muted-foreground">
<a
href="mailto:info@matthias-meister-webdesign.de"
className="flex items-center gap-2 transition-colors hover:text-foreground"
>
<Mail className="size-4" aria-hidden />
info@matthias-meister-webdesign.de
</a>
<a
href="tel:037627984400"
className="flex items-center gap-2 transition-colors hover:text-foreground"
>
<Phone className="size-4" aria-hidden />
03762 798 4400
</a>
</div>
</address>
</div>
<div className="mt-10 flex flex-col gap-4 border-t border-border/80 pt-6 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between">
<p>© 2026 Matthias Meister Webdesign Crimmitschau</p>
<div className="flex flex-wrap items-center gap-x-5 gap-y-2">
<a
href="/impressum"
className="underline underline-offset-4 transition-colors hover:text-foreground"
>
Impressum
</a>
<a
href="/datenschutz"
className="underline underline-offset-4 transition-colors hover:text-foreground"
>
Datenschutz
</a>
</div>
</div>
</div>
</section>
</footer>
);
};

View File

@@ -1,4 +1,4 @@
import { ArrowRight } from "lucide-react";
import { ArrowRight, Mail, MapPin, Phone } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -9,38 +9,103 @@ interface Hero235Props {
const Hero235 = ({ className }: Hero235Props) => {
return (
<section className={cn("px-4 sm:px-6 lg:px-8", className)}>
<div className="mx-auto max-w-5xl py-20 sm:py-24 lg:py-28">
<div className="mb-8 flex flex-col gap-3 border-b border-border/70 pb-5 sm:mb-10 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
Matthias Meister | Webdesign für KMU aus der Region
</p>
<p className="text-sm text-muted-foreground">
Rückmeldung innerhalb von 24 Stunden
</p>
</div>
<div className="flex max-w-4xl flex-col gap-7">
<h1 className="max-w-[13ch] text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl lg:text-6xl">
Websites für Unternehmen aus der Region - klar, schnell und
glaubwürdig.
</h1>
<p className="max-w-[65ch] text-base leading-7 text-muted-foreground sm:text-lg">
Ich arbeite direkt für Handwerk, Praxen und kleine Betriebe aus
der Region. Ohne Baukasten-Look, Agenturshow oder technischen
Umweg - sondern mit einer Website, die Ihr Angebot klar zeigt und
Anfragen leichter macht.
</p>
<div className="flex flex-wrap items-center gap-4 pt-1">
<Button asChild size="lg" className="h-11 rounded-full px-5">
<a href="#kontakt">
Projekt anfragen
<ArrowRight className="shrink-0" aria-hidden />
</a>
</Button>
<p className="text-sm text-muted-foreground">
Kurze Nachricht reicht - Sie erhalten direkt eine erste
Einschätzung und den passenden nächsten Schritt.
<section className={cn("px-4 pt-5 sm:px-6 lg:px-8", className)}>
<div className="mx-auto max-w-6xl">
<header className="flex flex-col gap-4 border-b border-border/80 pb-4 sm:flex-row sm:items-center sm:justify-between">
<a
href="/"
className="text-sm font-semibold tracking-tight text-foreground"
>
Matthias Meister Webdesign
</a>
<nav
aria-label="Direkte Kontaktwege"
className="flex flex-wrap items-center gap-x-5 gap-y-2 text-sm text-muted-foreground"
>
<a
href="tel:037627984400"
className="inline-flex items-center gap-1.5 transition-colors hover:text-foreground"
>
<Phone className="size-3.5" aria-hidden />
03762 798 4400
</a>
<a
href="mailto:info@matthias-meister-webdesign.de"
className="inline-flex items-center gap-1.5 transition-colors hover:text-foreground"
>
<Mail className="size-3.5" aria-hidden />
E-Mail
</a>
<span className="inline-flex items-center gap-1.5">
<MapPin className="size-3.5" aria-hidden />
Crimmitschau
</span>
</nav>
</header>
<div className="grid gap-10 py-16 sm:py-20 lg:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)] lg:items-end lg:py-24">
<div className="flex max-w-3xl flex-col gap-7">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Webdesign für regionale KMU
</p>
<h1 className="max-w-[12ch] text-5xl font-semibold leading-[0.95] tracking-tight text-balance text-foreground sm:text-6xl lg:text-7xl">
Websites, die vor Ort Vertrauen schaffen.
</h1>
<p className="max-w-[62ch] text-lg leading-8 text-muted-foreground">
Für Handwerk, Praxen und kleine Betriebe: klar erklärt, schnell
gebaut und so strukturiert, dass Besucher ohne Umwege verstehen,
warum sie gerade Sie anfragen sollten.
</p>
<div className="flex flex-col gap-3 pt-1 sm:flex-row sm:items-center">
<Button asChild size="lg" className="h-11 rounded-md px-5">
<a href="#kontakt">
Projekt anfragen
<ArrowRight className="shrink-0" aria-hidden />
</a>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="h-11 rounded-md px-5"
>
<a href="#preise">Pakete ansehen</a>
</Button>
</div>
<dl className="grid gap-4 border-t border-border/80 pt-6 sm:grid-cols-3">
{[
["24h", "Rückmeldung"],
["2 Wochen", "typischer Go-Live"],
["Sachsen", "Hosting & Betrieb"],
].map(([value, label]) => (
<div key={label}>
<dt className="text-2xl font-semibold tracking-tight text-foreground">
{value}
</dt>
<dd className="mt-1 text-sm text-muted-foreground">
{label}
</dd>
</div>
))}
</dl>
</div>
<div className="relative">
<figure className="overflow-hidden rounded-lg border border-border bg-card">
<img
src="/about.jpg"
alt="Arbeitsplatz von Matthias Meister beim Entwickeln einer Website"
className="h-[19rem] w-full object-cover sm:h-[24rem] lg:h-[31rem]"
/>
<figcaption className="grid gap-2 border-t border-border bg-card/95 p-5 sm:grid-cols-[1fr_auto] sm:items-center">
<span className="text-sm font-medium text-foreground">
Direkt mit dem Entwickler statt mit wechselnden Agenturrollen.
</span>
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
Persönlich geplant
</span>
</figcaption>
</figure>
</div>
</div>
</div>

View File

@@ -5,217 +5,211 @@ import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
interface PricingPlan {
name: string;
badge: string;
price: string;
description?: string;
period: string;
description: string;
features: string[];
buttonText: string;
isPopular?: boolean;
}
interface Pricing4Props {
title?: string;
description?: string;
plans?: PricingPlan[];
className?: string;
}
const developmentPlans: PricingPlan[] = [
{
name: "Basis",
price: "799 €",
period: "Einmalpreis",
description: "Für einen klaren Webauftritt mit den wichtigsten Inhalten.",
features: [
"Eine Seite mit fünf Sektionen",
"Kontaktformular",
"Impressum und Datenschutz",
"Mobilfreundlich und für Google vorbereitet",
"Cookiefreie Analytics ohne Banner",
],
},
{
name: "Profi",
price: "1.499 €",
period: "Einmalpreis",
description: "Für Betriebe, die mehrere Leistungen sauber erklären wollen.",
features: [
"Bis zu fünf Unterseiten",
"Google Maps Integration",
"SEO-Basis für lokale Auffindbarkeit",
"Optionaler Blog",
"Alles aus Basis inklusive",
],
isPopular: true,
},
{
name: "Maßarbeit",
price: "2.499 €",
period: "Einmalpreis",
description: "Für individuelle Anforderungen, CMS und spätere Erweiterungen.",
features: [
"Individuelles Design nach Ihren Anforderungen",
"CMS zur eigenen Inhaltspflege",
"Erweiterbare Struktur",
"Alles aus Profi inklusive",
],
},
];
const servicePlans: PricingPlan[] = [
{
name: "Hosting",
price: "19 €",
period: "pro Monat",
description: "Solide technische Basis für kleine Unternehmensseiten.",
features: [
"Hosting auf deutschen Servern in Sachsen",
"SSL, Domain und tägliche Backups",
"Monatlicher Einblick in Besucherzahlen",
],
},
{
name: "Wartung",
price: "39 €",
period: "pro Monat",
description: "Für Unternehmen, die Betrieb und Sicherheit abgeben möchten.",
features: [
"Alles aus Hosting inklusive",
"Regelmäßige Updates und Sicherheitschecks",
"1 Stunde Support pro Monat",
"Monitoring bei technischen Problemen",
],
isPopular: true,
},
{
name: "Full Service",
price: "69 €",
period: "pro Monat",
description: "Für laufende Änderungen ohne jedes Mal ein neues Projekt.",
features: [
"Alles aus Wartung inklusive",
"Kleinere Inhaltsänderungen bis 2 Stunden pro Monat",
"Häufigerer Einblick in Besucherzahlen",
],
},
];
const Pricing4 = ({
title = "Entwicklungspakete",
title = "Pakete mit klarer Kante.",
description =
"Alle Websites laufen auf deutschen Servern, sind DSGVO-konform und kommen ohne Cookie-Banner aus. Auf Wunsch erhalten Sie monatlich einen Einblick, wie viele Menschen Ihre Website besuchen haben — und woher sie kommen.",
plans = [
{
name: "BASIS",
badge: "799 €",
price: "799 €",
features: [
"Eine Seite, fünf Sektionen",
"Kontaktformular",
"Impressum & Datenschutz",
"Mobilfreundlich & für Google optimiert",
"DSGVO-konformes Kontaktformular",
"Cookiefreies Analytics — ohne Abmahnrisiko",
],
buttonText: "Kostenloses Angebot anfordern",
},
{
name: "PROFI",
badge: "1.499 € ⭐ Empfehlung",
price: "1.499 €",
features: [
"Bis zu 5 Unterseiten",
"Google Maps Integration",
"SEO-Basis (bessere Auffindbarkeit bei Google)",
"Optionaler Blog",
"DSGVO-konformes Kontaktformular",
"Cookiefreies Analytics — ohne Abmahnrisiko",
"Alles aus Basis inklusive",
],
buttonText: "Kostenloses Angebot anfordern",
isPopular: true,
},
{
name: "MASSARBEIT",
badge: "2.499 €",
price: "2.499 €",
features: [
"Individuelles Design nach Ihren Wünschen",
"CMS — Sie pflegen Inhalte selbst",
"DSGVO-konformes Kontaktformular",
"Cookiefreies Analytics — ohne Abmahnrisiko",
"Alles aus Profi inklusive",
],
buttonText: "Kostenloses Angebot anfordern",
},
],
"Die Preise sind bewusst nachvollziehbar gehalten. Im Gespräch klären wir, welches Paket passt und wo ein schlankerer Weg sinnvoller ist.",
className,
}: Pricing4Props) => {
const [isMonthly, setIsMonthly] = useState(false);
const [activeTab, setActiveTab] = useState<"development" | "service">(
"development",
);
const plans = activeTab === "development" ? developmentPlans : servicePlans;
return (
<section className={cn("py-32", className)}>
<div className="container mx-auto">
<div className="flex flex-col gap-6">
<h2 className="text-4xl font-semibold text-pretty lg:text-6xl">
{title}
</h2>
<div className="flex flex-col justify-between gap-10 md:flex-row">
<p className="max-w-3xl text-muted-foreground lg:text-xl">
<section
id="preise"
className={cn("px-4 py-20 sm:px-6 lg:px-8 lg:py-28", className)}
>
<div className="mx-auto max-w-6xl">
<div className="grid gap-8 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.4fr)] lg:items-end lg:gap-16">
<div className="max-w-xl">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary">
Preise
</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
{title}
</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">
{description}
</p>
<Tabs
value={isMonthly ? "monthly" : "yearly"}
onValueChange={(value: string) =>
setIsMonthly(value === "monthly")
}
className="w-fit shrink-0"
aria-label="Leistungsvariante"
</div>
<Tabs
value={activeTab}
onValueChange={(value: string) =>
setActiveTab(value as "development" | "service")
}
className="w-fit lg:justify-self-end"
aria-label="Paketart auswählen"
>
<TabsList className="grid h-11 w-full grid-cols-2 rounded-md border border-border bg-card p-1 sm:w-max">
<TabsTrigger
value="development"
className="h-full min-h-0 px-5 py-0 text-sm font-semibold data-active:bg-primary data-active:text-primary-foreground"
>
Entwicklung
</TabsTrigger>
<TabsTrigger
value="service"
className="h-full min-h-0 px-5 py-0 text-sm font-semibold data-active:bg-primary data-active:text-primary-foreground"
>
Hosting & Wartung
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="mt-10 grid gap-4 lg:grid-cols-3">
{plans.map((plan) => (
<article
key={plan.name}
className={cn(
"flex min-h-[32rem] flex-col rounded-lg border bg-card p-6",
plan.isPopular
? "border-primary shadow-[0_18px_60px_oklch(0.285_0.045_148/0.12)]"
: "border-border",
)}
>
<TabsList className="grid h-11 w-max grid-cols-2 gap-0 rounded-md p-1 text-lg">
<TabsTrigger
value="monthly"
className="h-full min-h-0 px-7 py-0 font-semibold text-muted-foreground data-active:text-foreground"
>
Entwicklung
</TabsTrigger>
<TabsTrigger
value="yearly"
className="h-full min-h-0 px-7 py-0 font-semibold text-muted-foreground data-active:text-foreground"
>
Hosting & Wartung
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="flex w-full flex-col items-stretch gap-6 md:flex-row">
{isMonthly ? (
plans.map((plan) => (
<div
key={plan.name}
className={`flex w-full flex-col rounded-xl border shadow-sm p-6 text-left ${
plan.isPopular ? "bg-muted" : ""
}`}
>
<Badge className="mb-8 block w-fit uppercase">
{plan.badge}
</Badge>
<h3 className="font-mono text-4xl lg:text-5xl">
{plan.price}
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-xl font-semibold tracking-tight">
{plan.name}
</h3>
<p className="text-muted-foreground">Einmalpreis</p>
<Separator className="my-6" />
<div className="flex h-full flex-col justify-between gap-20">
<ul className="space-y-4 text-muted-foreground md:leading-snug">
{plan.features.map((feature, featureIndex) => (
<li
key={featureIndex}
className="flex items-center gap-2"
>
<Check className="size-4 shrink-0" aria-hidden="true" />
<span>{feature}</span>
</li>
))}
</ul>
<Button className="w-full">{plan.buttonText}</Button>
</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{plan.description}
</p>
</div>
))
) : (
[
{
name: "BASIC HOSTING",
badge: "19 €/Monat",
price: "19 €",
features: [
"Hosting auf deutschen Servern in Sachsen",
"Grünes Schloss im Browser (SSL) — sicher & von Google bevorzugt",
"Tägliche Backups — Ihre Daten sind immer geschützt",
"Domain inklusive",
"Monatlicher Einblick in Ihre Besucherzahlen",
],
},
{
name: "WARTUNG",
badge: "39 €/Monat ⭐ Empfehlung",
price: "39 €",
features: [
"Alles aus Basic Hosting inklusive",
"Regelmäßige Updates & Sicherheitschecks",
"1 Stunde Support pro Monat",
"Monitoring — ich merke bevor Sie es tun, wenn etwas nicht stimmt",
"Wöchentlicher Einblick in Ihre Besucherzahlen",
],
isPopular: true,
},
{
name: "FULL SERVICE",
badge: "69 €/Monat",
price: "69 €",
features: [
"Alles aus Wartung inklusive",
"Kleinere Inhaltsänderungen (bis 2 Stunden/Monat)",
"Täglicher Einblick in Ihre Besucherzahlen",
],
},
].map((plan) => (
<div
key={plan.name}
className={`flex w-full flex-col rounded-xl border shadow-sm p-6 text-left ${
plan.isPopular ? "bg-muted" : ""
}`}
>
<Badge className="mb-8 block w-fit uppercase">
{plan.badge}
{plan.isPopular && (
<Badge variant="outline" className="rounded-md">
Empfohlen
</Badge>
<h3 className="font-mono text-4xl lg:text-5xl">
{plan.price}
</h3>
<p className="text-muted-foreground">Monatlicher Preis</p>
<Separator className="my-6" />
<div className="flex h-full flex-col justify-between gap-20">
<ul className="space-y-4 text-muted-foreground md:leading-snug">
{plan.features.map((feature, featureIndex) => (
<li
key={featureIndex}
className="flex items-center gap-2"
>
<Check className="size-4 shrink-0" aria-hidden="true" />
<span>{feature}</span>
</li>
))}
</ul>
<Button className="w-full">Kostenloses Angebot anfordern</Button>
</div>
</div>
))
)}
</div>
)}
</div>
<div className="mt-8">
<p className="text-4xl font-semibold tracking-tight">
{plan.price}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{plan.period}
</p>
</div>
<ul className="mt-8 flex-1 space-y-4 text-sm leading-6 text-muted-foreground">
{plan.features.map((feature) => (
<li key={feature} className="flex gap-3">
<Check
className="mt-0.5 size-4 shrink-0 text-primary"
aria-hidden="true"
/>
<span>{feature}</span>
</li>
))}
</ul>
<Button asChild className="mt-8 w-full rounded-md">
<a href="#kontakt">Kostenloses Angebot anfordern</a>
</Button>
</article>
))}
</div>
</div>
</section>

View File

@@ -5,59 +5,39 @@ interface Stats11Props {
}
const Stats11 = ({ className }: Stats11Props) => {
return (
<section className={cn("py-32", className)}>
<div className="container">
<div className="relative isolate overflow-hidden bg-linear-to-b from-primary/10 to-transparent md:border-x md:border-border">
<div className="absolute right-0 -left-px -z-20 h-full w-full bg-[linear-gradient(90deg,var(--muted-foreground)_1px,transparent_1px)] mask-[linear-gradient(transparent_25%,black_25%,black_75%,transparent_75%)] bg-size-[calc(100%/16)_100%] mask-size-[100%_16px] opacity-20 [-webkit-mask-image:linear-gradient(transparent_25%,black_25%,black_75%,transparent_75%)] [-webkit-mask-size:100%_16px]" />
const stats = [
["SEO-ready", "Leistungsseiten, Region und Kontakt sauber strukturiert."],
["< 1 Sek.", "Auf schnelle Ladezeiten und klare Technik ausgelegt."],
["ab 799 €", "Transparente Einstiegspreise ohne Paketnebel."],
["2 Wochen", "Typischer Zeitraum vom Startgespräch bis zur Vorschau."],
];
return (
<section className={cn("px-4 py-20 sm:px-6 lg:px-8 lg:py-28", className)}>
<div className="mx-auto max-w-6xl">
<div className="rounded-lg border border-border bg-primary p-6 text-primary-foreground sm:p-8 lg:grid lg:grid-cols-[minmax(0,0.85fr)_minmax(0,1.45fr)] lg:gap-12 lg:p-10">
<div>
<h2 className="mb-16 max-w-3xl text-3xl leading-10 font-semibold sm:mb-24 md:mx-10">
Für Google optimiert, schnell geladen und klar kalkulierbar.
<span className="font-medium text-primary/50">
{" "}
Genau die Zahlen, die bei einer Website wirklich zählen.
</span>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-primary-foreground/65">
Messbare Grundlagen
</p>
<h2 className="mt-4 max-w-xl text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
Gute Websites hlen sich ruhig an, weil die Basis stimmt.
</h2>
<div className="relative grid max-w-2xl gap-4 border-x border-border pb-32 sm:grid-cols-2 sm:gap-10 sm:pb-44 md:ml-10 md:border-0">
<div className="absolute inset-0 top-[-1100px] -left-[calc(1000px-22vw)] -z-10 size-[1500px] rounded-full border border-primary bg-background sm:top-[-480%] sm:left-[-185%] sm:size-[2000px] md:top-[-906%] md:left-[-294%] md:size-[3500px] lg:top-[-1186%] lg:left-[-380%] lg:size-[4500px] xl:top-[-1200%] xl:left-[-350%] 2xl:top-[-1196%] 2xl:left-[-345%]"></div>
<div className="flex flex-col gap-2">
<span className="flex gap-5 text-3xl font-semibold">
<span className="relative -left-px w-px bg-primary/50"></span>
SEO-ready
</span>
<p className="pl-5 font-medium text-muted-foreground/80">
Für Google optimiert
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:mt-0">
{stats.map(([value, label]) => (
<div
key={value}
className="rounded-md border border-primary-foreground/15 bg-primary-foreground/[0.06] p-5"
>
<p className="text-3xl font-semibold tracking-tight">
{value}
</p>
<p className="mt-3 text-sm leading-6 text-primary-foreground/72">
{label}
</p>
</div>
<div className="flex flex-col gap-2">
<span className="flex gap-5 text-3xl font-semibold">
<span className="relative -left-px w-px bg-primary/50"></span>
{"< 1 Sek."}
</span>
<p className="pl-5 font-medium text-muted-foreground/80">
Ladezeit
</p>
</div>
<div className="flex flex-col gap-2">
<span className="flex gap-5 text-3xl font-semibold">
<span className="relative -left-px w-px bg-primary/50"></span>
ab 799
</span>
<p className="pl-5 font-medium text-muted-foreground/80">
Transparenter Einmalpreis
</p>
</div>
<div className="flex flex-col gap-2">
<span className="flex gap-5 text-3xl font-semibold">
<span className="relative -left-px w-px bg-primary/50"></span>
2 Wochen
</span>
<p className="pl-5 font-medium text-muted-foreground/80">
Bis zum Go-Live
</p>
</div>
</div>
))}
</div>
</div>
</div>

View File

@@ -1,13 +1,5 @@
---
import { About19 } from "@/components/about19";
import { Contact21 } from "@/components/contact21";
import { Faq7 } from "@/components/faq7";
import { Feature284 } from "@/components/feature284";
import { Footer27 } from "@/components/footer27";
import { Hero235 } from "@/components/hero235";
import { Pricing4 } from "@/components/pricing4";
import { Stats11 } from "@/components/stats11";
import CTASection from "@/components/cta";
import { CanvaLanding } from "@/components/canva-landing";
import "@/styles/global.css";
---
@@ -25,16 +17,6 @@ import "@/styles/global.css";
defer></script>
</head>
<body>
<div class="mx-auto w-full max-w-7xl">
<Hero235 client:load />
<CTASection client:load />
<Feature284 client:load />
<Stats11 client:load />
<Pricing4 client:load />
<Faq7 client:load />
<About19 client:load />
<Contact21 client:load />
<Footer27 client:load />
</div>
<CanvaLanding />
</body>
</html>

View File

@@ -49,38 +49,38 @@
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--background: oklch(0.115 0.012 22);
--foreground: oklch(0.965 0.013 76);
--card: oklch(0.16 0.014 22);
--card-foreground: oklch(0.965 0.013 76);
--popover: oklch(0.16 0.014 22);
--popover-foreground: oklch(0.965 0.013 76);
--primary: oklch(0.61 0.235 27);
--primary-foreground: oklch(0.985 0.01 76);
--secondary: oklch(0.22 0.016 22);
--secondary-foreground: oklch(0.965 0.013 76);
--muted: oklch(0.19 0.014 22);
--muted-foreground: oklch(0.73 0.021 76);
--accent: oklch(0.61 0.235 27);
--accent-foreground: oklch(0.985 0.01 76);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--border: oklch(0.965 0.013 76 / 20%);
--input: oklch(0.965 0.013 76 / 20%);
--ring: oklch(0.61 0.235 27);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--sidebar: oklch(0.16 0.014 22);
--sidebar-foreground: oklch(0.965 0.013 76);
--sidebar-primary: oklch(0.61 0.235 27);
--sidebar-primary-foreground: oklch(0.985 0.01 76);
--sidebar-accent: oklch(0.22 0.016 22);
--sidebar-accent-foreground: oklch(0.965 0.013 76);
--sidebar-border: oklch(0.965 0.013 76 / 20%);
--sidebar-ring: oklch(0.61 0.235 27);
}
.dark {
@@ -123,8 +123,10 @@
}
body {
@apply bg-background text-foreground;
text-rendering: geometricPrecision;
}
html {
@apply font-sans;
scroll-behavior: smooth;
}
}
}

View File

@@ -0,0 +1,32 @@
import { readFile } from "node:fs/promises";
import test from "node:test";
import assert from "node:assert/strict";
const componentPath = new URL("../src/components/canva-landing.tsx", import.meta.url);
test("Canva landing component contains the core brief anchors", async () => {
const source = await readFile(componentPath, "utf8");
for (const phrase of ["Projektbrief", "01", "Website", "Kontakt", "für", "müssen", "Änderungen"]) {
assert.match(source, new RegExp(phrase));
}
});
test("Canva landing component uses real German umlauts in visible copy", async () => {
const source = await readFile(componentPath, "utf8");
for (const asciiFallback of [
"fuer",
"muessen",
"spaetere",
"Aenderungen",
"glaubwuerdig",
"naechste",
"Agenturlaerm",
"Erzaehlen",
"Saetze",
"Rueckmeldung",
]) {
assert.doesNotMatch(source, new RegExp(asciiFallback));
}
});