buildmymcpserver/apps/web/app/(marketing)/page.tsx
Marco Sadjadi c4f0db5719 fix(review): loop-2 findings — login fallback, proof-band confidence, contrast, content hedges
Code review: login no longer strands visitors with zero sign-in methods when
the providers fetch fails (falls back to SMS + error notice); skeleton while
providers load instead of a blank card.

Design re-audit: proof band flipped from apology to flex ('Don't take our
word for it / Every claim links to its proof') and promoted to the primary
heading tier; unverifiable '60 seconds'/'in minutes' speed claims dropped;
fg-subtle body text bumped to fg-muted (AA contrast); marketplace section
differentiated via border-y + elevated bg (adjacent hairlines removed);
preview frame traffic lights on-palette; header h-12 -> h-14 (hero svh calc
adjusted); emerald-400 -> --color-success token.

Content QA: rest-api article description drift between page and registry
resolved; ChatGPT plan-gating table and Atlassian Rovo SSE-cutoff claims now
carry dated hedges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
2026-07-08 23:13:53 +02:00

656 lines
26 KiB
TypeScript

import {
GitHubIcon,
NotionIcon,
PostgresIcon,
RestIcon,
SalesforceCloudIcon,
StripeIcon,
} from '@/components/brand-icons';
import { HeroStepRotator } from '@/components/hero-step-rotator';
import { HeroVideo } from '@/components/hero-video';
import { JsonLd } from '@/components/json-ld';
import { ParticleHero } from '@/components/particle-hero';
import { PulseLink } from '@/components/pulse';
import { ScrollCue } from '@/components/scroll-cue';
import { TIERS } from '@/lib/pricing';
import { FAQ, faqJsonLd } from '@/lib/seo';
import { Activity, ChevronDown, Container, ShieldCheck } from 'lucide-react';
import Link from 'next/link';
import type { ComponentType } from 'react';
interface ExampleEntry {
title: string;
desc: string;
/** Brand logo component rendered inside the coloured chip. */
Icon: ComponentType<{ size?: number; className?: string }>;
/** Official brand colour for the chip background. */
bg: string;
/** Foreground colour the icon paints in. */
fg: string;
}
const EXAMPLES: ExampleEntry[] = [
{
title: 'PostgreSQL',
desc: 'Read-only access to your tables with schema introspection.',
Icon: PostgresIcon,
bg: '#336791',
fg: '#ffffff',
},
{
title: 'Salesforce',
desc: 'Query opportunities, accounts and leads from Claude.',
Icon: SalesforceCloudIcon,
bg: '#00a1e0',
fg: '#ffffff',
},
{
title: 'Notion',
desc: 'Search pages, read content, append blocks.',
Icon: NotionIcon,
bg: '#ffffff',
fg: '#0a0a0b',
},
{
title: 'GitHub',
desc: 'List issues, search code, post comments. Scoped to one repo.',
Icon: GitHubIcon,
bg: '#181717',
fg: '#ffffff',
},
{
title: 'Stripe',
desc: 'Look up charges, customers, refunds (read-only by default).',
Icon: StripeIcon,
bg: '#635bff',
fg: '#ffffff',
},
{
title: 'Custom REST',
desc: 'Wrap any HTTP API behind one prompt-defined tool surface.',
Icon: RestIcon,
bg: '#6366f1',
fg: '#ffffff',
},
];
// Honest marketplace preview data: template names + tool counts only. These
// mirror the first-party starter templates; no invented fork counts, no
// "verified" theatre — the frame is labelled as a preview of the card format.
const PREVIEW_TEMPLATES: { name: string; author: string; tools: number }[] = [
{ name: 'notion-search', author: 'core', tools: 2 },
{ name: 'github-issues', author: 'core', tools: 3 },
{ name: 'stripe-readonly', author: 'core', tools: 4 },
{ name: 'postgres-readonly', author: 'core', tools: 3 },
];
const MARKETPLACE_POINTS: { t: string; d: string }[] = [
{
t: 'Fork and own',
d: 'Start from a server someone already shipped. Fork it, paste your own credentials, deploy. No prompt required.',
},
{
t: 'Secrets never travel',
d: "A template carries the spec and generated code, never the author's API keys. You add your own on fork.",
},
{
t: 'Open from day one',
d: 'Publish a server you built and anyone can fork it. The marketplace is young — early templates set the standard.',
},
];
// Proof-by-specificity band: three claims a visitor can verify with one
// click instead of taking our word for it. Substitute for social proof
// until there is social proof.
const PROOF_POINTS: {
t: string;
d: string;
href: string;
linkLabel: string;
Icon: ComponentType<{ size?: number; className?: string }>;
}[] = [
{
t: 'OAuth 2.1 authorization server',
d: 'PKCE, Dynamic Client Registration and Resource Indicators (RFC 8707) in front of every server.',
href: '/docs/oauth',
linkLabel: 'Read the auth docs',
Icon: ShieldCheck,
},
{
t: 'Live system status',
d: 'Uptime and incident history, public. If a build queue slows down, you see it before we tell you.',
href: '/status',
linkLabel: 'Check status now',
Icon: Activity,
},
{
t: 'Per-server container isolation',
d: 'Every generated server runs in its own Docker container. Secrets AES-256-GCM encrypted at rest.',
href: '/security',
linkLabel: 'Security architecture',
Icon: Container,
},
];
const PIPELINE_STEPS: { n: string; t: string; d: string }[] = [
{
n: '01',
t: 'Describe your tool',
d: 'A sentence is enough. List your secrets and which APIs to call.',
},
{
n: '02',
t: 'We generate, check, deploy',
d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.',
},
{
n: '03',
t: 'Install in your client',
d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.',
},
];
/** Mono shell-comment section kicker: `## how_it_works` */
function Kicker({ children }: { children: string }) {
return <p className="kicker">## {children}</p>;
}
export default function Landing() {
const teaserTiers = TIERS.filter((t) => t.name === 'Hobby' || t.name === 'Pro');
return (
<>
{/* Hero — left: copy + CTAs, right: cycling terminal tile. The WebGL
particle field sits behind at z-0 with pointer-events:none so the
CTAs stay interactive. */}
<section
className="relative flex items-center overflow-hidden border-b border-[--color-border]"
style={{ minHeight: 'calc(100svh - 3.5rem)' }}
>
<ParticleHero />
<div className="relative z-10 mx-auto grid w-full max-w-6xl gap-10 px-6 py-14 sm:py-20 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12">
<div className="min-w-0">
<h1 className="text-balance text-[36px] font-semibold leading-[1.04] tracking-[-0.03em] sm:text-[44px] md:text-[60px]">
Describe your tool.
<br />
We host the server.
<br />
<span className="text-[--color-fg-muted]">AI uses it.</span>
</h1>
<p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted] sm:text-[16px]">
From a prompt to a production MCP server OAuth 2.1, Streamable HTTP, ready for
Claude, Cursor and ChatGPT.
</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<PulseLink
href="/login"
className="btn-brand inline-flex h-11 items-center justify-center rounded-md px-5 text-[14px] font-medium"
>
Start building free
</PulseLink>
<PulseLink
href="#flow"
className="inline-flex h-11 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-5 text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
>
Watch a build
</PulseLink>
</div>
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-muted]">
<span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-[--color-success]" /> OAuth 2.1 + PKCE
</span>
<span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-[--color-success]" /> Streamable HTTP
</span>
<span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-[--color-success]" /> AES-256 secrets
</span>
<span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-[--color-success]" /> Per-server isolation
</span>
</div>
</div>
<div className="relative min-w-0">
<HeroStepRotator />
</div>
</div>
</section>
<ScrollCue targetId="flow" />
{/* Flow video — full-width edge-to-edge under the hero. Plays when
scrolled into view (see hero-video.tsx); preload=metadata keeps the
2.6 MB mp4 off the critical path on mobile. */}
<section
id="flow"
className="relative w-full overflow-hidden border-b border-[--color-border] bg-black"
>
<div className="relative aspect-video w-full">
<HeroVideo />
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
background:
'radial-gradient(ellipse at center, transparent 60%, rgba(10,10,11,0.55) 100%)',
}}
/>
</div>
</section>
{/* How it works — three pipeline nodes joined by a gradient connector.
Desktop: horizontal line behind the numbered nodes. Mobile: the grid
stacks and each card keeps its own node, joined by a left rail. */}
<section id="how" className="border-b border-[--color-border] py-14 sm:py-20">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-10 max-w-2xl">
<Kicker>how_it_works</Kicker>
<h2 className="mt-3 text-[28px] font-semibold tracking-tight sm:text-[32px]">
Three steps. No JSON to write, no Docker to manage.
</h2>
</div>
<div className="relative">
{/* Gradient connector — desktop only, sits behind the node row. */}
<div
aria-hidden
className="absolute left-0 right-0 top-[22px] hidden h-px md:block"
style={{ background: 'var(--gradient-brand)', opacity: 0.35 }}
/>
{/* Mobile left rail joining the stacked nodes. */}
<div
aria-hidden
className="absolute bottom-6 left-[22px] top-[22px] w-px md:hidden"
style={{ background: 'var(--gradient-brand)', opacity: 0.35 }}
/>
<div className="grid gap-8 md:grid-cols-3 md:gap-6">
{PIPELINE_STEPS.map((s) => (
<div key={s.n} className="relative flex gap-4 md:block">
<div
className="mono relative z-10 flex size-11 shrink-0 items-center justify-center rounded-full border border-[--color-border-strong] text-[12px] tracking-widest text-[--color-fg]"
style={{
background: 'var(--color-bg)',
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05)',
}}
>
{s.n}
</div>
<div className="min-w-0 md:mt-5">
<h3 className="text-[15px] font-semibold tracking-tight">{s.t}</h3>
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">
{s.d}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* Proof by specificity — three verifiable claims, each one click from
its evidence. Replaces the old pseudo-logo row: no invented marks,
just a plain-text compatibility line. */}
<section className="py-16 sm:py-24">
<div className="mx-auto max-w-6xl px-6">
<Kicker>verify_it_yourself</Kicker>
<h2 className="mt-3 max-w-2xl text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Don&apos;t take our word for it.
<br />
<span className="text-[--color-fg-muted]">Every claim links to its proof.</span>
</h2>
<div className="mt-10 grid gap-4 md:grid-cols-3">
{PROOF_POINTS.map((p) => {
const Icon = p.Icon;
return (
<Link
key={p.t}
href={p.href}
className="panel-raised group flex flex-col p-5 transition-colors hover:border-[--color-border-strong]"
>
<Icon size={20} className="text-[--color-accent]" />
<h3 className="mt-4 text-[15px] font-semibold tracking-tight">{p.t}</h3>
<p className="mt-2 flex-1 text-[13px] leading-relaxed text-[--color-fg-muted]">
{p.d}
</p>
<span className="mt-4 text-[13px] font-medium text-[--color-accent] transition-colors group-hover:text-[--color-fg]">
{p.linkLabel}
</span>
</Link>
);
})}
</div>
<p className="mt-10 text-center text-[13px] text-[--color-fg-muted]">
Works with Claude Desktop, Cursor, ChatGPT, VS Code Copilot and Continue.dev anything
that speaks MCP.
</p>
</div>
</section>
{/* Use cases — brand-coloured integration grid. No bottom hairline:
the marketplace section below brings its own border-y. */}
<section className="py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-12 grid gap-6 md:grid-cols-[1fr_auto] md:items-end md:gap-12">
<div>
<Kicker>use_cases</Kicker>
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Wrap any HTTP API.
<br />
<span className="text-[--color-fg-muted]">From one prompt.</span>
</h2>
</div>
<p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]">
Each shipped from a single prompt.
</p>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{EXAMPLES.map((e) => {
const Icon = e.Icon;
return (
<div
key={e.title}
className="panel-raised group flex items-start gap-4 p-5 transition-colors hover:border-[--color-border-strong]"
>
<div
aria-hidden
className="flex size-12 shrink-0 items-center justify-center rounded-lg"
style={{
backgroundColor: e.bg,
color: e.fg,
}}
>
<Icon size={24} />
</div>
<div className="min-w-0">
<h3 className="text-[15px] font-semibold tracking-tight">{e.title}</h3>
<p className="mt-1.5 text-[13px] leading-relaxed text-[--color-fg-muted]">
{e.desc}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
{/* Marketplace — split layout: selling points left, honest preview
frame right. Elevated background (no hairline) so the page reads
in blocks instead of one continuously ruled ledger. */}
<section className="border-y border-[--color-border] bg-[--color-bg-elevated] py-20 sm:py-28">
<div className="mx-auto grid max-w-6xl gap-14 px-6 md:grid-cols-[5fr_6fr] md:items-center md:gap-16">
<div>
<Kicker>marketplace</Kicker>
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Skip the prompt.
<br />
<span className="text-[--color-fg-muted]">Fork what works.</span>
</h2>
<p className="mt-5 max-w-md text-[14px] leading-relaxed text-[--color-fg-muted]">
The marketplace is a library of working MCP servers. Fork one, paste your credentials,
deploy. Or publish yours and let others build on it.
</p>
<ul className="mt-8 space-y-5">
{MARKETPLACE_POINTS.map((p) => (
<li key={p.t} className="flex items-start gap-3">
<span
aria-hidden
className="mt-[7px] size-1.5 shrink-0 rounded-full bg-[--color-accent]"
/>
<div>
<h3 className="text-[14px] font-semibold tracking-tight">{p.t}</h3>
<p className="mt-1 text-[13px] leading-relaxed text-[--color-fg-muted]">
{p.d}
</p>
</div>
</li>
))}
</ul>
<PulseLink
href="/templates"
className="btn-brand mt-8 inline-flex h-11 items-center gap-2 rounded-md px-5 text-[14px] font-medium"
>
Browse the marketplace
</PulseLink>
</div>
<MarketplacePreview />
</div>
</section>
{/* Pricing teaser — Hobby + Pro only; the full matrix lives on
/pricing. Data is shared via lib/pricing.ts so it can't drift. */}
<section id="pricing" className="border-b border-[--color-border] py-20 sm:py-28">
<div className="mx-auto max-w-4xl px-6">
<div className="mb-12 text-center">
<Kicker>pricing</Kicker>
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Pay for tool calls.
<br />
<span className="text-[--color-fg-muted]">Not for boilerplate.</span>
</h2>
</div>
<div className="grid gap-5 sm:grid-cols-2">
{teaserTiers.map((t) => {
const featured = Boolean(t.highlight);
return (
<div
key={t.name}
className={`relative flex flex-col gap-6 rounded-xl border p-6 ${
featured
? 'border-[--color-accent] bg-[--color-bg-elevated]'
: 'border-[--color-border] bg-[--color-bg-elevated]'
}`}
style={
featured
? {
boxShadow:
'0 0 0 4px rgba(99, 102, 241, 0.12), 0 24px 50px rgba(0, 0, 0, 0.35)',
}
: { boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04)' }
}
>
{featured && (
<span
className="absolute -top-3 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full border border-[--color-accent] px-3 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-[--color-accent]"
style={{ backgroundColor: 'var(--color-bg)' }}
>
Recommended
</span>
)}
<div>
<div className="text-[11px] font-medium uppercase tracking-[0.18em] text-[--color-fg-muted]">
{t.name}
</div>
<div className="mt-3 flex items-baseline gap-2">
<span className="text-[40px] font-semibold leading-none tracking-tight text-[--color-fg]">
{t.price}
</span>
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
</div>
<p className="mt-3 text-[13px] leading-relaxed text-[--color-fg-muted]">
{t.description}
</p>
</div>
<ul className="flex flex-1 flex-col gap-2.5 border-t border-[--color-border] pt-5 text-[13px] text-[--color-fg-muted]">
{t.features.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<span
aria-hidden
className={`mt-[7px] size-1 shrink-0 rounded-full ${
featured ? 'bg-[--color-accent]' : 'bg-[--color-fg-subtle]'
}`}
/>
<span>{f}</span>
</li>
))}
</ul>
<Link
href={t.href}
className={`inline-flex h-10 items-center justify-center rounded-md px-4 text-[13px] font-medium transition-colors ${
featured
? 'btn-brand'
: 'border border-[--color-border] bg-[--color-bg-subtle] text-[--color-fg] hover:border-[--color-border-strong]'
}`}
>
{t.cta}
</Link>
</div>
);
})}
</div>
<div className="mt-8 text-center">
<Link
href="/pricing"
className="text-[14px] font-medium text-[--color-accent] transition-colors hover:text-[--color-fg]"
>
See all plans Team, Enterprise
</Link>
</div>
</div>
</section>
{/* FAQ — collapsible accordion using native <details>. Crawlers and
screen readers see the full Q+A in the HTML. */}
<section className="border-b border-[--color-border] py-14 sm:py-20">
<JsonLd data={faqJsonLd()} />
<div className="mx-auto max-w-3xl px-6">
<Kicker>faq</Kicker>
<h2 className="mt-3 text-[28px] font-semibold tracking-tight">
Questions, answered straight.
</h2>
<div className="mt-8 border-t border-[--color-border]">
{FAQ.map((f) => (
<details
key={f.q}
className="group border-b border-[--color-border] [&_summary::-webkit-details-marker]:hidden"
>
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 py-4 text-[14.5px] font-semibold tracking-tight text-[--color-fg] transition-colors hover:text-[--color-accent]">
<span>{f.q}</span>
<ChevronDown
size={16}
className="shrink-0 text-[--color-fg-subtle] transition-transform duration-200 group-open:rotate-180 group-open:text-[--color-accent]"
/>
</summary>
<p className="pb-5 pr-8 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
{f.a}
</p>
</details>
))}
</div>
</div>
</section>
{/* Final CTA — the page must not end on FAQ. Gradient top border keeps
the brand mark scarce but present at the exit point. */}
<section className="relative py-20 sm:py-28">
<div
aria-hidden
className="absolute inset-x-0 top-0 h-px"
style={{ background: 'var(--gradient-brand)', opacity: 0.6 }}
/>
<div className="mx-auto max-w-3xl px-6 text-center">
<h2 className="text-balance text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[44px]">
Your first server is <span className="text-brand-gradient">one prompt away</span>.
</h2>
<p className="mx-auto mt-4 max-w-md text-[14.5px] leading-relaxed text-[--color-fg-muted]">
Free tier, full source export, no lock-in. If it doesn't work for you, take the
TypeScript and leave.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<PulseLink
href="/login"
className="btn-brand inline-flex h-11 items-center justify-center rounded-md px-6 text-[14px] font-medium"
>
Start building free →
</PulseLink>
<PulseLink
href="/docs"
className="inline-flex h-11 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-6 text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
>
Read the docs
</PulseLink>
</div>
</div>
</section>
</>
);
}
/**
* Honest marketplace preview.
*
* Static, server-rendered browser frame showing what a template card looks
* like. Card data mirrors the first-party starter templates — names, author
* and tool counts only. No fork counts, no "verified" badges: the frame is
* explicitly labelled a preview, not live marketplace traffic.
*/
function MarketplacePreview() {
return (
<div
className="overflow-hidden rounded-xl border border-[--color-border-strong] bg-[--color-bg-elevated]"
style={{
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04), 0 24px 60px rgba(0, 0, 0, 0.45)',
}}
>
{/* Browser chrome */}
<div className="flex items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3">
<div className="flex gap-1.5">
<span className="size-2.5 rounded-full bg-[#ff5f57]/80" />
<span className="size-2.5 rounded-full bg-[#febc2e]/80" />
<span className="size-2.5 rounded-full bg-[#28c840]/80" />
</div>
<div className="mono flex-1 truncate rounded-md border border-[--color-border] bg-[--color-bg] px-3 py-1 text-[11px] text-[--color-fg-subtle]">
buildmymcpserver.com/templates
</div>
</div>
{/* Toolbar inside the page chrome */}
<div className="flex items-center justify-between gap-4 border-b border-[--color-border] px-5 py-3">
<span className="text-[13px] font-semibold tracking-tight text-[--color-fg]">
Templates
</span>
<span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
preview
</span>
</div>
{/* Template cards grid */}
<div className="grid gap-3 p-4 sm:grid-cols-2">
{PREVIEW_TEMPLATES.map((t) => (
<div
key={t.name}
className="rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-4 transition-colors hover:border-[--color-accent]/40"
>
<div className="min-w-0">
<div className="mono truncate text-[13px] font-semibold tracking-tight text-[--color-fg]">
{t.name}
</div>
<div className="mt-1 flex items-center gap-1.5">
<span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
{t.author}
</span>
<span className="inline-flex items-center rounded-full border border-[--color-border-strong] px-1.5 py-px text-[9.5px] font-medium uppercase tracking-wider text-[--color-fg-subtle]">
template
</span>
</div>
</div>
<div className="mt-4 text-[11px] text-[--color-fg-subtle]">
<span className="mono">{t.tools} tools</span>
</div>
</div>
))}
</div>
</div>
);
}