Files
fivedevs.com/src/app/services/page.tsx
Chris Smith a7c05104af flatten new-site/ to repo root and remove old hugo site
Moves the Next.js app's contents from new-site/ to the repository
root and deletes the previous Hugo site (assets/, content/, themes/,
hugo.toml, etc.). Also retires the AWS Amplify config and old
Netlify _redirects file — the new site deploys to Vercel.

Updates STRATEGY.md path references to drop the new-site/ prefix.
LICENSE preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:14:01 +02:00

374 lines
13 KiB
TypeScript

import { Button } from "@/components/button";
import { Container } from "@/components/container";
import { GuaranteesMini } from "@/components/guarantees";
import { JsonLd } from "@/components/jsonld";
import { Eyebrow, SectionHeading } from "@/components/section-heading";
import { serviceSchema } from "@/lib/jsonld";
import {
blocks,
formatPrice,
retainer,
saleActive,
saleLabel,
type CheckoutOption,
} from "@/lib/pricing";
import { SITE_URL } from "@/lib/site";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Services & Engagements",
description:
"Three ways to work with Five Devs: a one-shot project, a monthly retainer for ongoing help, or a block of senior engineering time you can spend as you go.",
};
function PriceLine({
option,
perSuffix,
invert,
size = "lg",
}: {
option: CheckoutOption;
perSuffix?: string;
invert?: boolean;
size?: "lg" | "md";
}) {
const muted = invert ? "text-cream/60" : "text-muted";
const strong = invert ? "text-cream" : "text-ink";
const showSale = option.salePrice !== undefined;
const priceClass =
size === "lg"
? "text-3xl font-semibold tracking-tight"
: "text-2xl font-semibold tracking-tight";
const strikeClass = size === "lg" ? "text-lg line-through" : "text-base line-through";
return (
<div className="space-y-1">
<div className="flex items-baseline gap-3">
{showSale ? (
<>
<span className={`${priceClass} ${strong}`}>
{formatPrice(option.salePrice!)}
{perSuffix ? (
<span className={`text-base font-normal ${muted}`}>
{perSuffix}
</span>
) : null}
</span>
<span className={`${strikeClass} ${muted}`}>
{formatPrice(option.regularPrice)}
</span>
</>
) : (
<span className={`${priceClass} ${strong}`}>
{formatPrice(option.regularPrice)}
{perSuffix ? (
<span className={`text-base font-normal ${muted}`}>
{perSuffix}
</span>
) : null}
</span>
)}
</div>
<p className={`text-sm ${muted}`}>
{showSale && option.saleEffectiveHourly !== undefined
? `${formatPrice(option.saleEffectiveHourly)}/hr effective`
: `${formatPrice(option.effectiveHourly)}/hr effective`}
</p>
</div>
);
}
function BlockRow({ option }: { option: CheckoutOption }) {
const wired = !!option.buyHref;
const inner = (
<div className="flex flex-col gap-2 py-5 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-baseline gap-4">
<p className="font-medium text-ink">{option.label}</p>
{wired ? (
<span
aria-hidden="true"
className="font-mono text-base text-muted opacity-0 transition-all group-hover:translate-x-1 group-hover:text-accent group-hover:opacity-100"
>
</span>
) : null}
</div>
<PriceLine option={option} size="md" />
</div>
);
if (!wired) return <li className="px-1">{inner}</li>;
return (
<li>
<a
href={option.buyHref}
className="group block -mx-3 rounded-lg px-3 transition-colors hover:bg-cream"
aria-label={`Buy ${option.label}`}
>
{inner}
</a>
</li>
);
}
function SaleBadge({ invert }: { invert?: boolean }) {
if (!saleActive) return null;
return (
<span
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium uppercase tracking-wider ${
invert
? "bg-cream/10 text-cream ring-1 ring-cream/30"
: "bg-accent/10 text-accent ring-1 ring-accent/30"
}`}
>
{saleLabel}
</span>
);
}
const servicesUrl = `${SITE_URL}/services`;
const serviceSchemas = [
serviceSchema({
name: "Monthly PHP development retainer",
description:
"Reserved senior PHP development hours each month. Ideal for teams without a senior PHP developer in-house.",
url: servicesUrl,
price: retainer.salePrice ?? retainer.regularPrice,
priceType: "Subscription",
}),
...blocks.map((b) =>
serviceSchema({
name: `${b.label} of senior PHP development time`,
description: `Block of ${b.label.replace("-hour block", " hours")} of senior PHP development time, usable within 6 months.`,
url: servicesUrl,
price: b.salePrice ?? b.regularPrice,
priceType: "OneTime",
}),
),
serviceSchema({
name: "Project sprint",
description:
"Fixed-scope, fixed-price PHP development engagement quoted after a paid discovery sprint.",
url: servicesUrl,
}),
];
export default function ServicesPage() {
const retainerWired = !!retainer.buyHref;
return (
<>
{serviceSchemas.map((schema, i) => (
<JsonLd key={i} data={schema} />
))}
<section className="py-20 sm:py-28">
<Container>
<div className="max-w-2xl">
<Eyebrow>Services</Eyebrow>
<h1 className="mt-6 font-serif text-4xl font-semibold leading-tight tracking-tight text-ink sm:text-6xl">
Three ways to work together.
</h1>
<p className="mt-7 text-lg leading-relaxed text-ink-soft">
Most freelance engagements fall into one of three shapes.
Pick the one that matches how your work actually arrives.
Block and retainer engagements are{" "}
<span className="font-medium text-ink">self-serve</span>{" "}
&mdash; check out below, or book a call first if
you&rsquo;d rather talk.
</p>
{saleActive ? (
<div className="mt-8 flex flex-wrap items-center gap-3">
<SaleBadge />
<p className="text-sm text-ink-soft">
Half off retainer and block engagements while the
redesign is fresh. First clients lock it in.
</p>
</div>
) : null}
</div>
</Container>
</section>
<section className="pb-24 sm:pb-32">
<Container>
<ul className="grid gap-6 lg:grid-cols-3">
{/* Project sprint — no checkout */}
<li className="flex flex-col rounded-2xl border border-line/80 bg-cream-soft p-8">
<h2 className="font-serif text-2xl font-semibold text-ink">
Project sprint
</h2>
<p className="mt-3 text-lg leading-snug text-ink-soft">
A defined piece of work, scoped, quoted, shipped.
</p>
<div className="mt-7 space-y-1">
<p className="text-3xl font-semibold tracking-tight text-ink">
Custom quote
</p>
<p className="text-sm text-muted">
Fixed-price after a paid discovery
</p>
</div>
<ul className="mt-7 space-y-3 text-sm leading-relaxed text-ink-soft">
{[
"Two weeks to two months",
"Daily progress in shared Slack or GitHub",
"Documented handoff at the end",
].map((b) => (
<li key={b} className="flex gap-3">
<span className="mt-2 inline-block h-1 w-1 flex-none rounded-full bg-accent" />
<span>{b}</span>
</li>
))}
</ul>
<p className="mt-7 text-sm italic text-muted">
Best when you know exactly what you need built or fixed.
</p>
<div className="mt-auto pt-7">
<Button href="/contact" variant="primary">
Discuss a project &rarr;
</Button>
</div>
</li>
{/* Monthly retainer — featured, with checkout */}
<li className="flex flex-col rounded-2xl border border-ink bg-ink p-8 text-cream">
<div className="flex flex-wrap items-center gap-3">
<h2 className="font-serif text-2xl font-semibold text-cream">
Monthly retainer
</h2>
<SaleBadge invert />
</div>
<p className="mt-3 text-lg leading-snug text-cream/80">
Quiet, predictable senior help on the long tail.
</p>
<div className="mt-7">
<PriceLine option={retainer} perSuffix="/mo" invert />
<p className="mt-2 text-sm text-cream/70">
{retainer.hoursLabel}
</p>
</div>
<ul className="mt-7 space-y-3 text-sm leading-relaxed text-cream/85">
{[
"Reserved hours each month",
"Priority on incidents and requests",
"Quarterly roadmap & tech-health check-ins",
"Pause or cancel with 30 days notice",
].map((b) => (
<li key={b} className="flex gap-3">
<span className="mt-2 inline-block h-1 w-1 flex-none rounded-full bg-cream/70" />
<span>{b}</span>
</li>
))}
</ul>
<p className="mt-7 text-sm italic text-cream/70">
Best for teams without a senior PHP developer in-house.
</p>
<div className="mt-auto space-y-3 pt-7">
<Button
href={retainer.buyHref ?? "/contact"}
variant="secondary"
className="w-full justify-center border-cream/30 text-cream hover:border-cream hover:bg-cream/10"
>
{retainerWired
? "Subscribe — $2,500/mo →"
: "Talk first →"}
</Button>
<Button
href="/contact"
variant="ghost"
className="w-full justify-center text-cream/70 hover:text-cream"
>
Or book a call first
</Button>
</div>
</li>
{/* Block of time — selectable rows */}
<li className="flex flex-col rounded-2xl border border-line/80 bg-cream-soft p-8">
<div className="flex flex-wrap items-center gap-3">
<h2 className="font-serif text-2xl font-semibold text-ink">
Block of time
</h2>
<SaleBadge />
</div>
<p className="mt-3 text-lg leading-snug text-ink-soft">
Buy a block, spend it as needs come up.
</p>
<ul className="mt-7 divide-y divide-line/80 border-y border-line/80">
{blocks.map((b) => (
<BlockRow key={b.id} option={b} />
))}
</ul>
<p className="mt-7 text-sm italic text-muted">
Best for work that comes in bursts &mdash; integrations,
migrations, audits.
</p>
<div className="mt-auto pt-7">
<Button
href="/contact"
variant="secondary"
className="w-full justify-center"
>
Or book a call first
</Button>
</div>
</li>
</ul>
<p className="mt-10 max-w-2xl text-sm text-muted">
All purchases trigger a confirmation email and a link to
book your kickoff call. You&rsquo;ll hear from me within
one business day.
</p>
<div className="mt-12">
<GuaranteesMini />
</div>
</Container>
</section>
<section className="border-t border-line/70 bg-cream-soft py-24 sm:py-32">
<Container width="narrow">
<SectionHeading
eyebrow="A note on rates"
title="Senior, transparent, and worth it."
subtitle="Rates anchor in the senior end of the market. Retainer and block rates are lower per hour because I can plan around them."
/>
<div className="mt-12 space-y-6 text-ink-soft">
<p>
Project sprints are quoted as a fixed price after a short
paid discovery so we both know what we&rsquo;re
committing to. Retainer and block prices are listed above
and you can check out directly &mdash; no email
ping-pong.
</p>
<p>
I do not subcontract or hand work off to junior
developers without telling you. If a project genuinely
needs a second pair of hands, I&rsquo;ll tell you who and
why before they touch your code.
</p>
</div>
<div className="mt-12 flex flex-wrap items-center gap-4">
<Button href="/contact" variant="primary">
Book a call
</Button>
<Button href="mailto:chris@fivedevs.com" variant="ghost">
chris@fivedevs.com
</Button>
</div>
</Container>
</section>
</>
);
}