From 2cb15608dd8de31cc5304ee4962eb94f2f2ba02b Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 5 Oct 2026 12:32:33 +0200 Subject: [PATCH] feat: contact topics per audience, room above the last sentence, a hero line that says what a client gets Contact: the message starter offered every topic on both pages. The homepage now offers automation, a website, or something else; the freelance page a developer, a reliable app, or something else. Thread: where it crosses the page above the last sentence it ran too close to the text. It keeps more distance now, on phones too. Hero: the line under the hero listed years, working hours and two links, which says nothing to a client (owner). It now names four things a client gets: one contact who builds it himself, an offer before anything costs money, changes by email, servers in Germany. Co-Authored-By: Claude Opus 5.5 --- apps/web/app/[locale]/freelance/page.tsx | 5 +++- apps/web/app/[locale]/page.tsx | 9 +++++- apps/web/src/components/home/ClosingCta.tsx | 8 +++-- .../src/components/home/ContactComposer.tsx | 11 +++++-- .../web/src/components/home/HeroStatement.tsx | 29 +++++-------------- apps/web/src/components/story/StoryThread.tsx | 3 ++ apps/web/src/components/story/threadPath.ts | 6 ++-- apps/web/src/content/de.ts | 8 +++-- apps/web/src/content/en.ts | 8 +++-- apps/web/src/content/types.ts | 1 - 10 files changed, 51 insertions(+), 37 deletions(-) diff --git a/apps/web/app/[locale]/freelance/page.tsx b/apps/web/app/[locale]/freelance/page.tsx index c9e6737..d09dc0a 100644 --- a/apps/web/app/[locale]/freelance/page.tsx +++ b/apps/web/app/[locale]/freelance/page.tsx @@ -29,6 +29,9 @@ export async function generateMetadata({ params }: Props): Promise { }; } +/** What a team may ask for. Clients have their own on the homepage. */ +const TEAM_TOPICS = ["developer", "rescue", "other"] as const; + const MUTED = "text-slate-400"; const BODY = "m-0 font-sans text-lg leading-relaxed text-slate-500 md:text-xl"; const LIST_TITLE = "m-0 font-sans text-xl font-bold tracking-tight md:text-2xl"; @@ -255,7 +258,7 @@ export default async function FreelancePage({ params }: Props) { - + ); } diff --git a/apps/web/app/[locale]/page.tsx b/apps/web/app/[locale]/page.tsx index 2fb7eac..b540443 100644 --- a/apps/web/app/[locale]/page.tsx +++ b/apps/web/app/[locale]/page.tsx @@ -39,6 +39,9 @@ export async function generateMetadata({ params }: Props): Promise { }; } +/** What a client of the homepage may ask for. Teams have their own on the freelance page. */ +const HOME_TOPICS = ["automation", "website", "other"] as const; + const MUTED = "text-slate-400"; const BODY = "m-0 font-sans text-lg leading-relaxed text-slate-500 md:text-xl"; const LIST_TITLE = @@ -270,7 +273,11 @@ export default async function LandingPage({ params }: Props) { - + ); diff --git a/apps/web/src/components/home/ClosingCta.tsx b/apps/web/src/components/home/ClosingCta.tsx index cec2746..3866330 100644 --- a/apps/web/src/components/home/ClosingCta.tsx +++ b/apps/web/src/components/home/ClosingCta.tsx @@ -10,9 +10,11 @@ import { ContactComposer } from "./ContactComposer"; /** ClosingCta: the contact section. One address and a message starter, no form to submit. */ export const ClosingCta: React.FC<{ contact: Content["contact"]; + /** What visitors of this page may want: the topics of the message starter. */ + topics: readonly string[]; /** The last sentence of the story; the story thread ends in front of it. */ story?: string; -}> = ({ contact, story }) => ( +}> = ({ contact, topics, story }) => (
-
diff --git a/apps/web/src/components/story/StoryThread.tsx b/apps/web/src/components/story/StoryThread.tsx index f333cae..30521a9 100644 --- a/apps/web/src/components/story/StoryThread.tsx +++ b/apps/web/src/components/story/StoryThread.tsx @@ -29,6 +29,8 @@ const CONTENT_WIDTH = 1400; /** Offset of the second, parallel trace. */ const BUS_OFFSET = 5; const PAD_RADIUS = 4; +/** Room between the last crossing of the trace and the sentence it ends at. */ +const END_DROP = { wide: 120, narrow: 84 } as const; /** The bit at the head of the trace flips after this much travel, in px. */ const FLIP_EVERY = 46; /** The first chapter with a figure is the second of the page. */ @@ -127,6 +129,7 @@ export const StoryThread: React.FC = () => { start: [left, boxOf(startSection).y + 1], stops, end: endBox ? [endBox.x, endBox.y] : null, + endDrop: origin.width >= 768 ? END_DROP.wide : END_DROP.narrow, left, right, }); diff --git a/apps/web/src/components/story/threadPath.ts b/apps/web/src/components/story/threadPath.ts index 5c771b9..3c0335e 100644 --- a/apps/web/src/components/story/threadPath.ts +++ b/apps/web/src/components/story/threadPath.ts @@ -72,8 +72,6 @@ const POLYGONS: Readonly, readonly Point[]>> = }; const CORNER = 36; -/** How far above its end the trace crosses the page when it comes from the other side. */ -const END_DROP = 64; const round = (value: number): number => Math.round(value * 10) / 10; /** The figure as path commands. `direction` is 1 when the line arrives heading right, -1 heading left. */ @@ -111,6 +109,8 @@ export const threadPath = (thread: { readonly start: Point; readonly stops: readonly Stop[]; readonly end: Point | null; + /** How far above its end the trace crosses the page when it comes from the other side. */ + readonly endDrop: number; readonly left: number; readonly right: number; }): Thread => { @@ -144,7 +144,7 @@ export const threadPath = (thread: { // The end is on the other side: cross the page above it, then come // straight down to it, beside the text. One long diagonal instead would start far up the page. const inward = x < side ? -1 : 1; - const across = round(y - END_DROP); + const across = round(y - thread.endDrop); d += `L${side} ${across - CORNER}L${side + inward * CORNER} ${across}` + `L${round(x - inward * CORNER)} ${across}L${round(x)} ${across + CORNER}L${round(x)} ${round(y)}`; diff --git a/apps/web/src/content/de.ts b/apps/web/src/content/de.ts index 8c680d8..2f5e527 100644 --- a/apps/web/src/content/de.ts +++ b/apps/web/src/content/de.ts @@ -44,8 +44,12 @@ export const de: Content = { leadStrong: "Ich baue Web-Apps, Interfaces und Websites.", primaryCta: GET_IN_TOUCH, secondaryCta: "Was ich mache", - trust: ["15 Jahre im Web", "Remote, EU-Kernzeiten"], - builtAndRunning: "Gebaut und in Betrieb:", + trust: [ + "Ein Ansprechpartner, der es selbst baut", + "Ein Angebot, bevor etwas kostet", + "Änderungen per E-Mail, kein System zu lernen", + "Server in Deutschland", + ], }, hello: { label: "Hallo", diff --git a/apps/web/src/content/en.ts b/apps/web/src/content/en.ts index 139d590..bfcb592 100644 --- a/apps/web/src/content/en.ts +++ b/apps/web/src/content/en.ts @@ -43,8 +43,12 @@ export const en: Content = { leadStrong: "I build web apps, interfaces and websites.", primaryCta: GET_IN_TOUCH, secondaryCta: "What I do", - trust: ["15 years in web", "Remote, EU core hours"], - builtAndRunning: "Built and running:", + trust: [ + "One contact, who builds it himself", + "An offer before anything costs money", + "Changes by email, no system to learn", + "Servers in Germany", + ], }, hello: { label: "Hello", diff --git a/apps/web/src/content/types.ts b/apps/web/src/content/types.ts index f8af0e1..71d6a02 100644 --- a/apps/web/src/content/types.ts +++ b/apps/web/src/content/types.ts @@ -171,7 +171,6 @@ export type Content = { readonly primaryCta: string; readonly secondaryCta: string; readonly trust: readonly string[]; - readonly builtAndRunning: string; }; readonly hello: { readonly label: string;