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>
</header>
<main className="flex-1">{children}</main>
<footer className="border-t border-[--color-border] py-8">
<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">
<Link
href="/status"
className="flex items-center gap-2 transition-colors hover:text-[--color-fg]"
>
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
<span>System status</span>
</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>
<footer className="border-t border-[--color-border] py-12">
<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
href="/status"
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>System status</span>
</Link>
</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>&copy; {new Date().getFullYear()} BuildMyMCPServer</div>
</div>
</footer>
<CookieBanner />
</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 { PulseLink } from '@/components/pulse';
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 { ChevronDown } from 'lucide-react';
import type { ComponentType } from 'react';
import { Activity, ChevronDown, Container, ShieldCheck } from 'lucide-react';
import Link from 'next/link';
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"
}
}
}`;
import type { ComponentType } from 'react';
interface ExampleEntry {
title: string;
@ -50,41 +30,58 @@ interface 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: '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' },
{
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',
},
];
interface ClientEntry {
name: string;
/** Single-character mark for inline visual identity. */
mark: string;
}
const CLIENTS: ClientEntry[] = [
{ name: 'Claude Desktop', mark: 'C' },
{ 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 },
// 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 }[] = [
@ -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.",
},
{
t: 'Ranked by real usage',
d: 'Templates rise on fork count and active deploys, not vanity stars. The useful ones surface themselves.',
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.',
},
];
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',
price: '€0',
tag: 'Forever free',
features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'],
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,
},
{
name: 'Pro',
price: '€49',
tag: '/ month',
features: [
'5 servers',
'1M calls/mo',
'Priority build queue',
'Custom domain · soon',
'Email support',
],
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,
},
{
name: 'Team',
price: '€199',
tag: '/ month',
features: ['25 servers', '10M calls/mo', 'Audit log', 'RBAC · soon', 'Slack support'],
},
{
name: 'Enterprise',
price: 'Custom',
tag: 'talk to us',
features: ['Unlimited', 'Custom infra · on request', 'SSO/SAML · on request', 'Dedicated hosting', 'Customer success'],
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 step-rotator tile.
The old layout stacked three static code blocks vertically; the
new layout shows one centered tile that cycles through the same
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. */}
{/* 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 - 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 />
<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">
<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]">
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]">
<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]">
<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
for Claude, Cursor and ChatGPT.
</p>
<div className="mt-7 flex flex-wrap items-center gap-3">
<div className="mt-8 flex flex-wrap items-center gap-3">
<PulseLink
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
href="/docs"
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]"
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]"
>
Read the docs
See a live build
</PulseLink>
</div>
<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>
</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" />
{/* Flow video full-width edge-to-edge under the hero. The clip
shows the real flow (prompt server schematic live connection
to Claude Desktop) in three smooth phases. autoplay-muted-loop +
playsInline satisfies every mobile browser autoplay policy; the
`poster` carries first paint while the video decodes. */}
{/* 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: 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 />
{/* Subtle vignette to integrate edges into the rest of the page */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
@ -241,95 +237,101 @@ export default function Landing() {
</div>
</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">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-10 max-w-2xl">
<h2 className="text-[28px] font-semibold tracking-tight">How it works</h2>
<p className="mt-2 text-[14px] text-[--color-fg-muted]">
<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.
</p>
</h2>
</div>
{/* The same video used to live here; it now has its own
full-width section directly under the hero so it's teased
above the fold and gets edge-to-edge real estate. This
section keeps the three explanatory cards as supporting
copy under the video. */}
<div className="grid gap-6 md:grid-cols-3">
{[
{
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.',
},
].map((s) => (
<div key={s.n} className="panel p-5">
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">
{s.n}
<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>
<h3 className="mt-4 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>
</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">
{/* 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="border-b border-[--color-border] py-16 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>
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-14 gap-y-7 sm:gap-x-20">
{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]"
<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]"
>
{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>
))}
<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>
{/* Examples · integration grid with brand-coloured marks
Archetype: 2-col header (title left, supporting copy right) +
asymmetric 3-col card grid where each card carries a coloured
square brand mark for the service. The marks give each card
its own visual identity, breaking the "every card looks the
same" pattern of the previous version. */}
{/* Use cases — brand-coloured integration grid. */}
<section className="border-b border-[--color-border] 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>
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]">
Use cases
</p>
<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 />
@ -337,7 +339,7 @@ export default function Landing() {
</h2>
</div>
<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>
</div>
@ -347,7 +349,7 @@ export default function Landing() {
return (
<div
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
aria-hidden
@ -372,27 +374,20 @@ export default function Landing() {
</div>
</section>
{/* Marketplace · two-column with product preview
Archetype: split layout. Left column carries the headline,
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. */}
{/* Marketplace split layout: selling points left, honest preview
frame right. */}
<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">
{/* Left — text + points + CTA */}
<div>
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]">
Marketplace
</p>
<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 the community already built.
Fork one, paste your credentials, deploy. Or publish yours and let others build on it.
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">
@ -414,29 +409,22 @@ export default function Landing() {
<PulseLink
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
</PulseLink>
</div>
{/* Right — mock marketplace browser frame */}
<MarketplaceMock />
<MarketplacePreview />
</div>
</section>
{/* Pricing · 4 cards with Pro highlighted as recommended
Archetype: centred header + 4-card row where the Pro tier is
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. */}
{/* 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-6xl px-6">
<div className="mb-14 text-center">
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]">
Pricing
</p>
<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 />
@ -444,16 +432,16 @@ export default function Landing() {
</h2>
</div>
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-4">
{TIERS.map((t) => {
const featured = t.name === 'Pro';
<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 transition-colors ${
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] hover:border-[--color-border-strong]'
: 'border-[--color-border] bg-[--color-bg-elevated]'
}`}
style={
featured
@ -461,7 +449,7 @@ export default function Landing() {
boxShadow:
'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 && (
@ -483,6 +471,9 @@ export default function Landing() {
</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]">
@ -498,23 +489,42 @@ export default function Landing() {
</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 still see the full Q+A in the HTML; users
see one question at a time and expand on demand. No JS, no
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">
{/* 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">
<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]">
{FAQ.map((f) => (
<details
@ -536,28 +546,58 @@ export default function Landing() {
</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>
</>
);
}
/**
* Mock marketplace preview.
* Honest marketplace preview.
*
* Static, server-rendered. Renders a faux-browser frame containing four
* realistic template cards so the visitor sees what the marketplace
* actually looks like rather than reading marketing copy about it.
*
* 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.
* 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 MarketplaceMock() {
function MarketplacePreview() {
return (
<div
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 */}
<div className="flex items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3">
@ -573,50 +613,37 @@ function MarketplaceMock() {
{/* 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 gap-3">
<span className="text-[13px] font-semibold tracking-tight text-[--color-fg]">
Templates
</span>
<span className="text-[11px] text-[--color-fg-subtle]">·</span>
<span className="text-[11px] text-[--color-fg-subtle]">{MOCK_TEMPLATES.length} live</span>
</div>
<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>
<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">
{MOCK_TEMPLATES.map((t) => (
{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="flex items-start justify-between gap-3">
<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>
{t.verified && (
<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]">
verified
</span>
)}
</div>
<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 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 inline-flex items-center gap-1">
<ForkGlyph />
{t.forks}
</span>
</div>
</div>
))}
@ -624,20 +651,3 @@ function MarketplaceMock() {
</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 Link from 'next/link';
@ -8,80 +9,6 @@ export const metadata = pageMetadata({
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 = [
{
q: 'What counts as a tool call?',
@ -93,7 +20,7 @@ const FAQ = [
},
{
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?',

View File

@ -1,15 +1,21 @@
@import 'tailwindcss';
@import "tailwindcss";
@theme {
--color-bg: #0a0a0b;
--color-bg-elevated: #111114;
--color-bg-subtle: #16161a;
/* Elevated surfaces carry a ~1% indigo tint so panels read as part of the
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-muted: #a1a1aa;
--color-fg-subtle: #71717a;
--color-border: #1f1f22;
--color-border-strong: #2a2a2e;
--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-success: #22c55e;
--color-warn: #f59e0b;
@ -39,7 +45,7 @@
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-sans);
font-feature-settings: 'cv11', 'ss01';
font-feature-settings: "cv11", "ss01";
}
::selection {
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 {
/* 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 {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
@ -139,7 +194,8 @@
}
@keyframes pulse-dot {
0%, 100% {
0%,
100% {
opacity: 1;
transform: scale(1);
}

View File

@ -174,9 +174,19 @@ export function HeroStepRotator() {
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">
<span className="mono text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
{current.label}
<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]">
{current.label}
</span>
</span>
<span className="mono text-[10.5px] tracking-[0.16em] text-[--color-accent]">
{current.badge}
@ -189,23 +199,28 @@ export function HeroStepRotator() {
</AnimatePresence>
</div>
{/* Step indicator accent dot is wider + glows so the active step
reads at a glance. Buttons stay clickable so users can jump. */}
<div className="flex items-center gap-2" role="tablist" aria-label="Hero flow steps">
{/* Step indicator the visible dot stays small, but each button
carries generous padding so the effective touch target is 44px.
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) => (
<button
key={s.badge}
type="button"
role="tab"
aria-selected={i === step}
aria-current={i === step}
aria-label={`Jump to ${s.badge}`}
onClick={() => setStep(i)}
className={`h-1.5 rounded-full transition-all duration-300 ${
i === step
? '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]'
}`}
/>
className="flex min-h-11 min-w-11 items-center justify-center"
>
<span
className={`h-1.5 rounded-full transition-all duration-300 ${
i === step
? '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]'
}`}
/>
</button>
))}
</div>
</div>

View File

@ -47,6 +47,9 @@ function formatTime(seconds: number): string {
export function HeroVideo() {
const videoRef = useRef<HTMLVideoElement>(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 [playing, setPlaying] = useState(false);
const [playFailed, setPlayFailed] = useState(false);
@ -69,10 +72,26 @@ export function HeroVideo() {
v.addEventListener('timeupdate', onTime);
v.addEventListener('loadedmetadata', onMeta);
v.addEventListener('durationchange', onMeta);
// Best-effort autoplay attempt — silently fail on browsers that
// block it; the overlay is the user's escape hatch.
v.play().catch(() => undefined);
// Play only while scrolled into view: with preload="metadata" the 2.6 MB
// mp4 stays off the critical path until the section is actually visible,
// 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 () => {
io.disconnect();
v.removeEventListener('play', onPlay);
v.removeEventListener('pause', onPause);
v.removeEventListener('timeupdate', onTime);
@ -82,9 +101,12 @@ export function HeroVideo() {
}, []);
// Clear any pending hide timer on unmount.
useEffect(() => () => {
if (hideTimer.current) clearTimeout(hideTimer.current);
}, []);
useEffect(
() => () => {
if (hideTimer.current) clearTimeout(hideTimer.current);
},
[],
);
const scheduleHide = useCallback(() => {
if (hideTimer.current) clearTimeout(hideTimer.current);
@ -106,6 +128,7 @@ export function HeroVideo() {
if (!v) return;
if (v.paused) {
setPlayFailed(false);
userPausedRef.current = false;
try {
// .load() resets the media element's resource selection — required
// when an earlier autoplay attempt was blocked before the source
@ -116,6 +139,7 @@ export function HeroVideo() {
setPlayFailed(true);
}
} else {
userPausedRef.current = true;
v.pause();
}
}, []);
@ -165,11 +189,10 @@ export function HeroVideo() {
>
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
preload="auto"
preload="metadata"
poster="/videos/hero-poster.jpg"
onClick={onVideoClick}
className="size-full cursor-pointer object-cover"

View File

@ -2,27 +2,52 @@
import { Menu, X } from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
const LINKS = [
{ href: '/#how', label: 'How it works' },
{ href: '/templates', label: 'Templates' },
{ href: '/pricing', label: 'Pricing' },
{ href: '/docs', label: 'Docs' },
{ href: '/guides', label: 'Guides' },
{ href: '/changelog', label: 'Changelog' },
];
/** Hamburger menu shown below the md breakpoint, where the inline nav is hidden. */
export function MarketingMobileMenu() {
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 (
<div className="md:hidden">
<div ref={rootRef} className="md:hidden">
<button
type="button"
aria-label={open ? 'Close menu' : 'Open menu'}
aria-expanded={open}
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} />}
</button>
@ -34,20 +59,27 @@ export function MarketingMobileMenu() {
// 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"
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) => (
<Link
key={l.href}
href={l.href}
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}
</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>
</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',
},
];