feat(web): marketing landing page with Anthropic design system

Landing page at / matching claude.com/product/claude-code structure:
hero, surfaces, pricing, laptop-to-laptop, features, meets-you, faq, cta,
+ floating "Latest news" toaster. Motion-based scroll reveals.

Design system extracted from claude.com via playwriter reverse-engineering:
- Self-hosted Anthropic Sans/Serif/Mono fonts (6 woff2 files)
- --cm-* tokens in globals.css (clay #d97757, gray-050..900, fluid clamps)
- Serif display, Sans UI, Mono terminals & section markers
- Italic clay phrases for emphasis

Header rewritten for design consistency: claudemesh wordmark (mesh glyph +
serif), dark bg, nav (Docs · Pricing · Changelog · GitHub), "Start free" CTA.

Free-first messaging: hero subhead "Free and open-source. Forever.", primary
CTA "Start free", pricing defaults to Solo=Free.

Fixes:
- packages/api: comment out aiRouter (module removed in 1f094c4)
- packages/db/schema/mesh.ts: rename memberRelations → meshMemberRelations
  (missed in beeaa3b rename pass, caught via web build — ack'd by BotMou)
- credits/{api,server,index}: stub out @turbostarter/ai/credits/utils
- remove (marketing)/legal/[slug] route and common/mdx.tsx (cms-backed)
- sitemap: drop blog/legal enumeration (cms removed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 22:09:38 +01:00
parent e25115f1b0
commit 84e14ff410
28 changed files with 1358 additions and 2058 deletions

View File

@@ -1,89 +1,103 @@
import { getTranslation } from "@turbostarter/i18n/server";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@turbostarter/ui-web/accordion";
import { buttonVariants } from "@turbostarter/ui-web/button";
import { Icons } from "@turbostarter/ui-web/icons";
"use client";
import { useState } from "react";
import { Reveal } from "./_reveal";
import { pathsConfig } from "~/config/paths";
import { TurboLink } from "~/modules/common/turbo-link";
import {
Section,
SectionHeader,
SectionBadge,
SectionTitle,
SectionDescription,
} from "~/modules/marketing/layout/section";
const questions = [
{
question: "faq.question.whatDoesOurPlatformDo.question",
answer: "faq.question.whatDoesOurPlatformDo.answer",
},
{
question: "faq.question.howWillThisBenefitMyBusiness.question",
answer: "faq.question.howWillThisBenefitMyBusiness.answer",
},
{
question: "faq.question.isMyDataSafe.question",
answer: "faq.question.isMyDataSafe.answer",
},
{
question: "faq.question.whatKindOfIntegrationsAreAvailable.question",
answer: "faq.question.whatKindOfIntegrationsAreAvailable.answer",
},
{
question: "faq.question.howEasyIsItToOnboardMyTeam.question",
answer: "faq.question.howEasyIsItToOnboardMyTeam.answer",
},
{
question: "faq.question.whatTypesOfBusinessesCanUseThis.question",
answer: "faq.question.whatTypesOfBusinessesCanUseThis.answer",
},
{
question: "faq.question.canICustomizeThisToFitMyBusinessNeeds.question",
answer: "faq.question.canICustomizeThisToFitMyBusinessNeeds.answer",
},
] as const;
export const Faq = async () => {
const { t } = await getTranslation({ ns: "marketing" });
const ITEMS = [
{
q: "Is claudemesh free?",
a: "Yes — the broker, CLI, dashboard, and SDK are MIT-licensed and free forever. Solo developers and small teams can self-host at no cost. Paid tiers add hosted brokers, SSO, audit retention, and support.",
},
{
q: "How do I get started?",
a: "Install the broker with one curl command. Add one env var to your Claude Code config. Your session joins the mesh. `npx claudemesh init` does both in 60 seconds.",
},
{
q: "Does claudemesh send my code or prompts to the cloud?",
a: "No. The broker is a local WebSocket server. Messages stay on your network. The only data that leaves your machines is what your Claude Code already sends to Anthropic — we don't touch it.",
},
{
q: "Do I need to run a server?",
a: "Yes — one machine on your network runs the broker. That can be your laptop, a shared dev box, a Raspberry Pi, or a container in your cluster. It's one binary, SQLite-backed, ~15 MB.",
},
{
q: "Does it work across offices / continents?",
a: "Yes. Put the broker on a VPS, or expose it through Tailscale / WireGuard. Every Claude Code session that can reach the broker joins the mesh.",
},
{
q: "How does it route messages?",
a: "By peer name (alex, jordan), by repo, or by priority level (now / next / low). Messages are queued until the target peer is idle, then delivered. You can also set a peer to DND to block all but priority:now.",
},
{
q: "Which Claude Code versions work with claudemesh?",
a: "Claude Code 2.0 and above. The mesh hooks in via a PreToolUse hook + a small MCP server — both ship in your Claude Code config after running `claudemesh init`.",
},
];
export const FAQ = () => {
const [open, setOpen] = useState<number | null>(0);
return (
<Section id="faq" className="lg:flex-row lg:items-start">
<SectionHeader className="grow basis-0 lg:items-start">
<SectionBadge>{t("faq.label")}</SectionBadge>
<SectionTitle className="lg:text-left">{t("faq.title")}</SectionTitle>
<SectionDescription className="lg:text-left">
{t("faq.description")}
</SectionDescription>
<TurboLink
href={pathsConfig.marketing.contact}
className={buttonVariants({
variant: "outline",
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] px-6 py-24 md:px-12 md:py-32">
<div className="mx-auto max-w-4xl">
<Reveal>
<h2
className="mb-16 text-center text-[clamp(2rem,4.5vw,3.25rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
style={{ fontFamily: "var(--cm-font-serif)" }}
>
FAQ
</h2>
</Reveal>
<div className="divide-y divide-[var(--cm-border)] border-y border-[var(--cm-border)]">
{ITEMS.map((item, i) => {
const isOpen = open === i;
return (
<Reveal key={i} delay={i * 0.5}>
<button
onClick={() => setOpen(isOpen ? null : i)}
className="group flex w-full items-start justify-between gap-8 py-6 text-left transition-colors"
aria-expanded={isOpen}
>
<h3
className={
"text-xl font-medium leading-snug transition-colors " +
(isOpen
? "text-[var(--cm-fg)]"
: "text-[var(--cm-fg-secondary)] group-hover:text-[var(--cm-fg)]")
}
style={{ fontFamily: "var(--cm-font-serif)" }}
>
{item.q}
</h3>
<span
className={
"flex-shrink-0 text-2xl leading-none text-[var(--cm-clay)] transition-transform duration-300 " +
(isOpen ? "rotate-45" : "rotate-0")
}
>
+
</span>
</button>
<div
className={
"grid overflow-hidden transition-all duration-500 " +
(isOpen
? "grid-rows-[1fr] pb-6 opacity-100"
: "grid-rows-[0fr] opacity-0")
}
>
<div className="min-h-0">
<p
className="max-w-3xl text-base leading-[1.7] text-[var(--cm-fg-secondary)]"
style={{ fontFamily: "var(--cm-font-serif)" }}
>
{item.a}
</p>
</div>
</div>
</Reveal>
);
})}
>
{t("faq.cta")}
<Icons.ArrowRight className="ml-1 size-4" />
</TurboLink>
</SectionHeader>
<Accordion type="multiple" className="grow basis-0">
{questions.map((question) => (
<AccordionItem key={question.question} value={question.question}>
<AccordionTrigger className="text-base">
{t(question.question)}
</AccordionTrigger>
<AccordionContent className="text-base">
{t(question.answer)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</Section>
</div>
</div>
</section>
);
};