diff --git a/index.html b/index.html index 9cb9f36..f6cdfeb 100644 --- a/index.html +++ b/index.html @@ -25,18 +25,18 @@ - WEBklar – Webagentur für KMU | Digitalisierung & Automatisierung + WEBklar – Websites, Shops und Apps für kleine Betriebe - + @@ -45,10 +45,10 @@ - + diff --git a/public/relaunch/portal/phone-2-projekte-lang-390.webp b/public/relaunch/portal/phone-2-projekte-lang-390.webp index 592f2f2..60a194e 100644 Binary files a/public/relaunch/portal/phone-2-projekte-lang-390.webp and b/public/relaunch/portal/phone-2-projekte-lang-390.webp differ diff --git a/public/relaunch/portal/phone-2-projekte-lang-780.webp b/public/relaunch/portal/phone-2-projekte-lang-780.webp index 740d2f6..2d854fc 100644 Binary files a/public/relaunch/portal/phone-2-projekte-lang-780.webp and b/public/relaunch/portal/phone-2-projekte-lang-780.webp differ diff --git a/scripts/relaunch-bilder.py b/scripts/relaunch-bilder.py index 9d8c9d7..918760f 100644 --- a/scripts/relaunch-bilder.py +++ b/scripts/relaunch-bilder.py @@ -34,6 +34,9 @@ BUDGET_HANDY = 60 * KB # Der lange Screenshot ist gut dreimal so hoch wie ein Handy-Bild (5428 statt 1688 px). Sein Budget # wächst mit der Höhe (je Bildschirmhöhe weiterhin unter 60 KB), sonst wäre der Text nur noch Matsch. BUDGET_LANG = {390: 80 * KB, 780: 150 * KB} +# Unter der letzten Inhaltszeile (Zeile 5179) ist der Rohexport nur noch schwarz. Abgeschnitten wird +# 40 px darunter, sonst endet die Verschiebung im Handy-Rahmen auf einem leeren Streifen. +LANG_HOEHE = 5219 # Dateiname (ohne .webp) -> (Art, Zielbreiten) BILDER = { @@ -43,7 +46,7 @@ BILDER = { "desktop-3b-chat-foto": ("desktop", (800, 1200, 1600)), "desktop-4-rechnungen": ("desktop", (800, 1200, 1600)), "desktop-7-news-pinnwand": ("desktop", (800, 1200, 1600)), - # Handy (Roh 780 x 1688, das lange Bild 780 x 5428) + # Handy (Roh 780 x 1688, das lange Bild 780 x 5428, beschnitten auf 5219) "phone-1-projekte": ("handy", (390, 780)), "phone-3-projektdetails": ("handy", (390, 780)), "phone-4b-chat-foto": ("handy", (390, 780)), @@ -81,6 +84,8 @@ def main() -> int: fehler += 1 continue roh = Image.open(quelle).convert("RGB") + if name.endswith("-lang"): + roh = roh.crop((0, 0, roh.width, LANG_HOEHE)) for breite in breiten: if name.endswith("-lang"): budget = BUDGET_LANG[breite] diff --git a/src/components/home/Kundenportal.tsx b/src/components/home/Kundenportal.tsx index 7f49254..2370d6a 100644 --- a/src/components/home/Kundenportal.tsx +++ b/src/components/home/Kundenportal.tsx @@ -32,7 +32,7 @@ const desktopBild = (name: string, alt: string): RahmenBildDaten => ({ alt, }); -/** Handy-Bild: Rohformat 780 x 1688 (das lange Bild 780 x 5428), ausgeliefert in 390 / 780 px. */ +/** Handy-Bild: Rohformat 780 x 1688 (das lange Bild 780 x 5219), ausgeliefert in 390 / 780 px. */ const handyBild = (name: string, alt: string, sizes: string, hoehe = 1688): RahmenBildDaten => ({ src: `${BILDER}/${name}-780.webp`, srcSet: srcSet(name, HANDY_BREITEN), @@ -291,7 +291,7 @@ const Buehne = () => { > {f.lang ? (
- +
) : ( @@ -326,7 +326,7 @@ const LangeKarte = ({ f }: { f: Funktion }) => {
- +

{f.titel}

@@ -358,7 +358,7 @@ const HandyReihe = () => ( const Schrittzeile = ({ desktop }: { desktop: boolean }) => desktop ? (
-
    +
      {SCHRITTE.map((s) => (
    1. {s.nr} @@ -369,7 +369,7 @@ const Schrittzeile = ({ desktop }: { desktop: boolean }) =>
) : (
- + {SCHRITTE.map((s) => (
{s.nr} diff --git a/src/components/home/home.css b/src/components/home/home.css index 007878d..a33ae96 100644 --- a/src/components/home/home.css +++ b/src/components/home/home.css @@ -7,7 +7,12 @@ align-items: start; } .wk-ueber-uns__link { - margin-top: 1.5rem; + margin-top: 1.25rem; +} +.wk-ueber-uns__link a { + display: inline-flex; + align-items: center; + min-height: 44px; } @media (max-width: 899.98px) { .wk-ueber-uns { diff --git a/src/components/home/kundenportal.css b/src/components/home/kundenportal.css index 32e2d96..275934a 100644 --- a/src/components/home/kundenportal.css +++ b/src/components/home/kundenportal.css @@ -210,8 +210,8 @@ transform: translate(-35cqw, -16.75cqw) scale(1.5); } -/* Der lange Screenshot: gleiche Breite wie der Schirm, 68,9 % seiner Höhe wandern aus dem Bild - ((5428 - 1688) / 5428). Die Verschiebung hängt am Scrollfortschritt (--kp-q), kein eigener Scrollbereich. */ +/* Der lange Screenshot: gleiche Breite wie der Schirm, 67,66 % seiner Höhe wandern aus dem Bild + ((5219 - 1688) / 5219). Die Verschiebung hängt am Scrollfortschritt (--kp-q), kein eigener Scrollbereich. */ .kp-lang { position: absolute; top: 0; @@ -233,7 +233,7 @@ transition: transform var(--dur-enter) var(--ease); } .kp-buehne .kp-lang { - transform: translateY(calc(var(--kp-q, 0) * -68.9%)); + transform: translateY(calc(var(--kp-q, 0) * -67.66%)); } /* die große Ebene nur im letzten Schritt auf die Grafikkarte legen */ .kp-buehne[data-app="ja"] .kp-lang { @@ -276,7 +276,7 @@ transform: translateY(0); } 100% { - transform: translateY(-68.9%); + transform: translateY(-67.66%); } } diff --git a/src/components/home/leistungen.css b/src/components/home/leistungen.css index ed7f7f4..f552c3d 100644 --- a/src/components/home/leistungen.css +++ b/src/components/home/leistungen.css @@ -68,6 +68,9 @@ .wk-leistungen__kopf { grid-column: 1; grid-row: 1 / span 2; + position: sticky; + top: calc(var(--header-h) + 3rem); + align-self: start; } .wk-leistungen__liste { grid-column: 2; diff --git a/src/components/site/SiteHeader.tsx b/src/components/site/SiteHeader.tsx index bca6c60..1991144 100644 --- a/src/components/site/SiteHeader.tsx +++ b/src/components/site/SiteHeader.tsx @@ -3,18 +3,9 @@ import { Link, useLocation } from "react-router-dom"; import { useTheme } from "next-themes"; import { Moon, Sun } from "lucide-react"; import Logo from "@/components/Logo"; -import { projekte } from "@/data/projekte"; +import { NAVIGATION } from "./navigation"; import "./site.css"; -/** Navigation der Kopfleiste (Spezifikation 6.1). „Projekte“ erscheint erst mit freigegebenen Referenzen. */ -export const NAVIGATION = [ - { label: "Kundenportal", to: "/#kundenportal" }, - { label: "Was wir bauen", to: "/#leistungen" }, - ...(projekte.length > 0 ? [{ label: "Projekte", to: "/#projekte" }] : []), - { label: "Preise", to: "/#preise" }, - { label: "Über uns", to: "/#ueber-uns" }, -]; - /** Hell/Dunkel umschalten. Vor dem ersten Rendern ist das Thema unbekannt, daher kein Symbolwechsel beim Laden. */ const ThemaSchalter = ({ mitText = false }: { mitText?: boolean }) => { const { resolvedTheme, setTheme } = useTheme(); diff --git a/src/components/site/navigation.ts b/src/components/site/navigation.ts new file mode 100644 index 0000000..2d6c206 --- /dev/null +++ b/src/components/site/navigation.ts @@ -0,0 +1,10 @@ +import { projekte } from "@/data/projekte"; + +/** Navigation der Kopfleiste (Spezifikation 6.1). „Projekte“ erscheint erst mit freigegebenen Referenzen. */ +export const NAVIGATION = [ + { label: "Kundenportal", to: "/#kundenportal" }, + { label: "Was wir bauen", to: "/#leistungen" }, + ...(projekte.length > 0 ? [{ label: "Projekte", to: "/#projekte" }] : []), + { label: "Preise", to: "/#preise" }, + { label: "Über uns", to: "/#ueber-uns" }, +]; diff --git a/src/components/site/site.css b/src/components/site/site.css index 2cc736e..8bf70c1 100644 --- a/src/components/site/site.css +++ b/src/components/site/site.css @@ -4,7 +4,7 @@ position: sticky; top: 0; z-index: 50; - background: color-mix(in srgb, var(--bg) 94%, transparent); + background: var(--bg); border-bottom: 1px solid transparent; transition: border-color var(--dur-state) var(--ease); } @@ -233,7 +233,8 @@ color: var(--fg); font-weight: 600; font-size: 1rem; - margin-bottom: 0.75rem; + min-height: 44px; + margin-bottom: 0.25rem; } .wk-footer__marke svg { border-radius: 6px; diff --git a/src/hooks/use-page-meta.ts b/src/hooks/use-page-meta.ts index afbac20..bbeef0f 100644 --- a/src/hooks/use-page-meta.ts +++ b/src/hooks/use-page-meta.ts @@ -1,9 +1,23 @@ import { useEffect } from "react"; const SITE_NAME = "WEBklar"; -const DEFAULT_TITLE = `${SITE_NAME} – Webagentur für KMU`; +const DEFAULT_TITLE = `${SITE_NAME} – Websites, Shops und Apps für kleine Betriebe`; const DEFAULT_DESCRIPTION = - "WEBklar digitalisiert KMU: Website, Prozesse und Systeme aus einer Hand. Kostenlose Potenzialanalyse – Antwort spätestens am nächsten Werktag."; + "WEBklar baut Websites, Online-Shops und Web-Apps. Im Kundenportal sehen Sie jeden Schritt, mit Live-Vorschau Ihrer Website."; +const SITE_URL = "https://webklar.com"; + +/** Kanonische Adresse je Route, damit Unterseiten nicht als Kopie der Startseite gelten. */ +function setCanonical(pathname: string): void { + const href = `${SITE_URL}${pathname === "/" ? "/" : pathname.replace(/\/$/, "")}`; + let el = document.querySelector('link[rel="canonical"]'); + if (!el) { + el = document.createElement("link"); + el.setAttribute("rel", "canonical"); + document.head.appendChild(el); + } + el.setAttribute("href", href); + setMetaTag("property", "og:url", href); +} function setMetaTag( attribute: "name" | "property", @@ -33,6 +47,7 @@ export function usePageMeta(title: string, description?: string): void { setMetaTag("property", "og:description", desc); setMetaTag("name", "twitter:title", fullTitle); setMetaTag("name", "twitter:description", desc); + setCanonical(window.location.pathname); return () => { document.title = DEFAULT_TITLE; diff --git a/src/pages/About.tsx b/src/pages/About.tsx index 408e30c..1b9e6c6 100644 --- a/src/pages/About.tsx +++ b/src/pages/About.tsx @@ -2,29 +2,34 @@ import { Link } from "react-router-dom"; import { usePageMeta } from "@/hooks/use-page-meta"; import "./unterseiten.css"; -/** „Der Unterschied“: nummerierte Textzeilen statt Karten (Phase 2 schreibt die ganze Seite neu). */ -const UNTERSCHIED = [ +/** Was man bei uns sicher bekommt (Spezifikation 6.4, nur belegte Aussagen; Team mit Namen und Fotos erst nach O4). */ +const SO_ARBEITEN_WIR_ZUSAMMEN = [ { nr: "01", - titel: "Alles aus einer Hand", - text: "Website, Automation und Schnittstellen aus einer Hand – ein Ansprechpartner statt fünf Agenturen.", + titel: "Direkt mit dem Team", + text: "Ihre Fragen gehen an die Leute, die an Ihrem Projekt arbeiten – im Chat des Kundenportals, per E-Mail oder am Telefon.", }, { nr: "02", - titel: "Systeme statt Inseln", - text: "CRM, Formulare und Website tauschen Daten – ohne manuelles Nachpflegen.", + titel: "Alles im Kundenportal", + text: "Fortschritt, Live-Vorschau Ihrer Website, Chat und Rechnungen finden Sie an einem Ort unter project.webklar.com.", }, { nr: "03", - titel: "Betreuung im Abo", - text: "Nach dem Go-live kümmern wir uns im Wartungs-Abo um Updates und kleine Änderungen.", + titel: "Preis vorher", + text: "Sie bekommen ein Angebot mit Preis, bevor wir anfangen. Weitere Arbeit gibt es nur mit Ihrer Freigabe.", + }, + { + nr: "04", + titel: "Server in Deutschland", + text: "Ihre Website und das Kundenportal laufen auf unserem Server im Rechenzentrum von Hetzner in Falkenstein, Sachsen.", }, ]; const About = () => { usePageMeta( "Über uns", - "WEBklar hilft KMU beim digitalen Wachstum: Systeme statt Chaos, ein Partner für Website, Prozesse und Automatisierung." + "WEBklar ist ein kleines Team aus Winnweiler in der Pfalz. Wir bauen Websites, Online-Shops und Web-Apps für kleine Betriebe.", ); return ( @@ -33,52 +38,30 @@ const About = () => {
Über WEBklar -

Digitalisierung, die im Alltag spürbar wird.

+

Wer hinter WEBklar steckt.

- WEBklar ist die Webagentur für Unternehmen, die wachsen wollen – ohne - dass Geschäftsführung und Team unter Tool-Chaos und manuellen Abläufen leiden. + Wir sind ein kleines Team aus Winnweiler in der Pfalz. Sie sprechen bei uns direkt mit den Leuten, die Ihre + Website bauen.

-

- Wir verbinden Website, Prozesse und Automatisierung zu einem System. Kein - isoliertes Redesign, kein weiteres Plugin – sondern eine Lösung, die Anfragen, Daten und Abläufe - zusammenführt. + Wir bauen Websites, Online-Shops, Web-Apps und Automatisierungen für kleine Betriebe. Was + wir gerade tun, sehen Sie jederzeit in Ihrem Kundenportal.

- -
-

Unser Anspruch: weniger Reibung.

-

Mehr planbares Wachstum.

-
- -

- Starten Sie mit einem kostenlosen Gespräch – wir zeigen Ihnen die nächsten Schritte. -

- -
- - Projekt besprechen - - - Zur Startseite - -
- {/* Der Unterschied */} -
+
- Der Unterschied -

- Warum Unternehmen zu uns wechseln. +

+ So arbeiten wir zusammen.

    - {UNTERSCHIED.map((punkt) => ( + {SO_ARBEITEN_WIR_ZUSAMMEN.map((punkt) => (
  1. {punkt.nr} diff --git a/src/pages/unterseiten.css b/src/pages/unterseiten.css index f354281..aef327a 100644 --- a/src/pages/unterseiten.css +++ b/src/pages/unterseiten.css @@ -400,7 +400,7 @@ } } -/* ── Über uns (Phase 2 schreibt die Seite neu) ─────────────────────────── */ +/* ── Über uns ─────────────────────────────────────────────────────────── */ .wk-ueber__text { max-width: var(--measure-text); @@ -416,31 +416,6 @@ color: var(--fg); } -.wk-ueber__aussage { - margin-top: clamp(40px, 6vw, 72px); - max-width: 24ch; - font-size: clamp(1.75rem, 1.35rem + 1.6vw, 2.75rem); - line-height: 1.12; - font-weight: 600; - letter-spacing: -0.015em; - text-wrap: balance; -} -.wk-ueber__aussage p:first-child { - color: var(--fg); -} -.wk-ueber__aussage p:last-child { - color: var(--fg-muted); -} - -.wk-ueber__schluss { - max-width: var(--measure-text); - margin-top: clamp(32px, 5vw, 56px); - font-size: var(--fs-lead); - line-height: 1.38; - font-weight: 600; - text-wrap: pretty; -} - /* Nummerierte Textzeilen statt Karten */ .wk-unterschied { margin-top: clamp(32px, 5vw, 56px); diff --git a/src/test/relaunch-kundenportal.test.tsx b/src/test/relaunch-kundenportal.test.tsx index 138690c..c93957a 100644 --- a/src/test/relaunch-kundenportal.test.tsx +++ b/src/test/relaunch-kundenportal.test.tsx @@ -130,7 +130,7 @@ describe("Kundenportal: Bilder (Spezifikation 9.5)", () => { expect(bild.getAttribute("height")).toBe("1688"); } expect(bilder[5].getAttribute("src")).toContain("phone-2-projekte-lang"); - expect(bilder[5].getAttribute("height")).toBe("5428"); + expect(bilder[5].getAttribute("height")).toBe("5219"); }); it("Desktop: zu Beginn hängen nur die ersten beiden Schritte ihre Bilder ein (spart Daten)", () => { @@ -180,7 +180,7 @@ describe("Kundenportal: Bilder (Spezifikation 9.5)", () => { describe("Kundenportal: Schrittzeile", () => { it("Desktop: eine nummerierte Liste mit vier Schritten", () => { zeigeDesktop(); - const liste = screen.getByRole("list", { name: /So arbeiten wir/ }); + const liste = screen.getByRole("list", { name: /Ablauf in vier Schritten/ }); const punkte = within(liste).getAllByRole("listitem"); expect(punkte).toHaveLength(4); punkte.forEach((punkt, i) => { @@ -190,7 +190,7 @@ describe("Kundenportal: Schrittzeile", () => { it("Handy: vier kleine Karten in einer Wisch-Reihe", () => { zeigeHandy(); - const reihe = screen.getByRole("list", { name: /So arbeiten wir/ }); + const reihe = screen.getByRole("list", { name: /Ablauf in vier Schritten/ }); const karten = within(reihe).getAllByRole("listitem"); expect(karten).toHaveLength(4); karten.forEach((karte, i) => { diff --git a/src/test/relaunch-layout.test.tsx b/src/test/relaunch-layout.test.tsx index ba88be3..25009a7 100644 --- a/src/test/relaunch-layout.test.tsx +++ b/src/test/relaunch-layout.test.tsx @@ -4,7 +4,7 @@ import { MemoryRouter } from "react-router-dom"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { AppRoutes } from "@/routes"; -import { NAVIGATION } from "@/components/site/SiteHeader"; +import { NAVIGATION } from "@/components/site/navigation"; import { projekte } from "@/data/projekte"; // Netzwerk gibt es in jsdom nicht: das Kontaktformular-Backend wird durch eine Attrappe ersetzt. diff --git a/src/test/relaunch-leistungen-preise.test.tsx b/src/test/relaunch-leistungen-preise.test.tsx index 5485a11..f830741 100644 --- a/src/test/relaunch-leistungen-preise.test.tsx +++ b/src/test/relaunch-leistungen-preise.test.tsx @@ -74,7 +74,7 @@ describe("Leistungen (Spezifikation 7.3)", () => { expect(h2).toHaveTextContent("Was wir für Sie bauen."); expect(abschnitt).toHaveAttribute("aria-labelledby", h2.id); // die Kopfleiste verlinkt genau diesen Anker - expect(lies("src/components/site/SiteHeader.tsx")).toContain("/#leistungen"); + expect(lies("src/components/site/navigation.ts")).toContain("/#leistungen"); }); it("zeigt die vier Einträge in der richtigen Reihenfolge, auf dem Handy als Wisch-Reihe", () => { @@ -163,7 +163,7 @@ describe("Preise (Spezifikation 7.5)", () => { const h2 = within(abschnitt).getByRole("heading", { level: 2 }); expect(h2).toHaveTextContent("Was es kostet. Vorher."); expect(abschnitt).toHaveAttribute("aria-labelledby", h2.id); - expect(lies("src/components/site/SiteHeader.tsx")).toContain("/#preise"); + expect(lies("src/components/site/navigation.ts")).toContain("/#preise"); expect(norm(abschnitt.querySelector(".wk-lead")?.textContent)).toBe( "Für Ihr Projekt bekommen Sie vorher ein Angebot mit Preis. Für den Betrieb danach gibt es zwei Abos. " + "Alle Preise sind Endpreise; als Kleinunternehmer (§ 19 UStG) berechnen wir keine Umsatzsteuer.", diff --git a/src/test/sofort-paket.test.tsx b/src/test/sofort-paket.test.tsx index 3c5854c..fd8fc88 100644 --- a/src/test/sofort-paket.test.tsx +++ b/src/test/sofort-paket.test.tsx @@ -281,13 +281,13 @@ describe("B5 Erfolgsversprechen entschärft", () => { ).toBeInTheDocument(); }); - it("Über uns: Karte »Betreuung im Abo« statt »Langfristige Partnerschaft«", () => { - zeige(); - expect(screen.getByRole("heading", { level: 3, name: "Betreuung im Abo" })).toBeInTheDocument(); - expect( - screen.getByText("Nach dem Go-live kümmern wir uns im Wartungs-Abo um Updates und kleine Änderungen."), - ).toBeInTheDocument(); - expect(screen.queryByText("Langfristige Partnerschaft")).toBeNull(); + it("Über uns: keine Erfolgsversprechen mehr, nur belegte Aussagen (Relaunch, Spezifikation 6.4)", () => { + const { container } = zeige(); + expect(screen.getByRole("heading", { level: 1, name: "Wer hinter WEBklar steckt." })).toBeInTheDocument(); + expect(screen.getByRole("heading", { level: 3, name: "Server in Deutschland" })).toBeInTheDocument(); + expect(container).not.toHaveTextContent( + /Langfristige Partnerschaft|planbares Wachstum|Warum Unternehmen zu uns wechseln|CRM|Tool-Chaos/, + ); }); it("Über uns: ohne Glüh-Rahmen und hochzählende Zahlen, ohne »Potenzialanalyse« (Spezifikation 8.5)", () => {