feat: the homepage speaks to people who commission a result

Owner: the homepage is for people who need a website or process automation
and have no app of their own; companies that book by the hour belong on the
freelance page.

Homepage: two cases instead of three (automation, website). The price box
with its steps and figures is replaced by one box: you get an offer before
anything costs money. No rates or weekly prices there any more.

Freelance page: gets the developer case and "make an app reliable" with
their steps and prices, and the section on how working together goes.

Headlines the owner called unclear are rewritten: the automation section
("Your tools do the routine work. Nobody retypes anything.") and the
working section ("How working together goes. Direct and easy to follow.").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:50:34 +02:00
co-authored by Claude Opus 5.5
parent f0c25b802b
commit 1b6a96225e
7 changed files with 187 additions and 128 deletions
+70 -46
View File
@@ -7,21 +7,16 @@ import { DecodeText } from "@/src/components/bits/DecodeText";
import { PathScene } from "@/src/components/illustrations/PathScenes";
import { useContent } from "@/src/components/ContentProvider";
import { TopicButton } from "./TopicButton";
import type { ServicePath } from "@/src/content/types";
import type { ServicePath, ServicePathId } from "@/src/content/types";
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500";
const STEP_MS = 4800;
/**
* Booked by the hour by a team, not bought as a result: this case is not on the
* homepage. It lives on the freelance page.
*/
const TEAM_PATH = "developer";
/**
* PathStory: the four steps of one path, each with a drawing that explains it.
* The steps advance on their own until the visitor picks one.
*/
export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
path,
active,
}) => {
@@ -151,10 +146,17 @@ export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
* what they get, how it runs, what it costs, what to do next.
* All three panels stay in the DOM so the content is readable without JavaScript.
*/
export const PathPicker: React.FC = () => {
export const PathPicker: React.FC<{
/** The cases to show, in this order. */
ids: readonly ServicePathId[];
/** `offer`: ask for an offer, no figures. `estimate`: the way to a price, with figures. */
pricing: "offer" | "estimate";
}> = ({ ids, pricing }) => {
const { services } = useContent();
const { estimate } = services;
const paths = services.paths.filter((path) => path.id !== TEAM_PATH);
const paths = ids.flatMap((id) =>
services.paths.filter((path) => path.id === id),
);
const [activeId, setActiveId] = React.useState(paths[0]?.id ?? "");
return (
@@ -162,7 +164,7 @@ export const PathPicker: React.FC = () => {
<div
role="tablist"
aria-label={services.tablistLabel}
className="grid grid-cols-1 gap-2 md:grid-cols-3 md:gap-4"
className="grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-4"
>
{paths.map((path, index) => {
const selected = path.id === activeId;
@@ -249,48 +251,70 @@ export const PathPicker: React.FC = () => {
<PathStory path={path} active={selected} />
</div>
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10">
<p className={`${MONO} m-0`}>{estimate.label}</p>
<div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12">
<div className="md:col-span-7">
{pricing === "offer" ? (
<div className="mt-12 flex flex-col gap-8 rounded-3xl bg-slate-50 p-6 md:mt-16 md:flex-row md:items-center md:justify-between md:gap-12 md:p-10">
<div className="max-w-2xl">
<p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl">
{estimate.title}
{services.offer.title}
</p>
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500 md:text-lg">
{services.offer.text}
</p>
<ol className="m-0 mt-6 list-none p-0">
{estimate.steps.map((item, index) => (
<li
key={item.title}
className="flex gap-5 border-t border-slate-200 py-4"
>
<span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300">
{index + 1}
</span>
<span>
<span className="block font-sans text-lg font-bold tracking-tight text-slate-900">
{item.title}
</span>
<span className="mt-1 block font-sans text-base leading-relaxed text-slate-500">
{item.text}
</span>
</span>
</li>
))}
</ol>
</div>
<div className="flex flex-col md:col-span-5">
<p className={`${MONO} m-0`}>{estimate.orientationLabel}</p>
<p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl">
{path.orientation}
</p>
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500">
{path.orientationNote} {estimate.net}
</p>
<div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8">
<TopicButton topic={path.id}>{estimate.cta}</TopicButton>
<div className="shrink-0">
<TopicButton topic={path.id}>
{services.offer.cta}
</TopicButton>
</div>
</div>
) : (
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10">
<p className={`${MONO} m-0`}>{estimate.label}</p>
<div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12">
<div className="md:col-span-7">
<p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl">
{estimate.title}
</p>
<ol className="m-0 mt-6 list-none p-0">
{estimate.steps.map((item, index) => (
<li
key={item.title}
className="flex gap-5 border-t border-slate-200 py-4"
>
<span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300">
{index + 1}
</span>
<span>
<span className="block font-sans text-lg font-bold tracking-tight text-slate-900">
{item.title}
</span>
<span className="mt-1 block font-sans text-base leading-relaxed text-slate-500">
{item.text}
</span>
</span>
</li>
))}
</ol>
</div>
<div className="flex flex-col md:col-span-5">
<p className={`${MONO} m-0`}>
{estimate.orientationLabel}
</p>
<p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl">
{path.orientation}
</p>
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500">
{path.orientationNote} {estimate.net}
</p>
<div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8">
<TopicButton topic={path.id}>
{estimate.cta}
</TopicButton>
</div>
</div>
</div>
</div>
</div>
)}
</motion.div>
</div>
);
+15 -4
View File
@@ -74,10 +74,15 @@ export const de: Content = {
},
services: {
label: "Leistungen",
title: { lead: "Was brauchen Sie?", rest: "Drei typische Fälle." },
title: { lead: "Was brauchen Sie?", rest: "Meist eines von beiden." },
tablistLabel: "Was brauchen Sie?",
youGet: "Das bekommen Sie",
howItRuns: "So läuft es ab",
offer: {
title: "Sie bekommen ein Angebot, bevor etwas kostet.",
text: "Schreiben Sie mir kurz, was Sie brauchen. Wir sprechen etwa 20 Minuten, danach bekommen Sie ein schriftliches Angebot. Ich fange erst an, wenn Sie zugestimmt haben.",
cta: "Angebot anfragen",
},
estimate: {
label: "Was es kostet",
title: "So kommen Sie zu einem Preis",
@@ -277,7 +282,10 @@ export const de: Content = {
},
automation: {
label: "Prozessautomatisierung",
title: { lead: "Lassen Sie die Systeme", rest: "das Kopieren übernehmen." },
title: {
lead: "Ihre Programme erledigen die Routine.",
rest: "Niemand tippt mehr ab.",
},
intro:
"In den meisten Unternehmen tragen Menschen Informationen von einem Programm ins andere: hier lesen, dort eintippen, eine E-Mail dazu schreiben. Automatisierung verbindet diese Programme, sodass die Arbeit von selbst vom einen zum nächsten wandert. Drei typische Beispiele:",
examplesLabel: "Beispielabläufe",
@@ -489,7 +497,6 @@ export const de: Content = {
"Manchmal ist es gar keine neue Software, sondern weniger Handarbeit.",
presentations:
"Und manchmal ist es ein Termin, bei dem Sie überzeugen müssen.",
working: "Wenn klar ist, was entsteht, läuft es immer gleich ab.",
work: "Am Ende steht etwas, das läuft.",
faq: "Vorher bleiben meist ein paar Fragen.",
contact: "Und dann beginnt es mit einer E-Mail.",
@@ -506,6 +513,7 @@ export const de: Content = {
intro:
"Diese Seite ist für alle, die selbst entwickeln oder ein Team leiten: für welche Projekte ich passe, womit ich arbeite und wie ich Änderungen absichere, bevor sie live gehen.",
pipelineLabel: "Absicherung",
cases: { lead: "Wofür Teams mich buchen.", rest: "Zwei typische Fälle." },
booking: {
label: "Buchung",
title: { lead: "Gebucht wird monatsweise.", rest: "Nicht tageweise." },
@@ -646,7 +654,10 @@ export const de: Content = {
},
working: {
label: "Zusammenarbeit",
title: { lead: "So arbeite ich.", rest: "Nichts Ungewöhnliches." },
title: {
lead: "So läuft die Zusammenarbeit.",
rest: "Direkt und nachvollziehbar.",
},
principles: [
{
title: "Ein Ansprechpartner",
+15 -5
View File
@@ -73,10 +73,15 @@ export const en: Content = {
},
services: {
label: "Services",
title: { lead: "What do you need?", rest: "Three typical cases." },
title: { lead: "What do you need?", rest: "Usually one of the two." },
tablistLabel: "What do you need?",
youGet: "What you get",
howItRuns: "How it runs",
offer: {
title: "You get an offer before anything costs money.",
text: "Write me briefly what you need. We talk for about 20 minutes, then you get a written offer. I only start once you have agreed.",
cta: "Ask for an offer",
},
estimate: {
label: "What it costs",
title: "How you get to a price",
@@ -273,7 +278,10 @@ export const en: Content = {
},
automation: {
label: "Process automation",
title: { lead: "Let the systems", rest: "do the copying." },
title: {
lead: "Your tools do the routine work.",
rest: "Nobody retypes anything.",
},
intro:
"In most companies, people move information from one tool to another: read it here, type it in there, send an email about it. Automation connects those tools so that the work passes from one to the next on its own. Three typical examples:",
examplesLabel: "Example workflows",
@@ -477,8 +485,6 @@ export const en: Content = {
"Sometimes it is not new software at all, but less work by hand.",
presentations:
"And sometimes it is a meeting in which you have to convince.",
working:
"Once it is clear what will be built, it always runs the same way.",
work: "In the end there is something that runs.",
faq: "Before that, a few questions usually remain.",
contact: "And then it starts with an email.",
@@ -495,6 +501,7 @@ export const en: Content = {
intro:
"This page is for people who develop themselves or lead a team: which projects I am right for, what I work with, and how I secure changes before they go live.",
pipelineLabel: "Safeguards",
cases: { lead: "What teams book me for.", rest: "Two typical cases." },
booking: {
label: "Booking",
title: { lead: "Booked by the month.", rest: "Not by the day." },
@@ -631,7 +638,10 @@ export const en: Content = {
},
working: {
label: "Working with me",
title: { lead: "How I work.", rest: "Nothing unusual." },
title: {
lead: "How working together goes.",
rest: "Direct and easy to follow.",
},
principles: [
{
title: "One contact",
+4 -1
View File
@@ -125,7 +125,6 @@ export type ChapterId =
| "services"
| "automation"
| "presentations"
| "working"
| "work"
| "faq"
| "contact";
@@ -189,6 +188,8 @@ export type Content = {
readonly tablistLabel: string;
readonly youGet: string;
readonly howItRuns: string;
/** The homepage's way to a price: ask for an offer. No figures there. */
readonly offer: TitledText & { readonly cta: string };
/** How a visitor gets to a price: the homepage shows this instead of a price list. */
readonly estimate: {
readonly label: string;
@@ -251,6 +252,8 @@ export type Content = {
readonly title: SplitTitle;
readonly intro: string;
readonly pipelineLabel: string;
/** Headline over the cases teams book the owner for. */
readonly cases: SplitTitle;
/** How the owner is booked as a developer: by the month, not by the day. */
readonly booking: {
readonly label: string;