The homepage was too long and read as a list of sections. The technical ones (project fit, tools, how a change is secured) move to a page of their own, /freelance and /en/freelance, linked from the header, the footer and a pointer on the homepage. The rest is reordered into one story (who, what you need, examples, how working together goes, results, questions, contact) and each section opens with a sentence that leads over from the one before. routes.ts: a page may now have the same name in both languages. Before, /en/<german name> was always moved to the English name, which for such a page would have redirected the address onto itself. Design: list titles such as "Passt gut" are real headings instead of small grey labels. The automation examples, their description and the run are one box, so it is clear they belong together. Digits on light backgrounds are the blue of the panels instead of black. The square corner marks on the rounded service cards are gone. The years in the career line are readable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { DecodeText } from "@/src/components/bits/DecodeText";
|
|
import { Block } from "@/src/components/home/Block";
|
|
import { ClosingCta } from "@/src/components/home/ClosingCta";
|
|
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
|
|
import { Reveal } from "@/src/components/Reveal";
|
|
import { SwipeRow } from "@/src/components/SwipeRow";
|
|
import { contentFor } from "@/src/content";
|
|
import { DEFAULT_LOCALE, parseLocale } from "@/src/domain/locale";
|
|
import { languageAlternates, pathFor } from "@/src/domain/routes";
|
|
|
|
type Props = { readonly params: Promise<{ readonly locale: string }> };
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
const { freelance } = contentFor(locale);
|
|
return {
|
|
title: { absolute: freelance.metaTitle },
|
|
description: freelance.metaDescription,
|
|
alternates: {
|
|
canonical: pathFor("freelance", locale),
|
|
languages: languageAlternates("freelance"),
|
|
},
|
|
};
|
|
}
|
|
|
|
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";
|
|
|
|
/**
|
|
* The page for people who develop themselves or lead a team: which projects
|
|
* fit, the tools, and how a change is secured before it goes live.
|
|
*/
|
|
export default async function FreelancePage({ params }: Props) {
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
const content = contentFor(locale);
|
|
const { freelance, technology } = content;
|
|
|
|
return (
|
|
<div className="relative flex flex-col bg-white">
|
|
<section className="bg-white">
|
|
<div className="mx-auto w-full max-w-[1400px] px-5 pb-4 pt-14 md:px-12 md:pb-10 md:pt-28">
|
|
<p className="m-0 font-mono text-[11px] uppercase tracking-[0.25em] text-slate-500">
|
|
{freelance.label}
|
|
</p>
|
|
<h1 className="m-0 mt-8 max-w-6xl font-sans text-4xl font-bold leading-[0.95] tracking-tighter text-slate-900 md:text-7xl lg:text-[6.5rem]">
|
|
{freelance.title.lead}{" "}
|
|
<span className={MUTED}>{freelance.title.rest}</span>
|
|
</h1>
|
|
<p className={`${BODY} mt-10 max-w-3xl md:mt-14`}>
|
|
{freelance.intro}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<Block
|
|
index="01"
|
|
label={technology.label}
|
|
title={
|
|
<>
|
|
{technology.title.lead}{" "}
|
|
<span className={MUTED}>{technology.title.rest}</span>
|
|
</>
|
|
}
|
|
>
|
|
<SwipeRow className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-10">
|
|
{technology.fit.map((group, i) => (
|
|
<Reveal key={group.title} delay={0.1 * i}>
|
|
<div
|
|
className={`border-t pt-6 ${i === 2 ? "border-slate-300" : "border-slate-900"}`}
|
|
>
|
|
<h3
|
|
className={`${LIST_TITLE} ${i === 2 ? "text-slate-400" : "text-slate-900"}`}
|
|
>
|
|
{group.title}
|
|
</h3>
|
|
<ul className="m-0 mt-4 list-none p-0">
|
|
{group.items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className={`border-b border-slate-100 py-4 font-sans text-lg md:text-xl ${
|
|
i === 2 ? "text-slate-400" : "text-slate-900"
|
|
}`}
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</Reveal>
|
|
))}
|
|
</SwipeRow>
|
|
|
|
<Reveal>
|
|
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-24 md:p-10">
|
|
<h3 className={`${LIST_TITLE} text-slate-900`}>
|
|
{technology.techLabel}
|
|
</h3>
|
|
<div className="mt-6 grid grid-cols-1 gap-7 sm:grid-cols-2 md:mt-8 md:gap-10 lg:grid-cols-4">
|
|
{technology.tech.map((group) => (
|
|
<div key={group.area}>
|
|
<p className="m-0 font-sans text-xl font-bold tracking-tight text-slate-900 md:text-2xl">
|
|
{group.area}
|
|
</p>
|
|
<ul className="m-0 mt-4 flex list-none flex-wrap gap-2 p-0">
|
|
{group.items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className="rounded-full border border-slate-200 bg-white px-4 py-2 font-mono text-[11px] uppercase tracking-[0.12em] text-slate-700"
|
|
>
|
|
<DecodeText text={item} trigger="both" />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<Block
|
|
index="02"
|
|
label={freelance.pipelineLabel}
|
|
title={technology.pipelineTitle}
|
|
>
|
|
<Reveal>
|
|
<p className={`${BODY} mb-12 max-w-3xl`}>
|
|
{technology.pipelineIntro}
|
|
</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<PipelineRun />
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<ClosingCta contact={content.contact} />
|
|
</div>
|
|
);
|
|
}
|