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:
parent
cba45402ce
commit
17056d0b30
@ -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]">
|
||||
© {new Date().getFullYear()} BuildMyMCPServer
|
||||
</div>
|
||||
<div>© {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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 · ~10–20s 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?',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
91
apps/web/lib/pricing.ts
Normal 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 · ~10–20s 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',
|
||||
},
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user