feat(landing): honest high-end redesign — kill fake social proof, brand gradient identity, final CTA, mobile fixes

- removed fabricated fork counts/'verified' badges, 'our customers ship
  today' copy, pseudo client logo marks and stale v0.1 badge (zero-user
  product must not fake traction)
- new proof-by-specificity band: verifiable links to /docs/oauth, /status,
  /security instead of testimonial cosplay
- identity: indigo-to-cyan brand gradient, indigo-tinted elevated surfaces,
  mono section kickers, terminal-chrome hero rotator, gradient h-11 CTA
- how-it-works as connected pipeline; new final CTA band (page no longer
  ends on FAQ); footer upgraded to 4-column product/resources/legal
- lib/pricing.ts single tier source for pricing page + landing teaser;
  annual-billing FAQ claim softened to truth (no annual checkout exists)
- hero video preload=metadata + IntersectionObserver play (2.6MB off the
  critical path); 44px touch targets; mobile menu: Guides link, CTA,
  scroll-lock, escape/outside-tap close

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
This commit is contained in:
Marco Sadjadi 2026-07-08 23:00:25 +02:00
parent cba45402ce
commit 17056d0b30
8 changed files with 627 additions and 432 deletions

View File

@ -39,42 +39,83 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
</div> </div>
</header> </header>
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
<footer className="border-t border-[--color-border] py-8"> <footer className="border-t border-[--color-border] py-12">
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-6 text-[12px] text-[--color-fg-subtle] md:flex-row md:items-center md:justify-between"> <div className="mx-auto max-w-6xl px-6">
<div className="grid gap-10 sm:grid-cols-2 md:grid-cols-4">
{/* Brand column — positioning line + live status. */}
<div className="sm:col-span-2 md:col-span-1">
<Logo />
<p className="mt-3 max-w-xs text-[12.5px] leading-relaxed text-[--color-fg-subtle]">
From a prompt to a hosted, OAuth-protected MCP server for Claude, Cursor and
ChatGPT.
</p>
<Link <Link
href="/status" href="/status"
className="flex items-center gap-2 transition-colors hover:text-[--color-fg]" className="mt-4 flex items-center gap-2 text-[12px] text-[--color-fg-subtle] transition-colors hover:text-[--color-fg]"
> >
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" /> <span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
<span>System status</span> <span>System status</span>
</Link> </Link>
<div className="flex flex-wrap gap-x-5 gap-y-1">
<Link href="/docs" className="transition-colors hover:text-[--color-fg]">
Docs
</Link>
<Link href="/contact" className="transition-colors hover:text-[--color-fg]">
Contact
</Link>
<Link href="/security" className="transition-colors hover:text-[--color-fg]">
Security
</Link>
<Link href="/privacy" className="transition-colors hover:text-[--color-fg]">
Privacy
</Link>
<Link href="/agb" className="transition-colors hover:text-[--color-fg]">
AGB
</Link>
<Link href="/impressum" className="transition-colors hover:text-[--color-fg]">
Impressum
</Link>
<Link href="/terms" className="transition-colors hover:text-[--color-fg]">
Terms
</Link>
</div> </div>
<div>&copy; {new Date().getFullYear()} BuildMyMCPServer</div> <FooterColumn
title="Product"
links={[
{ href: '/templates', label: 'Templates' },
{ href: '/pricing', label: 'Pricing' },
{ href: '/changelog', label: 'Changelog' },
{ href: '/security', label: 'Security' },
]}
/>
<FooterColumn
title="Resources"
links={[
{ href: '/docs', label: 'Docs' },
{ href: '/guides', label: 'Guides' },
{ href: '/docs/faq', label: 'FAQ' },
{ href: '/contact', label: 'Contact' },
]}
/>
<FooterColumn
title="Legal"
links={[
{ href: '/privacy', label: 'Privacy' },
{ href: '/terms', label: 'Terms' },
{ href: '/agb', label: 'AGB' },
{ href: '/impressum', label: 'Impressum' },
]}
/>
</div>
<div className="mt-10 border-t border-[--color-border] pt-6 text-[12px] text-[--color-fg-subtle]">
&copy; {new Date().getFullYear()} BuildMyMCPServer
</div>
</div> </div>
</footer> </footer>
<CookieBanner /> <CookieBanner />
</div> </div>
); );
} }
function FooterColumn({
title,
links,
}: {
title: string;
links: { href: string; label: string }[];
}) {
return (
<div>
<h3 className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[--color-fg-muted]">
{title}
</h3>
<ul className="mt-3 space-y-2 text-[12.5px] text-[--color-fg-subtle]">
{links.map((l) => (
<li key={l.href}>
<Link href={l.href} className="transition-colors hover:text-[--color-fg]">
{l.label}
</Link>
</li>
))}
</ul>
</div>
);
}

View File

