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
This commit is contained in:
2026-05-05 22:41:58 +02:00
7 changed files with 366 additions and 66 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ dist/
# dependencies
node_modules/
.worktrees/
# logs
npm-debug.log*

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

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

@@ -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>
<main class="w-full overflow-x-hidden">
<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 />
</main>
<CanvaLanding />
</body>
</html>

View File

@@ -49,38 +49,38 @@
}
:root {
--background: oklch(0.982 0.012 82);
--foreground: oklch(0.235 0.024 72);
--card: oklch(0.996 0.006 82);
--card-foreground: oklch(0.235 0.024 72);
--popover: oklch(0.996 0.006 82);
--popover-foreground: oklch(0.235 0.024 72);
--primary: oklch(0.285 0.045 148);
--primary-foreground: oklch(0.985 0.011 82);
--secondary: oklch(0.915 0.028 82);
--secondary-foreground: oklch(0.275 0.031 72);
--muted: oklch(0.94 0.018 82);
--muted-foreground: oklch(0.485 0.024 72);
--accent: oklch(0.705 0.079 141);
--accent-foreground: oklch(0.17 0.026 148);
--destructive: oklch(0.56 0.19 27);
--border: oklch(0.86 0.018 80);
--input: oklch(0.84 0.018 80);
--ring: oklch(0.53 0.071 142);
--chart-1: oklch(0.66 0.08 142);
--chart-2: oklch(0.55 0.055 72);
--chart-3: oklch(0.47 0.04 215);
--chart-4: oklch(0.72 0.055 93);
--chart-5: oklch(0.38 0.046 148);
--radius: 0.5rem;
--sidebar: oklch(0.965 0.012 82);
--sidebar-foreground: oklch(0.235 0.024 72);
--sidebar-primary: oklch(0.285 0.045 148);
--sidebar-primary-foreground: oklch(0.985 0.011 82);
--sidebar-accent: oklch(0.93 0.02 82);
--sidebar-accent-foreground: oklch(0.275 0.031 72);
--sidebar-border: oklch(0.86 0.018 80);
--sidebar-ring: oklch(0.53 0.071 142);
--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.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.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 {
@@ -122,20 +122,11 @@
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground antialiased;
@apply bg-background text-foreground;
text-rendering: geometricPrecision;
}
html {
@apply scroll-smooth font-sans;
@apply font-sans;
scroll-behavior: smooth;
}
}
body {
background-image:
linear-gradient(to bottom, oklch(0.988 0.01 82), var(--background) 46rem),
linear-gradient(90deg, oklch(0.86 0.018 80 / 0.28) 1px, transparent 1px);
background-size: auto, 6rem 6rem;
}
::selection {
background: oklch(0.78 0.075 141 / 0.35);
}

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