Replaces the hugo site with a custom Next.js 16 app under new-site/. Old hugo site is untouched until cutover. What's in here: - Next.js 16 (App Router, Turbopack) + React 19 + Tailwind v4 + MDX - Pages: home, services, about, work (+ 2 case studies), contact, blog (3 seed posts), thank-you, /sitemap.xml, /robots.txt - Stripe Payment Link checkout for retainer + 5/10/20-hour blocks, with website-redesign-launch sale (50% off) toggleable in src/lib/pricing.ts. Falls back to /contact when env vars unset. - Guarantees component on home + services (4 promises in writing) - HowIWork principles on about - Client logo strip with the 8 named clients (text wordmarks for now) - JSON-LD on every important page (Organization / Person / Service / Article / BreadcrumbList) for SEO + LLM discoverability - /llms.txt at site root - STRATEGY.md with positioning, 30-day content plan, Stripe setup walkthrough, SEO checklist, and a list of what Chris needs to wire before launch (Cal.com link, mailbox, photo, client sign-off, env vars) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
208 lines
6.9 KiB
TypeScript
208 lines
6.9 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { Button } from "@/components/button";
|
|
import { Container } from "@/components/container";
|
|
import { JsonLd } from "@/components/jsonld";
|
|
import { Eyebrow } from "@/components/section-heading";
|
|
import { articleSchema, breadcrumbSchema } from "@/lib/jsonld";
|
|
import { caseStudies } from "@/lib/case-studies";
|
|
import type { Metadata } from "next";
|
|
|
|
type Params = Promise<{ slug: string }>;
|
|
|
|
const bodies: Record<string, React.ReactNode> = {
|
|
"pritikin-foods": (
|
|
<>
|
|
<h2>The shape of the problem</h2>
|
|
<p>
|
|
Pritikin Foods sells direct-to-consumer prepared meals nationwide.
|
|
Every order from the e-commerce site has to be parsed, validated,
|
|
priced for shipping, and handed off to their 3PL fulfillment
|
|
partner — on time, with the right items, to the right
|
|
address, in the right shipping window.
|
|
</p>
|
|
<p>
|
|
That sounds simple. It isn’t. There’s SKU
|
|
normalization, address validation, transit-time math against meal
|
|
perishability, special-handling rules, batch hand-offs, and a
|
|
whole tail of reconciliation when something doesn’t match
|
|
what the warehouse actually shipped.
|
|
</p>
|
|
|
|
<h2>What I built and what I maintain</h2>
|
|
<ul>
|
|
<li>
|
|
The order-parsing pipeline that translates e-commerce orders
|
|
into the format the 3PL accepts.
|
|
</li>
|
|
<li>
|
|
The shipping and label flow: rate calculation, label
|
|
generation, and carrier-side error handling.
|
|
</li>
|
|
<li>
|
|
Reconciliation tooling so customer service can answer
|
|
“where is my order?” in seconds, not minutes.
|
|
</li>
|
|
<li>
|
|
The boring, on-call work of keeping it all running as carriers,
|
|
rates, and partners change.
|
|
</li>
|
|
</ul>
|
|
|
|
<h2>Why it’s been a multi-year relationship</h2>
|
|
<p>
|
|
This work isn’t glamorous, but it’s the difference
|
|
between Pritikin shipping orders today and not. Emil
|
|
Boschert’s words say it best:
|
|
</p>
|
|
<blockquote>
|
|
“They are vital for our e-commerce site and our 3PL.”
|
|
</blockquote>
|
|
<p>
|
|
That’s the bar I aim for on every long engagement —
|
|
becoming the kind of help a small team can stop worrying about.
|
|
</p>
|
|
</>
|
|
),
|
|
americold: (
|
|
<>
|
|
<h2>The shape of the problem</h2>
|
|
<p>
|
|
Americold is one of the largest temperature-controlled warehousing
|
|
and logistics networks in the world. At their scale, even
|
|
“small” pieces of operational tooling touch a lot of
|
|
people, a lot of pallets, and a lot of carriers.
|
|
</p>
|
|
|
|
<h2>What the engagement looked like</h2>
|
|
<p>
|
|
Senior-level PHP work on customer-facing logistics tooling.
|
|
Solution-oriented, customer-focused, delivered consistently
|
|
— in the words of Americold’s Timothy Dorcas:
|
|
</p>
|
|
<blockquote>
|
|
“They are solution oriented, customer focused, and
|
|
consistently deliver at a high level. Highly recommended.”
|
|
</blockquote>
|
|
|
|
<h2>Why this kind of work fits Five Devs</h2>
|
|
<p>
|
|
Enterprise logistics teams don’t need another agency. They
|
|
need a senior developer who can read the existing code, talk to
|
|
the operations people, ship the change, and write down what they
|
|
learned — without three layers of project management
|
|
between them and the work.
|
|
</p>
|
|
<p>
|
|
That’s the role I’ve played for Americold and others.
|
|
I show up, get up to speed quickly on the existing stack, and
|
|
keep the work moving.
|
|
</p>
|
|
</>
|
|
),
|
|
};
|
|
|
|
export async function generateStaticParams() {
|
|
return caseStudies.map((c) => ({ slug: c.slug }));
|
|
}
|
|
|
|
export const dynamicParams = false;
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: Params;
|
|
}): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const study = caseStudies.find((c) => c.slug === slug);
|
|
if (!study) return {};
|
|
return {
|
|
title: `${study.client} — Case Study`,
|
|
description: study.summary,
|
|
};
|
|
}
|
|
|
|
export default async function CaseStudyPage({
|
|
params,
|
|
}: {
|
|
params: Params;
|
|
}) {
|
|
const { slug } = await params;
|
|
const study = caseStudies.find((c) => c.slug === slug);
|
|
if (!study) notFound();
|
|
const body = bodies[slug];
|
|
|
|
return (
|
|
<article className="py-20 sm:py-28">
|
|
<JsonLd
|
|
data={articleSchema({
|
|
title: `${study.client} — Case Study`,
|
|
description: study.summary,
|
|
slug,
|
|
date: "2026-01-01",
|
|
basePath: "work",
|
|
})}
|
|
/>
|
|
<JsonLd
|
|
data={breadcrumbSchema([
|
|
{ name: "Home", path: "/" },
|
|
{ name: "Work", path: "/work" },
|
|
{ name: study.client, path: `/work/${slug}` },
|
|
])}
|
|
/>
|
|
<Container width="narrow">
|
|
<Link
|
|
href="/work"
|
|
className="text-sm text-muted hover:text-ink"
|
|
>
|
|
← All work
|
|
</Link>
|
|
<Eyebrow className="mt-8">Case study</Eyebrow>
|
|
<h1 className="mt-4 font-serif text-4xl font-semibold leading-tight tracking-tight text-ink sm:text-5xl">
|
|
{study.client}
|
|
</h1>
|
|
<p
|
|
className="mt-3 text-sm text-muted"
|
|
dangerouslySetInnerHTML={{ __html: study.industry }}
|
|
/>
|
|
|
|
<dl className="mt-10 grid grid-cols-2 gap-6 border-y border-line/70 py-6 text-sm sm:grid-cols-3">
|
|
<div>
|
|
<dt className="text-xs uppercase tracking-wider text-muted">
|
|
Role
|
|
</dt>
|
|
<dd className="mt-1 text-ink">{study.role ?? "Developer"}</dd>
|
|
</div>
|
|
{study.yearsRunning ? (
|
|
<div>
|
|
<dt className="text-xs uppercase tracking-wider text-muted">
|
|
Duration
|
|
</dt>
|
|
<dd className="mt-1 text-ink">{study.yearsRunning}</dd>
|
|
</div>
|
|
) : null}
|
|
<div>
|
|
<dt className="text-xs uppercase tracking-wider text-muted">
|
|
Outcome
|
|
</dt>
|
|
<dd className="mt-1 text-ink">{study.outcome}</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
<div className="prose prose-lg mt-12 max-w-none text-ink-soft prose-headings:font-serif prose-headings:text-ink prose-headings:font-semibold prose-h2:mt-12 prose-h2:text-2xl prose-strong:text-ink prose-blockquote:border-accent prose-blockquote:text-ink">
|
|
{body ?? <p>{study.summary}</p>}
|
|
</div>
|
|
|
|
<div className="mt-16 flex flex-wrap items-center gap-4">
|
|
<Button href="/contact" variant="primary">
|
|
Talk about your project
|
|
</Button>
|
|
<Button href="/work" variant="secondary">
|
|
More work
|
|
</Button>
|
|
</div>
|
|
</Container>
|
|
</article>
|
|
);
|
|
}
|