@ -12,31 +12,11 @@ import { JsonLd } from '@/components/json-ld';
import { ParticleHero } from '@/components/particle-hero'; import { ParticleHero } from '@/components/particle-hero';
import { PulseLink } from '@/components/pulse'; import { PulseLink } from '@/components/pulse';
import { ScrollCue } from '@/components/scroll-cue'; import { ScrollCue } from '@/components/scroll-cue';
import { StaticCodeBlock } from '@/components/static-code-block'; import { TIERS } from '@/lib/pricing';
import { FAQ, faqJsonLd } from '@/lib/seo'; import { FAQ, faqJsonLd } from '@/lib/seo';
import { ChevronDown } from 'lucide-react'; import { Activity, ChevronDown, Container, ShieldCheck } from 'lucide-react';
import type { ComponentType } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import type { ComponentType } from 'react';
const PROMPT_EXAMPLE = `Create an MCP server that searches our Notion workspace.
Tools: search_pages, get_page_content.
Auth: NOTION_API_KEY.`;
const OUTPUT_EXAMPLE = `> Generating spec... OK (2 tools)
> Static checks OK
> Building image bmm-mcp-notion OK 17.2s
> Deploying container OK
> Live at https://notion-x9.mcp.buildmymcpserver.com
> First request: 401 token 200 OK`;
const INSTALL_SNIPPET = `{
"mcpServers": {
"notion": {
"url": "https://notion-x9.mcp.buildmymcpserver.com/mcp",
"auth": "oauth2"
}
}
}`;
interface ExampleEntry { interface ExampleEntry {
title: string; title: string;
@ -50,41 +30,58 @@ interface ExampleEntry {
} }
const EXAMPLES: ExampleEntry[] = [ 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: 'PostgreSQL',
{ title: 'Notion', desc: 'Search pages, read content, append blocks.', Icon: NotionIcon, bg: '#ffffff', fg: '#0a0a0b' }, desc: 'Read-only access to your tables with schema introspection.',
{ title: 'GitHub', desc: 'List issues, search code, post comments. Scoped to one repo.', Icon: GitHubIcon, bg: '#181717', fg: '#ffffff' }, Icon: PostgresIcon,
{ title: 'Stripe', desc: 'Look up charges, customers, refunds (read-only by default).', Icon: StripeIcon, bg: '#635bff', fg: '#ffffff' }, bg: '#336791',
{ title: 'Custom REST',desc: 'Wrap any HTTP API behind one prompt-defined tool surface.', Icon: RestIcon, bg: '#6366f1', fg: '#ffffff' }, 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',
},
]; ];
interface ClientEntry { // Honest marketplace preview data: template names + tool counts only. These
name: string; // mirror the first-party starter templates; no invented fork counts, no
/** Single-character mark for inline visual identity. */ // "verified" theatre — the frame is labelled as a preview of the card format.
mark: string; const PREVIEW_TEMPLATES: { name: string; author: string; tools: number }[] = [
} { name: 'notion-search', author: 'core', tools: 2 },
{ name: 'github-issues', author: 'core', tools: 3 },
const CLIENTS: ClientEntry[] = [ { name: 'stripe-readonly', author: 'core', tools: 4 },
{ name: 'Claude Desktop', mark: 'C' }, { name: 'postgres-readonly', author: 'core', tools: 3 },
{ name: 'Cursor', mark: '⌘' },
{ name: 'ChatGPT', mark: '✦' },
{ name: 'VS Code Copilot', mark: '<>' },
{ name: 'Continue.dev', mark: '→' },
];
interface MockTemplate {
name: string;
author: string;
tools: number;
forks: number;
verified: boolean;
}
const MOCK_TEMPLATES: MockTemplate[] = [
{ name: 'notion-search', author: 'core', tools: 2, forks: 247, verified: true },
{ name: 'github-issues', author: 'core', tools: 3, forks: 89, verified: true },
{ name: 'stripe-readonly', author: 'core', tools: 4, forks: 156, verified: true },
{ name: 'linear-tasks', author: 'community', tools: 5, forks: 34, verified: false },
]; ];
const MARKETPLACE_POINTS: { t: string; d: string }[] = [ const MARKETPLACE_POINTS: { t: string; d: string }[] = [
@ -97,94 +94,104 @@ const MARKETPLACE_POINTS: { t: string; d: string }[] = [
d: "A template carries the spec and generated code, never the author's API keys. You add your own on fork.", d: "A template carries the spec and generated code, never the author's API keys. You add your own on fork.",
}, },
{ {
t: 'Ranked by real usage', t: 'Open from day one',
d: 'Templates rise on fork count and active deploys, not vanity stars. The useful ones surface themselves.', d: 'Publish a server you built and anyone can fork it. The marketplace is young — early templates set the standard.',
}, },
]; ];
const TIERS = [ // 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 }>;
}[] = [
{ {
name: 'Hobby', t: 'OAuth 2.1 authorization server',
price: '€0', d: 'PKCE, Dynamic Client Registration and Resource Indicators (RFC 8707) in front of every server.',
tag: 'Forever free', href: '/docs/oauth',
features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'], linkLabel: 'Read the auth docs',
Icon: ShieldCheck,
}, },
{ {
name: 'Pro', t: 'Live system status',
price: '€49', d: 'Uptime and incident history, public. If a build queue slows down, you see it before we tell you.',
tag: '/ month', href: '/status',
features: [ linkLabel: 'Check status now',
'5 servers', Icon: Activity,
'1M calls/mo',
'Priority build queue',
'Custom domain · soon',
'Email support',
],
}, },
{ {
name: 'Team', t: 'Per-server container isolation',
price: '€199', d: 'Every generated server runs in its own Docker container. Secrets AES-256-GCM encrypted at rest.',
tag: '/ month', href: '/security',
features: ['25 servers', '10M calls/mo', 'Audit log', 'RBAC · soon', 'Slack support'], linkLabel: 'Security architecture',
}, Icon: Container,
{
name: 'Enterprise',
price: 'Custom',
tag: 'talk to us',
features: ['Unlimited', 'Custom infra · on request', 'SSO/SAML · on request', 'Dedicated hosting', 'Customer success'],
}, },
]; ];
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() { export default function Landing() {
const teaserTiers = TIERS.filter((t) => t.name === 'Hobby' || t.name === 'Pro');
return ( return (
<> <>
{/* Hero left: copy + CTAs, right: cycling step-rotator tile. {/* Hero left: copy + CTAs, right: cycling terminal tile. The WebGL
The old layout stacked three static code blocks vertically; the particle field sits behind at z-0 with pointer-events:none so the
new layout shows one centered tile that cycles through the same CTAs stay interactive. */}
three artifacts (prompt build.log claude config) with a
mouse-reactive 3D tilt and a step indicator. Shorter overall
so the video section below is teased above the fold. */}
<section <section
className="relative flex items-center overflow-hidden border-b border-[--color-border]" className="relative flex items-center overflow-hidden border-b border-[--color-border]"
style={{ minHeight: 'calc(100svh - 3rem)' }} style={{ minHeight: 'calc(100svh - 3rem)' }}
> >
{/* WebGL particle field capability-detected client component.
Sits behind the hero content at z-0 with pointer-events:none
so the CTAs above remain fully interactive. The canvas listens
for pointermove on window itself, so the ring still tracks
the cursor through the content above. With the hero now
filling the full first-viewport (minus the 48px sticky nav),
the field has cinematic-scale room and the indigo radial
glow + dot mask read as the dominant background motif. */}
<ParticleHero /> <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="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"> <div className="min-w-0">
<span className="mono inline-block rounded-full border border-[--color-border] bg-[--color-bg-elevated] px-2.5 py-0.5 text-[11px] tracking-wide text-[--color-fg-muted]"> <h1 className="text-balance text-[36px] font-semibold leading-[1.04] tracking-[-0.03em] sm:text-[44px] md:text-[60px]">
v0.1 · updated 2026-05-20
</span>
<h1 className="mt-6 text-balance text-[30px] font-semibold leading-[1.06] tracking-tight sm:text-[40px] md:text-[52px]">
Describe your tool. Describe your tool.
<br /> <br />
We host the server. We host the server.
<br /> <br />
<span className="text-[--color-fg-muted]">AI uses it.</span> <span className="text-[--color-fg-muted]">AI uses it.</span>
</h1> </h1>
<p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted]"> <p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted] sm:text-[16px]">
From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready
for Claude, Cursor and ChatGPT. for Claude, Cursor and ChatGPT.
</p> </p>
<div className="mt-7 flex flex-wrap items-center gap-3"> <div className="mt-8 flex flex-wrap items-center gap-3">
<PulseLink <PulseLink
href="/login" href="/login"
className="inline-flex h-9 items-center justify-center rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]" className="btn-brand inline-flex h-11 items-center justify-center rounded-md px-5 text-[14px] font-medium"
> >
Start building free Start building free
</PulseLink> </PulseLink>
<PulseLink <PulseLink
href="/docs" href="#flow"
className="inline-flex h-9 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-4 text-[13px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]" 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]"
> >
Read the docs See a live build
</PulseLink> </PulseLink>
</div> </div>
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-subtle]"> <div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-subtle]">
@ -208,28 +215,17 @@ export default function Landing() {
</div> </div>
</div> </div>
</section> </section>
{/* Scroll cue fixed at the bottom of the loadscreen rather than
inside the hero, so it sits at the natural lower edge of the
first viewport regardless of how tall the hero ends up. Fades
out once the user has scrolled past the loadscreen. */}
<ScrollCue targetId="flow" /> <ScrollCue targetId="flow" />
{/* Flow video full-width edge-to-edge under the hero. The clip {/* Flow video full-width edge-to-edge under the hero. Plays when
shows the real flow (prompt server schematic live connection scrolled into view (see hero-video.tsx); preload=metadata keeps the
to Claude Desktop) in three smooth phases. autoplay-muted-loop + 2.6 MB mp4 off the critical path on mobile. */}
playsInline satisfies every mobile browser autoplay policy; the
`poster` carries first paint while the video decodes. */}
<section <section
id="flow" id="flow"
className="relative w-full overflow-hidden border-b border-[--color-border] bg-black" className="relative w-full overflow-hidden border-b border-[--color-border] bg-black"
> >
<div className="relative aspect-video w-full"> <div className="relative aspect-video w-full">
{/* HeroVideo: native <video> with autoplay+muted+loop, plus a
frosted mute toggle pinned bottom-right so visitors can
switch the narration on. See components/hero-video.tsx for
the autoplay/unmute mechanics. */}
<HeroVideo /> <HeroVideo />
{/* Subtle vignette to integrate edges into the rest of the page */}
<div <div
aria-hidden aria-hidden
className="pointer-events-none absolute inset-0" className="pointer-events-none absolute inset-0"
@ -241,95 +237,101 @@ export default function Landing() {
</div> </div>
</section> </section>
{/* How it works */} {/* 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"> <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="mx-auto max-w-6xl px-6">
<div className="mb-10 max-w-2xl"> <div className="mb-10 max-w-2xl">
<h2 className="text-[28px] font-semibold tracking-tight">How it works</h2> <Kicker>how_it_works</Kicker>
<p className="mt-2 text-[14px] text-[--color-fg-muted]"> <h2 className="mt-3 text-[28px] font-semibold tracking-tight sm:text-[32px]">
Three steps. No JSON to write, no Docker to manage. Three steps. No JSON to write, no Docker to manage.
</p> </h2>
</div> </div>
{/* The same video used to live here; it now has its own <div className="relative">
full-width section directly under the hero so it's teased {/* Gradient connector — desktop only, sits behind the node row. */}
above the fold and gets edge-to-edge real estate. This <div
section keeps the three explanatory cards as supporting aria-hidden
copy under the video. */} className="absolute left-0 right-0 top-[22px] hidden h-px md:block"
<div className="grid gap-6 md:grid-cols-3"> style={{ background: 'var(--gradient-brand)', opacity: 0.35 }}
{[ />
{ {/* Mobile left rail joining the stacked nodes. */}
n: '01', <div
t: 'Describe your tool', aria-hidden
d: 'A sentence is enough. List your secrets and which APIs to call.', className="absolute bottom-6 left-[22px] top-[22px] w-px md:hidden"
}, style={{ background: 'var(--gradient-brand)', opacity: 0.35 }}
{ />
n: '02', <div className="grid gap-8 md:grid-cols-3 md:gap-6">
t: 'We generate, check, deploy', {PIPELINE_STEPS.map((s) => (
d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.', <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]"
n: '03', style={{
t: 'Install in your client', background: 'var(--color-bg)',
d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.', boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05)',
}, }}
].map((s) => ( >
<div key={s.n} className="panel p-5">
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">
{s.n} {s.n}
</div> </div>
<h3 className="mt-4 text-[15px] font-semibold tracking-tight">{s.t}</h3> <div className="min-w-0 md:mt-5">
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">{s.d}</p> <h3 className="text-[15px] font-semibold tracking-tight">{s.t}</h3>
</div> <p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">
))} {s.d}
</div>
</div>
</section>
{/* Clients · trust-signal row
Archetype: typographic logo row. No panels, no bullets, just
well-spaced word-marks with a small symbolic mark. The whole
section breathes generous py so it reads as a moment of
trust, not a feature card. */}
<section className="border-b border-[--color-border] bg-[--color-bg] py-20 sm:py-24">
<div className="mx-auto max-w-6xl px-6">
<p className="text-center text-[11px] uppercase tracking-[0.24em] text-[--color-fg-subtle]">
Connects everywhere your AI lives
</p> </p>
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-14 gap-y-7 sm:gap-x-20"> </div>
{CLIENTS.map((c) => (
<div
key={c.name}
className="group flex items-center gap-3 transition-colors"
>
<span
aria-hidden
className="mono flex size-7 items-center justify-center rounded-md border border-[--color-border] text-[12px] text-[--color-fg-muted] transition-colors group-hover:border-[--color-accent] group-hover:text-[--color-accent]"
>
{c.mark}
</span>
<span className="text-[17px] font-medium tracking-tight text-[--color-fg-muted] transition-colors group-hover:text-[--color-fg]">
{c.name}
</span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div>
</section> </section>
{/* Examples · integration grid with brand-coloured marks {/* Proof by specificity three verifiable claims, each one click from
Archetype: 2-col header (title left, supporting copy right) + its evidence. Replaces the old pseudo-logo row: no invented marks,
asymmetric 3-col card grid where each card carries a coloured just a plain-text compatibility line. */}
square brand mark for the service. The marks give each card <section className="border-b border-[--color-border] py-16 sm:py-24">
its own visual identity, breaking the "every card looks the <div className="mx-auto max-w-6xl px-6">
same" pattern of the previous version. */} <Kicker>verify_it_yourself</Kicker>
<h2 className="mt-3 max-w-2xl text-[28px] font-semibold leading-[1.1] tracking-tight sm:text-[32px]">
No testimonials yet we're new.
<br />
<span className="text-[--color-fg-muted]">So check the claims instead.</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-subtle]">
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. */}
<section className="border-b border-[--color-border] py-20 sm:py-28"> <section className="border-b border-[--color-border] py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-6"> <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 className="mb-12 grid gap-6 md:grid-cols-[1fr_auto] md:items-end md:gap-12">
<div> <div>
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]"> <Kicker>use_cases</Kicker>
Use cases
</p>
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]"> <h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Wrap any HTTP API. Wrap any HTTP API.
<br /> <br />
@ -337,7 +339,7 @@ export default function Landing() {
</h2> </h2>
</div> </div>
<p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]"> <p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]">
Real integrations our customers ship today. Built from one prompt each. Ship integrations like these from one prompt each.
</p> </p>
</div> </div>
@ -347,7 +349,7 @@ export default function Landing() {
return ( return (
<div <div
key={e.title} key={e.title}
className="group flex items-start gap-4 rounded-xl border border-[--color-border] bg-[--color-bg-elevated] p-5 transition-colors hover:border-[--color-border-strong]" className="panel-raised group flex items-start gap-4 p-5 transition-colors hover:border-[--color-border-strong]"
> >
<div <div
aria-hidden aria-hidden
@ -372,27 +374,20 @@ export default function Landing() {
</div> </div>
</section> </section>
{/* Marketplace · two-column with product preview {/* Marketplace split layout: selling points left, honest preview
Archetype: split layout. Left column carries the headline, frame right. */}
three bullet-form selling points, and the CTA. Right column
carries a mock browser frame containing real-looking template
cards so the visitor SEES the marketplace, not just reads
a description of it. */}
<section className="border-b border-[--color-border] py-20 sm:py-28"> <section className="border-b border-[--color-border] 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 className="mx-auto grid max-w-6xl gap-14 px-6 md:grid-cols-[5fr_6fr] md:items-center md:gap-16">
{/* Left — text + points + CTA */}
<div> <div>
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]"> <Kicker>marketplace</Kicker>
Marketplace
</p>
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]"> <h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Skip the prompt. Skip the prompt.
<br /> <br />
<span className="text-[--color-fg-muted]">Fork what works.</span> <span className="text-[--color-fg-muted]">Fork what works.</span>
</h2> </h2>
<p className="mt-5 max-w-md text-[14px] leading-relaxed text-[--color-fg-muted]"> <p className="mt-5 max-w-md text-[14px] leading-relaxed text-[--color-fg-muted]">
The marketplace is a library of working MCP servers the community already built. The marketplace is a library of working MCP servers. Fork one, paste your credentials,
Fork one, paste your credentials, deploy. Or publish yours and let others build on it. deploy. Or publish yours and let others build on it.
</p> </p>
<ul className="mt-8 space-y-5"> <ul className="mt-8 space-y-5">
@ -414,29 +409,22 @@ export default function Landing() {
<PulseLink <PulseLink
href="/templates" href="/templates"
className="mt-8 inline-flex h-9 items-center gap-2 rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]" className="btn-brand mt-8 inline-flex h-11 items-center gap-2 rounded-md px-5 text-[14px] font-medium"
> >
Browse the marketplace Browse the marketplace
</PulseLink> </PulseLink>
</div> </div>
{/* Right — mock marketplace browser frame */} <MarketplacePreview />
<MarketplaceMock />
</div> </div>
</section> </section>
{/* Pricing · 4 cards with Pro highlighted as recommended {/* Pricing teaser Hobby + Pro only; the full matrix lives on
Archetype: centred header + 4-card row where the Pro tier is /pricing. Data is shared via lib/pricing.ts so it can't drift. */}
visually distinct (accent border, indigo glow shadow, floating
"recommended" pill, slightly elevated). Other tiers stay calm
so the eye lands on Pro first. Big price typography (text-
[40px]) replaces the previous flat 26px. */}
<section id="pricing" className="border-b border-[--color-border] py-20 sm:py-28"> <section id="pricing" className="border-b border-[--color-border] py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-6"> <div className="mx-auto max-w-4xl px-6">
<div className="mb-14 text-center"> <div className="mb-12 text-center">
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]"> <Kicker>pricing</Kicker>
Pricing
</p>
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]"> <h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Pay for tool calls. Pay for tool calls.
<br /> <br />
@ -444,16 +432,16 @@ export default function Landing() {
</h2> </h2>
</div> </div>
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-5 sm:grid-cols-2">
{TIERS.map((t) => { {teaserTiers.map((t) => {
const featured = t.name === 'Pro'; const featured = Boolean(t.highlight);
return ( return (
<div <div
key={t.name} key={t.name}
className={`relative flex flex-col gap-6 rounded-xl border p-6 transition-colors ${ className={`relative flex flex-col gap-6 rounded-xl border p-6 ${
featured featured
? 'border-[--color-accent] bg-[--color-bg-elevated]' ? 'border-[--color-accent] bg-[--color-bg-elevated]'
: 'border-[--color-border] bg-[--color-bg-elevated] hover:border-[--color-border-strong]' : 'border-[--color-border] bg-[--color-bg-elevated]'
}`} }`}
style={ style={
featured featured
@ -461,7 +449,7 @@ export default function Landing() {
boxShadow: boxShadow:
'0 0 0 4px rgba(99, 102, 241, 0.12), 0 24px 50px rgba(0, 0, 0, 0.35)', '0 0 0 4px rgba(99, 102, 241, 0.12), 0 24px 50px rgba(0, 0, 0, 0.35)',
} }
: undefined : { boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04)' }
} }
> >
{featured && ( {featured && (
@ -483,6 +471,9 @@ export default function Landing() {
</span> </span>
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span> <span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
</div> </div>
<p className="mt-3 text-[13px] leading-relaxed text-[--color-fg-muted]">
{t.description}
</p>
</div> </div>
<ul className="flex flex-1 flex-col gap-2.5 border-t border-[--color-border] pt-5 text-[13px] text-[--color-fg-muted]"> <ul className="flex flex-1 flex-col gap-2.5 border-t border-[--color-border] pt-5 text-[13px] text-[--color-fg-muted]">
@ -498,23 +489,42 @@ export default function Landing() {
</li> </li>
))} ))}
</ul> </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> </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> </div>
</section> </section>
{/* FAQ collapsible accordion using native <details>. Crawlers {/* FAQ collapsible accordion using native <details>. Crawlers and
and screen readers still see the full Q+A in the HTML; users screen readers see the full Q+A in the HTML. */}
see one question at a time and expand on demand. No JS, no <section className="border-b border-[--color-border] py-14 sm:py-20">
state, semantically correct. `list-none` + the WebKit-marker
pseudo-class suppress the default disclosure triangle so we
can render our own chevron that rotates via `group-open`. */}
<section className="py-14 sm:py-20">
<JsonLd data={faqJsonLd()} /> <JsonLd data={faqJsonLd()} />
<div className="mx-auto max-w-3xl px-6"> <div className="mx-auto max-w-3xl px-6">
<h2 className="text-[28px] font-semibold tracking-tight">FAQ</h2> <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]"> <div className="mt-8 border-t border-[--color-border]">
{FAQ.map((f) => ( {FAQ.map((f) => (
<details <details
@ -536,28 +546,58 @@ export default function Landing() {
</div> </div>
</div> </div>
</section> </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>
</> </>
); );
} }
/** /**
* Mock marketplace preview. * Honest marketplace preview.
* *
* Static, server-rendered. Renders a faux-browser frame containing four * Static, server-rendered browser frame showing what a template card looks
* realistic template cards so the visitor sees what the marketplace * like. Card data mirrors the first-party starter templates names, author
* actually looks like rather than reading marketing copy about it. * and tool counts only. No fork counts, no "verified" badges: the frame is
* * explicitly labelled a preview, not live marketplace traffic.
* The card data lives in `MOCK_TEMPLATES` at the top of this file
* those are real template-flavoured names, modest fork counts, and the
* `core` / `community` author tag. Numbers are deliberately small and
* truthful-looking for a young marketplace; nothing here pretends to be
* GitHub-trending traffic.
*/ */
function MarketplaceMock() { function MarketplacePreview() {
return ( return (
<div <div
className="overflow-hidden rounded-xl border border-[--color-border-strong] bg-[--color-bg-elevated]" className="overflow-hidden rounded-xl border border-[--color-border-strong] bg-[--color-bg-elevated]"
style={{ boxShadow: '0 24px 60px rgba(0, 0, 0, 0.45)' }} style={{
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04), 0 24px 60px rgba(0, 0, 0, 0.45)',
}}
> >
{/* Browser chrome */} {/* 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 items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3">
@ -573,27 +613,21 @@ function MarketplaceMock() {
{/* Toolbar inside the page chrome */} {/* Toolbar inside the page chrome */}
<div className="flex items-center justify-between gap-4 border-b border-[--color-border] px-5 py-3"> <div className="flex items-center justify-between gap-4 border-b border-[--color-border] px-5 py-3">
<div className="flex items-center gap-3">
<span className="text-[13px] font-semibold tracking-tight text-[--color-fg]"> <span className="text-[13px] font-semibold tracking-tight text-[--color-fg]">
Templates Templates
</span> </span>
<span className="text-[11px] text-[--color-fg-subtle]">·</span> <span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
<span className="text-[11px] text-[--color-fg-subtle]">{MOCK_TEMPLATES.length} live</span> preview
</div> </span>
<div className="hidden items-center gap-1.5 rounded-md border border-[--color-border] px-2 py-1 sm:flex">
<span className="text-[11px] text-[--color-fg-subtle]">trending</span>
<ChevronDown size={11} className="text-[--color-fg-subtle]" />
</div>
</div> </div>
{/* Template cards grid */} {/* Template cards grid */}
<div className="grid gap-3 p-4 sm:grid-cols-2"> <div className="grid gap-3 p-4 sm:grid-cols-2">
{MOCK_TEMPLATES.map((t) => ( {PREVIEW_TEMPLATES.map((t) => (
<div <div
key={t.name} key={t.name}
className="rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-4 transition-colors hover:border-[--color-accent]/40" className="rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-4 transition-colors hover:border-[--color-accent]/40"
> >
<div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="mono truncate text-[13px] font-semibold tracking-tight text-[--color-fg]"> <div className="mono truncate text-[13px] font-semibold tracking-tight text-[--color-fg]">
{t.name} {t.name}
@ -602,21 +636,14 @@ function MarketplaceMock() {
<span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]"> <span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
{t.author} {t.author}
</span> </span>
{t.verified && ( <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]">
<span className="inline-flex items-center gap-0.5 rounded-full border border-[--color-accent]/50 bg-[--color-accent]/10 px-1.5 py-px text-[9.5px] font-medium uppercase tracking-wider text-[--color-accent]"> template
verified
</span> </span>
)}
</div>
</div> </div>
</div> </div>
<div className="mt-4 flex items-center justify-between text-[11px] text-[--color-fg-subtle]"> <div className="mt-4 text-[11px] text-[--color-fg-subtle]">
<span className="mono">{t.tools} tools</span> <span className="mono">{t.tools} tools</span>
<span className="mono inline-flex items-center gap-1">
<ForkGlyph />
{t.forks}
</span>
</div> </div>
</div> </div>
))} ))}
@ -624,20 +651,3 @@ function MarketplaceMock() {
</div> </div>
); );
} }
function ForkGlyph() {
return (
<svg width="11" height="11" viewBox="0 0 14 14" fill="none" aria-hidden>
<circle cx="3.5" cy="3" r="1.4" stroke="currentColor" strokeWidth={1.2} />
<circle cx="10.5" cy="3" r="1.4" stroke="currentColor" strokeWidth={1.2} />
<circle cx="7" cy="11" r="1.4" stroke="currentColor" strokeWidth={1.2} />
<path
d="M 3.5 4.5 L 3.5 6.5 Q 3.5 7.5 4.5 7.5 L 9.5 7.5 Q 10.5 7.5 10.5 6.5 L 10.5 4.5"
stroke="currentColor"
strokeWidth={1.2}
fill="none"
/>
<path d="M 7 7.5 L 7 9.5" stroke="currentColor" strokeWidth={1.2} />
</svg>
);
}

View File

@ -1,3 +1,4 @@
import { TIERS } from '@/lib/pricing';
import { pageMetadata } from '@/lib/seo'; import { pageMetadata } from '@/lib/seo';
import Link from 'next/link'; import Link from 'next/link';
@ -8,80 +9,6 @@ export const metadata = pageMetadata({
path: '/pricing', path: '/pricing',
}); });
const TIERS = [
{
name: 'Hobby',
price: '€0',
tag: 'Forever free',
description: 'For trying things out and shipping single-user tools.',
model: 'Open-tier AI',
modelDetail: 'Free-tier model · ~30-60s analyze',
features: [
'1 MCP server',
'100,000 tool calls / month',
'5 prompt analyses / day',
'BuildMyMCP subdomain',
'Community support',
],
cta: 'Start free',
href: '/login',
},
{
name: 'Pro',
price: '€49',
tag: '/ month',
description: 'For solo founders and small teams shipping production tools.',
model: 'Claude AI',
modelDetail: 'Powered by Anthropic Claude · ~1020s analyze',
features: [
'5 MCP servers',
'1M tool calls / month',
'40 prompt analyses / day',
'Priority build queue',
'Custom domain · coming soon',
'Email support, 1 business-day response',
],
cta: 'Start Pro',
href: '/settings/billing?tier=pro_monthly',
highlight: true,
},
{
name: 'Team',
price: '€199',
tag: '/ month',
description: 'For teams that need an audit trail and room to scale.',
model: 'Claude AI',
modelDetail: "Anthropic's flagship quality",
features: [
'25 MCP servers',
'10M tool calls / month',
'50 prompt analyses / day',
'Audit log',
'RBAC · coming soon',
'Shared Slack channel support',
],
cta: 'Start Team',
href: '/settings/billing?tier=team_monthly',
},
{
name: 'Enterprise',
price: 'Custom',
tag: 'talk to us',
description: 'For organizations with custom infrastructure, compliance and scale needs.',
model: 'Claude AI',
modelDetail: 'Top-tier Claude · EU data-residency option',
features: [
'Unlimited servers',
'Custom infrastructure & data residency — on request',
'Dedicated hosting — scoped per contract',
'SSO / SAML — on request',
'Customer success manager',
],
cta: 'Contact sales',
href: 'mailto:sales@buildmymcpserver.com',
},
];
const FAQ = [ const FAQ = [
{ {
q: 'What counts as a tool call?', q: 'What counts as a tool call?',
@ -93,7 +20,7 @@ const FAQ = [
}, },
{ {
q: 'Annual billing?', q: 'Annual billing?',
a: 'Yes — save 20% on Pro and Team paying annually. Enterprise is annual by default.', a: 'Annual plans are coming — contact us for annual invoicing today. Enterprise is annual by default.',
}, },
{ {
q: 'Plan changes?', q: 'Plan changes?',

View File

@ -1,15 +1,21 @@
@import 'tailwindcss'; @import "tailwindcss";
@theme { @theme {
--color-bg: #0a0a0b; --color-bg: #0a0a0b;
--color-bg-elevated: #111114; /* Elevated surfaces carry a ~1% indigo tint so panels read as part of the
--color-bg-subtle: #16161a; brand instead of flat gray. Kept close enough to the old #111114/#16161a
that dashboard contrast ratios are unaffected. */
--color-bg-elevated: #121218;
--color-bg-subtle: #17171d;
--color-fg: #fafafa; --color-fg: #fafafa;
--color-fg-muted: #a1a1aa; --color-fg-muted: #a1a1aa;
--color-fg-subtle: #71717a; --color-fg-subtle: #71717a;
--color-border: #1f1f22; --color-border: #1f1f22;
--color-border-strong: #2a2a2e; --color-border-strong: #2a2a2e;
--color-accent: #6366f1; --color-accent: #6366f1;
/* Secondary brand hue cyan endpoint of the identity gradient. Reserved
for the gradient, kickers and "live" accents; never a surface color. */
--color-accent-2: #22d3ee;
--color-accent-fg: #ffffff; --color-accent-fg: #ffffff;
--color-success: #22c55e; --color-success: #22c55e;
--color-warn: #f59e0b; --color-warn: #f59e0b;
@ -39,7 +45,7 @@
background: var(--color-bg); background: var(--color-bg);
color: var(--color-fg); color: var(--color-fg);
font-family: var(--font-sans); font-family: var(--font-sans);
font-feature-settings: 'cv11', 'ss01'; font-feature-settings: "cv11", "ss01";
} }
::selection { ::selection {
background: rgba(99, 102, 241, 0.3); background: rgba(99, 102, 241, 0.3);
@ -100,7 +106,56 @@
} }
} }
/* Identity gradient indigo cyan. Defined outside @theme (it's not a
Tailwind token, just a shared value) and used ONLY for: primary CTAs,
section kickers, the featured pricing card and one hero glow. Scarcity is
what keeps it feeling like a brand mark instead of decoration. */
:root {
--gradient-brand: linear-gradient(135deg, #6366f1 0%, #22d3ee 100%);
}
@layer components { @layer components {
/* Gradient-filled primary CTA. A dark inner overlay on hover (instead of a
second gradient) keeps the transition GPU-cheap. */
.btn-brand {
position: relative;
background: var(--gradient-brand);
color: #ffffff;
isolation: isolate;
}
.btn-brand::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: rgba(10, 10, 11, 0);
transition: background-color 0.2s ease-out;
z-index: -1;
}
.btn-brand:hover::after {
background: rgba(10, 10, 11, 0.18);
}
/* Gradient text for the rare display moment (final CTA heading). */
.text-brand-gradient {
background: var(--gradient-brand);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Mono section kicker, shell-comment style: `## how_it_works` */
.kicker {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.08em;
color: var(--color-accent);
}
/* Elevated panel with an inner top highlight — reads as machined edge. */
.panel-raised {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.panel { .panel {
background: var(--color-bg-elevated); background: var(--color-bg-elevated);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
@ -139,7 +194,8 @@
} }
@keyframes pulse-dot { @keyframes pulse-dot {
0%, 100% { 0%,
100% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }

View File

@ -174,10 +174,20 @@ export function HeroStepRotator() {
y: glowDy, y: glowDy,
}} }}
/> />
{/* Terminal chrome: traffic lights + filename tab. The tile IS
the product's aesthetic (build logs, configs), so it should
look like a real terminal window, not a generic card. */}
<div className="relative flex items-center justify-between border-b border-[--color-border] px-4 py-2.5"> <div className="relative flex items-center justify-between border-b border-[--color-border] px-4 py-2.5">
<span className="flex items-center gap-3">
<span aria-hidden 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" />
</span>
<span className="mono text-[11px] uppercase tracking-wider text-[--color-fg-subtle]"> <span className="mono text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
{current.label} {current.label}
</span> </span>
</span>
<span className="mono text-[10.5px] tracking-[0.16em] text-[--color-accent]"> <span className="mono text-[10.5px] tracking-[0.16em] text-[--color-accent]">
{current.badge} {current.badge}
</span> </span>
@ -189,23 +199,28 @@ export function HeroStepRotator() {
</AnimatePresence> </AnimatePresence>
</div> </div>
{/* Step indicator accent dot is wider + glows so the active step {/* Step indicator the visible dot stays small, but each button
reads at a glance. Buttons stay clickable so users can jump. */} carries generous padding so the effective touch target is 44px.
<div className="flex items-center gap-2" role="tablist" aria-label="Hero flow steps"> Plain buttons with aria-current (not a tablist no arrow-key
semantics to honour, they're simple jump buttons). */}
<div className="flex items-center" aria-label="Hero flow steps">
{STEPS.map((s, i) => ( {STEPS.map((s, i) => (
<button <button
key={s.badge} key={s.badge}
type="button" type="button"
role="tab" aria-current={i === step}
aria-selected={i === step}
aria-label={`Jump to ${s.badge}`} aria-label={`Jump to ${s.badge}`}
onClick={() => setStep(i)} onClick={() => setStep(i)}
className="flex min-h-11 min-w-11 items-center justify-center"
>
<span
className={`h-1.5 rounded-full transition-all duration-300 ${ className={`h-1.5 rounded-full transition-all duration-300 ${
i === step i === step
? 'w-9 bg-[--color-accent] shadow-[0_0_10px_rgba(99,102,241,0.65)]' ? 'w-9 bg-[--color-accent] shadow-[0_0_10px_rgba(99,102,241,0.65)]'
: 'w-1.5 bg-[--color-border-strong] hover:bg-[--color-fg-subtle]' : 'w-1.5 bg-[--color-border-strong] hover:bg-[--color-fg-subtle]'
}`} }`}
/> />
</button>
))} ))}
</div> </div>
</div> </div>

View File

@ -47,6 +47,9 @@ function formatTime(seconds: number): string {
export function HeroVideo() { export function HeroVideo() {
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
// Set when the user explicitly pauses — the IntersectionObserver must not
// restart a video the user chose to stop.
const userPausedRef = useRef(false);
const [muted, setMuted] = useState(true); const [muted, setMuted] = useState(true);
const [playing, setPlaying] = useState(false); const [playing, setPlaying] = useState(false);
const [playFailed, setPlayFailed] = useState(false); const [playFailed, setPlayFailed] = useState(false);
@ -69,10 +72,26 @@ export function HeroVideo() {
v.addEventListener('timeupdate', onTime); v.addEventListener('timeupdate', onTime);
v.addEventListener('loadedmetadata', onMeta); v.addEventListener('loadedmetadata', onMeta);
v.addEventListener('durationchange', onMeta); v.addEventListener('durationchange', onMeta);
// Best-effort autoplay attempt — silently fail on browsers that // Play only while scrolled into view: with preload="metadata" the 2.6 MB
// block it; the overlay is the user's escape hatch. // mp4 stays off the critical path until the section is actually visible,
v.play().catch(() => undefined); // and playback pauses again off-screen (saves battery + decode). The
// .play() attempt is still best-effort — browsers that block autoplay
// leave the overlay as the user's escape hatch. `userPaused` tracks an
// explicit pause so the observer doesn't fight the user's choice.
const io = new IntersectionObserver(
([entry]) => {
if (!entry) return;
if (entry.isIntersecting) {
if (!userPausedRef.current && v.paused) v.play().catch(() => undefined);
} else if (!v.paused) {
v.pause();
}
},
{ threshold: 0.35 },
);
io.observe(v);
return () => { return () => {
io.disconnect();
v.removeEventListener('play', onPlay); v.removeEventListener('play', onPlay);
v.removeEventListener('pause', onPause); v.removeEventListener('pause', onPause);
v.removeEventListener('timeupdate', onTime); v.removeEventListener('timeupdate', onTime);
@ -82,9 +101,12 @@ export function HeroVideo() {
}, []); }, []);
// Clear any pending hide timer on unmount. // Clear any pending hide timer on unmount.
useEffect(() => () => { useEffect(
() => () => {
if (hideTimer.current) clearTimeout(hideTimer.current); if (hideTimer.current) clearTimeout(hideTimer.current);
}, []); },
[],
);
const scheduleHide = useCallback(() => { const scheduleHide = useCallback(() => {
if (hideTimer.current) clearTimeout(hideTimer.current); if (hideTimer.current) clearTimeout(hideTimer.current);
@ -106,6 +128,7 @@ export function HeroVideo() {
if (!v) return; if (!v) return;
if (v.paused) { if (v.paused) {
setPlayFailed(false); setPlayFailed(false);
userPausedRef.current = false;
try { try {
// .load() resets the media element's resource selection — required // .load() resets the media element's resource selection — required
// when an earlier autoplay attempt was blocked before the source // when an earlier autoplay attempt was blocked before the source
@ -116,6 +139,7 @@ export function HeroVideo() {
setPlayFailed(true); setPlayFailed(true);
} }
} else { } else {
userPausedRef.current = true;
v.pause(); v.pause();
} }
}, []); }, []);
@ -165,11 +189,10 @@ export function HeroVideo() {
> >
<video <video
ref={videoRef} ref={videoRef}
autoPlay
muted muted
loop loop
playsInline playsInline
preload="auto" preload="metadata"
poster="/videos/hero-poster.jpg" poster="/videos/hero-poster.jpg"
onClick={onVideoClick} onClick={onVideoClick}
className="size-full cursor-pointer object-cover" className="size-full cursor-pointer object-cover"

View File

@ -2,27 +2,52 @@
import { Menu, X } from 'lucide-react'; import { Menu, X } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
const LINKS = [ const LINKS = [
{ href: '/#how', label: 'How it works' }, { href: '/#how', label: 'How it works' },
{ href: '/templates', label: 'Templates' }, { href: '/templates', label: 'Templates' },
{ href: '/pricing', label: 'Pricing' }, { href: '/pricing', label: 'Pricing' },
{ href: '/docs', label: 'Docs' }, { href: '/docs', label: 'Docs' },
{ href: '/guides', label: 'Guides' },
{ href: '/changelog', label: 'Changelog' }, { href: '/changelog', label: 'Changelog' },
]; ];
/** Hamburger menu shown below the md breakpoint, where the inline nav is hidden. */ /** Hamburger menu shown below the md breakpoint, where the inline nav is hidden. */
export function MarketingMobileMenu() { export function MarketingMobileMenu() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
// While the panel is open: lock body scroll, close on Escape and on any
// pointerdown outside the menu subtree. All three effects unwind together
// when the panel closes or the component unmounts.
useEffect(() => {
if (!open) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
const onPointerDown = (e: PointerEvent) => {
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('keydown', onKey);
document.addEventListener('pointerdown', onPointerDown);
return () => {
document.body.style.overflow = prevOverflow;
document.removeEventListener('keydown', onKey);
document.removeEventListener('pointerdown', onPointerDown);
};
}, [open]);
return ( return (
<div className="md:hidden"> <div ref={rootRef} className="md:hidden">
<button <button
type="button" type="button"
aria-label={open ? 'Close menu' : 'Open menu'} aria-label={open ? 'Close menu' : 'Open menu'}
aria-expanded={open} aria-expanded={open}
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className="flex size-8 items-center justify-center rounded-md text-[--color-fg-muted] transition-colors hover:text-[--color-fg]" className="flex size-11 items-center justify-center rounded-md text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
> >
{open ? <X size={18} /> : <Menu size={18} />} {open ? <X size={18} /> : <Menu size={18} />}
</button> </button>
@ -34,20 +59,27 @@ export function MarketingMobileMenu() {
// browser falls back to transparent. Inline `var()` is unambiguous. // browser falls back to transparent. Inline `var()` is unambiguous.
className="absolute inset-x-0 top-12 z-40 border-b border-[--color-border] shadow-lg shadow-black/40 backdrop-blur-md" className="absolute inset-x-0 top-12 z-40 border-b border-[--color-border] shadow-lg shadow-black/40 backdrop-blur-md"
style={{ style={{
backgroundColor: 'color-mix(in oklab, var(--color-bg) 80%, transparent)', backgroundColor: 'color-mix(in oklab, var(--color-bg) 92%, transparent)',
}} }}
> >
<nav className="mx-auto flex max-w-6xl flex-col px-6"> <nav className="mx-auto flex max-w-6xl flex-col px-6 pb-5">
{LINKS.map((l) => ( {LINKS.map((l) => (
<Link <Link
key={l.href} key={l.href}
href={l.href} href={l.href}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="border-b border-[--color-border] py-3.5 text-[14px] text-[--color-fg-muted] transition-colors last:border-0 hover:text-[--color-fg]" className="flex min-h-11 items-center border-b border-[--color-border] text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
> >
{l.label} {l.label}
</Link> </Link>
))} ))}
<Link
href="/login"
onClick={() => setOpen(false)}
className="btn-brand mt-4 inline-flex h-11 w-full items-center justify-center rounded-md text-[14px] font-medium"
>
Start building free
</Link>
</nav> </nav>
</div> </div>
)} )}

91
apps/web/lib/pricing.ts Normal file
View File

@ -0,0 +1,91 @@
// Single source of truth for pricing tiers. Rendered on /pricing (all tiers)
// and as the landing-page teaser (Hobby + Pro). The landing page previously
// carried its own hardcoded copy of this data, which had already drifted —
// one array, two consumers, no drift.
export interface PricingTier {
name: string;
price: string;
tag: string;
description: string;
model: string;
modelDetail: string;
features: string[];
cta: string;
href: string;
highlight?: boolean;
}
export const TIERS: PricingTier[] = [
{
name: 'Hobby',
price: '€0',
tag: 'Forever free',
description: 'For trying things out and shipping single-user tools.',
model: 'Open-tier AI',
modelDetail: 'Free-tier model · ~30-60s analyze',
features: [
'1 MCP server',
'100,000 tool calls / month',
'5 prompt analyses / day',
'BuildMyMCP subdomain',
'Community support',
],
cta: 'Start free',
href: '/login',
},
{
name: 'Pro',
price: '€49',
tag: '/ month',
description: 'For solo founders and small teams shipping production tools.',
model: 'Claude AI',
modelDetail: 'Powered by Anthropic Claude · ~1020s analyze',
features: [
'5 MCP servers',
'1M tool calls / month',
'40 prompt analyses / day',
'Priority build queue',
'Custom domain · coming soon',
'Email support, 1 business-day response',
],
cta: 'Start Pro',
href: '/settings/billing?tier=pro_monthly',
highlight: true,
},
{
name: 'Team',
price: '€199',
tag: '/ month',
description: 'For teams that need an audit trail and room to scale.',
model: 'Claude AI',
modelDetail: "Anthropic's flagship quality",
features: [
'25 MCP servers',
'10M tool calls / month',
'50 prompt analyses / day',
'Audit log',
'RBAC · coming soon',
'Shared Slack channel support',
],
cta: 'Start Team',
href: '/settings/billing?tier=team_monthly',
},
{
name: 'Enterprise',
price: 'Custom',
tag: 'talk to us',
description: 'For organizations with custom infrastructure, compliance and scale needs.',
model: 'Claude AI',
modelDetail: 'Top-tier Claude · EU data-residency option',
features: [
'Unlimited servers',
'Custom infrastructure & data residency — on request',
'Dedicated hosting — scoped per contract',
'SSO / SAML — on request',
'Customer success manager',
],
cta: 'Contact sales',
href: 'mailto:sales@buildmymcpserver.com',
},
];