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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1">{children}</main>
|
<main className="flex-1">{children}</main>
|
||||||
<footer className="border-t border-[--color-border] py-8">
|
<footer className="border-t border-[--color-border] py-12">
|
||||||
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-6 text-[12px] text-[--color-fg-subtle] md:flex-row md:items-center md:justify-between">
|
<div className="mx-auto max-w-6xl px-6">
|
||||||
|
<div className="grid gap-10 sm:grid-cols-2 md:grid-cols-4">
|
||||||
|
{/* Brand column — positioning line + live status. */}
|
||||||
|
<div className="sm:col-span-2 md:col-span-1">
|
||||||
|
<Logo />
|
||||||
|
<p className="mt-3 max-w-xs text-[12.5px] leading-relaxed text-[--color-fg-subtle]">
|
||||||
|
From a prompt to a hosted, OAuth-protected MCP server — for Claude, Cursor and
|
||||||
|
ChatGPT.
|
||||||
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/status"
|
href="/status"
|
||||||
className="flex items-center gap-2 transition-colors hover:text-[--color-fg]"
|
className="mt-4 flex items-center gap-2 text-[12px] text-[--color-fg-subtle] transition-colors hover:text-[--color-fg]"
|
||||||
>
|
>
|
||||||
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
|
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
|
||||||
<span>System status</span>
|
<span>System status</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-wrap gap-x-5 gap-y-1">
|
|
||||||
<Link href="/docs" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
Docs
|
|
||||||
</Link>
|
|
||||||
<Link href="/contact" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
Contact
|
|
||||||
</Link>
|
|
||||||
<Link href="/security" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
Security
|
|
||||||
</Link>
|
|
||||||
<Link href="/privacy" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
Privacy
|
|
||||||
</Link>
|
|
||||||
<Link href="/agb" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
AGB
|
|
||||||
</Link>
|
|
||||||
<Link href="/impressum" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
Impressum
|
|
||||||
</Link>
|
|
||||||
<Link href="/terms" className="transition-colors hover:text-[--color-fg]">
|
|
||||||
Terms
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div>© {new Date().getFullYear()} BuildMyMCPServer</div>
|
<FooterColumn
|
||||||
|
title="Product"
|
||||||
|
links={[
|
||||||
|
{ href: '/templates', label: 'Templates' },
|
||||||
|
{ href: '/pricing', label: 'Pricing' },
|
||||||
|
{ href: '/changelog', label: 'Changelog' },
|
||||||
|
{ href: '/security', label: 'Security' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<FooterColumn
|
||||||
|
title="Resources"
|
||||||
|
links={[
|
||||||
|
{ href: '/docs', label: 'Docs' },
|
||||||
|
{ href: '/guides', label: 'Guides' },
|
||||||
|
{ href: '/docs/faq', label: 'FAQ' },
|
||||||
|
{ href: '/contact', label: 'Contact' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<FooterColumn
|
||||||
|
title="Legal"
|
||||||
|
links={[
|
||||||
|
{ href: '/privacy', label: 'Privacy' },
|
||||||
|
{ href: '/terms', label: 'Terms' },
|
||||||
|
{ href: '/agb', label: 'AGB' },
|
||||||
|
{ href: '/impressum', label: 'Impressum' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 border-t border-[--color-border] pt-6 text-[12px] text-[--color-fg-subtle]">
|
||||||
|
© {new Date().getFullYear()} BuildMyMCPServer
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FooterColumn({
|
||||||
|
title,
|
||||||
|
links,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
links: { href: string; label: string }[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[--color-fg-muted]">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-3 space-y-2 text-[12.5px] text-[--color-fg-subtle]">
|
||||||
|
{links.map((l) => (
|
||||||
|
<li key={l.href}>
|
||||||
|
<Link href={l.href} className="transition-colors hover:text-[--color-fg]">
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -12,31 +12,11 @@ import { JsonLd } from '@/components/json-ld';
|
|||||||
import { ParticleHero } from '@/components/particle-hero';
|
import { ParticleHero } from '@/components/particle-hero';
|
||||||
import { PulseLink } from '@/components/pulse';
|
import { PulseLink } from '@/components/pulse';
|
||||||
import { ScrollCue } from '@/components/scroll-cue';
|
import { ScrollCue } from '@/components/scroll-cue';
|
||||||
import { StaticCodeBlock } from '@/components/static-code-block';
|
import { TIERS } from '@/lib/pricing';
|
||||||
import { FAQ, faqJsonLd } from '@/lib/seo';
|
import { FAQ, faqJsonLd } from '@/lib/seo';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { Activity, ChevronDown, Container, ShieldCheck } from 'lucide-react';
|
||||||
import type { ComponentType } from 'react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import type { ComponentType } from 'react';
|
||||||
const PROMPT_EXAMPLE = `Create an MCP server that searches our Notion workspace.
|
|
||||||
Tools: search_pages, get_page_content.
|
|
||||||
Auth: NOTION_API_KEY.`;
|
|
||||||
|
|
||||||
const OUTPUT_EXAMPLE = `> Generating spec... OK (2 tools)
|
|
||||||
> Static checks OK
|
|
||||||
> Building image bmm-mcp-notion OK 17.2s
|
|
||||||
> Deploying container OK
|
|
||||||
> Live at https://notion-x9.mcp.buildmymcpserver.com
|
|
||||||
> First request: 401 → token → 200 OK`;
|
|
||||||
|
|
||||||
const INSTALL_SNIPPET = `{
|
|
||||||
"mcpServers": {
|
|
||||||
"notion": {
|
|
||||||
"url": "https://notion-x9.mcp.buildmymcpserver.com/mcp",
|
|
||||||
"auth": "oauth2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
interface ExampleEntry {
|
interface ExampleEntry {
|
||||||
title: string;
|
title: string;
|
||||||
@ -50,41 +30,58 @@ interface ExampleEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EXAMPLES: ExampleEntry[] = [
|
const EXAMPLES: ExampleEntry[] = [
|
||||||
{ title: 'PostgreSQL', desc: 'Read-only access to your tables with schema introspection.', Icon: PostgresIcon, bg: '#336791', fg: '#ffffff' },
|
{
|
||||||
{ title: 'Salesforce', desc: 'Query opportunities, accounts and leads from Claude.', Icon: SalesforceCloudIcon, bg: '#00a1e0', fg: '#ffffff' },
|
title: 'PostgreSQL',
|
||||||
{ title: 'Notion', desc: 'Search pages, read content, append blocks.', Icon: NotionIcon, bg: '#ffffff', fg: '#0a0a0b' },
|
desc: 'Read-only access to your tables with schema introspection.',
|
||||||
{ title: 'GitHub', desc: 'List issues, search code, post comments. Scoped to one repo.', Icon: GitHubIcon, bg: '#181717', fg: '#ffffff' },
|
Icon: PostgresIcon,
|
||||||
{ title: 'Stripe', desc: 'Look up charges, customers, refunds (read-only by default).', Icon: StripeIcon, bg: '#635bff', fg: '#ffffff' },
|
bg: '#336791',
|
||||||
{ title: 'Custom REST',desc: 'Wrap any HTTP API behind one prompt-defined tool surface.', Icon: RestIcon, bg: '#6366f1', fg: '#ffffff' },
|
fg: '#ffffff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Salesforce',
|
||||||
|
desc: 'Query opportunities, accounts and leads from Claude.',
|
||||||
|
Icon: SalesforceCloudIcon,
|
||||||
|
bg: '#00a1e0',
|
||||||
|
fg: '#ffffff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Notion',
|
||||||
|
desc: 'Search pages, read content, append blocks.',
|
||||||
|
Icon: NotionIcon,
|
||||||
|
bg: '#ffffff',
|
||||||
|
fg: '#0a0a0b',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'GitHub',
|
||||||
|
desc: 'List issues, search code, post comments. Scoped to one repo.',
|
||||||
|
Icon: GitHubIcon,
|
||||||
|
bg: '#181717',
|
||||||
|
fg: '#ffffff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Stripe',
|
||||||
|
desc: 'Look up charges, customers, refunds (read-only by default).',
|
||||||
|
Icon: StripeIcon,
|
||||||
|
bg: '#635bff',
|
||||||
|
fg: '#ffffff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Custom REST',
|
||||||
|
desc: 'Wrap any HTTP API behind one prompt-defined tool surface.',
|
||||||
|
Icon: RestIcon,
|
||||||
|
bg: '#6366f1',
|
||||||
|
fg: '#ffffff',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ClientEntry {
|
// Honest marketplace preview data: template names + tool counts only. These
|
||||||
name: string;
|
// mirror the first-party starter templates; no invented fork counts, no
|
||||||
/** Single-character mark for inline visual identity. */
|
// "verified" theatre — the frame is labelled as a preview of the card format.
|
||||||
mark: string;
|
const PREVIEW_TEMPLATES: { name: string; author: string; tools: number }[] = [
|
||||||
}
|
{ name: 'notion-search', author: 'core', tools: 2 },
|
||||||
|
{ name: 'github-issues', author: 'core', tools: 3 },
|
||||||
const CLIENTS: ClientEntry[] = [
|
{ name: 'stripe-readonly', author: 'core', tools: 4 },
|
||||||
{ name: 'Claude Desktop', mark: 'C' },
|
{ name: 'postgres-readonly', author: 'core', tools: 3 },
|
||||||
{ name: 'Cursor', mark: '⌘' },
|
|
||||||
{ name: 'ChatGPT', mark: '✦' },
|
|
||||||
{ name: 'VS Code Copilot', mark: '<>' },
|
|
||||||
{ name: 'Continue.dev', mark: '→' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface MockTemplate {
|
|
||||||
name: string;
|
|
||||||
author: string;
|
|
||||||
tools: number;
|
|
||||||
forks: number;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MOCK_TEMPLATES: MockTemplate[] = [
|
|
||||||
{ name: 'notion-search', author: 'core', tools: 2, forks: 247, verified: true },
|
|
||||||
{ name: 'github-issues', author: 'core', tools: 3, forks: 89, verified: true },
|
|
||||||
{ name: 'stripe-readonly', author: 'core', tools: 4, forks: 156, verified: true },
|
|
||||||
{ name: 'linear-tasks', author: 'community', tools: 5, forks: 34, verified: false },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const MARKETPLACE_POINTS: { t: string; d: string }[] = [
|
const MARKETPLACE_POINTS: { t: string; d: string }[] = [
|
||||||
@ -97,94 +94,104 @@ const MARKETPLACE_POINTS: { t: string; d: string }[] = [
|
|||||||
d: "A template carries the spec and generated code, never the author's API keys. You add your own on fork.",
|
d: "A template carries the spec and generated code, never the author's API keys. You add your own on fork.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: 'Ranked by real usage',
|
t: 'Open from day one',
|
||||||
d: 'Templates rise on fork count and active deploys, not vanity stars. The useful ones surface themselves.',
|
d: 'Publish a server you built and anyone can fork it. The marketplace is young — early templates set the standard.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const TIERS = [
|
// Proof-by-specificity band: three claims a visitor can verify with one
|
||||||
|
// click instead of taking our word for it. Substitute for social proof
|
||||||
|
// until there is social proof.
|
||||||
|
const PROOF_POINTS: {
|
||||||
|
t: string;
|
||||||
|
d: string;
|
||||||
|
href: string;
|
||||||
|
linkLabel: string;
|
||||||
|
Icon: ComponentType<{ size?: number; className?: string }>;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
name: 'Hobby',
|
t: 'OAuth 2.1 authorization server',
|
||||||
price: '€0',
|
d: 'PKCE, Dynamic Client Registration and Resource Indicators (RFC 8707) in front of every server.',
|
||||||
tag: 'Forever free',
|
href: '/docs/oauth',
|
||||||
features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'],
|
linkLabel: 'Read the auth docs',
|
||||||
|
Icon: ShieldCheck,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Pro',
|
t: 'Live system status',
|
||||||
price: '€49',
|
d: 'Uptime and incident history, public. If a build queue slows down, you see it before we tell you.',
|
||||||
tag: '/ month',
|
href: '/status',
|
||||||
features: [
|
linkLabel: 'Check status now',
|
||||||
'5 servers',
|
Icon: Activity,
|
||||||
'1M calls/mo',
|
|
||||||
'Priority build queue',
|
|
||||||
'Custom domain · soon',
|
|
||||||
'Email support',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Team',
|
t: 'Per-server container isolation',
|
||||||
price: '€199',
|
d: 'Every generated server runs in its own Docker container. Secrets AES-256-GCM encrypted at rest.',
|
||||||
tag: '/ month',
|
href: '/security',
|
||||||
features: ['25 servers', '10M calls/mo', 'Audit log', 'RBAC · soon', 'Slack support'],
|
linkLabel: 'Security architecture',
|
||||||
},
|
Icon: Container,
|
||||||
{
|
|
||||||
name: 'Enterprise',
|
|
||||||
price: 'Custom',
|
|
||||||
tag: 'talk to us',
|
|
||||||
features: ['Unlimited', 'Custom infra · on request', 'SSO/SAML · on request', 'Dedicated hosting', 'Customer success'],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PIPELINE_STEPS: { n: string; t: string; d: string }[] = [
|
||||||
|
{
|
||||||
|
n: '01',
|
||||||
|
t: 'Describe your tool',
|
||||||
|
d: 'A sentence is enough. List your secrets and which APIs to call.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: '02',
|
||||||
|
t: 'We generate, check, deploy',
|
||||||
|
d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: '03',
|
||||||
|
t: 'Install in your client',
|
||||||
|
d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Mono shell-comment section kicker: `## how_it_works` */
|
||||||
|
function Kicker({ children }: { children: string }) {
|
||||||
|
return <p className="kicker">## {children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Landing() {
|
export default function Landing() {
|
||||||
|
const teaserTiers = TIERS.filter((t) => t.name === 'Hobby' || t.name === 'Pro');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hero — left: copy + CTAs, right: cycling step-rotator tile.
|
{/* Hero — left: copy + CTAs, right: cycling terminal tile. The WebGL
|
||||||
The old layout stacked three static code blocks vertically; the
|
particle field sits behind at z-0 with pointer-events:none so the
|
||||||
new layout shows one centered tile that cycles through the same
|
CTAs stay interactive. */}
|
||||||
three artifacts (prompt → build.log → claude config) with a
|
|
||||||
mouse-reactive 3D tilt and a step indicator. Shorter overall
|
|
||||||
so the video section below is teased above the fold. */}
|
|
||||||
<section
|
<section
|
||||||
className="relative flex items-center overflow-hidden border-b border-[--color-border]"
|
className="relative flex items-center overflow-hidden border-b border-[--color-border]"
|
||||||
style={{ minHeight: 'calc(100svh - 3rem)' }}
|
style={{ minHeight: 'calc(100svh - 3rem)' }}
|
||||||
>
|
>
|
||||||
{/* WebGL particle field — capability-detected client component.
|
|
||||||
Sits behind the hero content at z-0 with pointer-events:none
|
|
||||||
so the CTAs above remain fully interactive. The canvas listens
|
|
||||||
for pointermove on window itself, so the ring still tracks
|
|
||||||
the cursor through the content above. With the hero now
|
|
||||||
filling the full first-viewport (minus the 48px sticky nav),
|
|
||||||
the field has cinematic-scale room and the indigo radial
|
|
||||||
glow + dot mask read as the dominant background motif. */}
|
|
||||||
<ParticleHero />
|
<ParticleHero />
|
||||||
<div className="relative z-10 mx-auto grid w-full max-w-6xl gap-10 px-6 py-14 sm:py-20 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12">
|
<div className="relative z-10 mx-auto grid w-full max-w-6xl gap-10 px-6 py-14 sm:py-20 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="mono inline-block rounded-full border border-[--color-border] bg-[--color-bg-elevated] px-2.5 py-0.5 text-[11px] tracking-wide text-[--color-fg-muted]">
|
<h1 className="text-balance text-[36px] font-semibold leading-[1.04] tracking-[-0.03em] sm:text-[44px] md:text-[60px]">
|
||||||
v0.1 · updated 2026-05-20
|
|
||||||
</span>
|
|
||||||
<h1 className="mt-6 text-balance text-[30px] font-semibold leading-[1.06] tracking-tight sm:text-[40px] md:text-[52px]">
|
|
||||||
Describe your tool.
|
Describe your tool.
|
||||||
<br />
|
<br />
|
||||||
We host the server.
|
We host the server.
|
||||||
<br />
|
<br />
|
||||||
<span className="text-[--color-fg-muted]">AI uses it.</span>
|
<span className="text-[--color-fg-muted]">AI uses it.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted]">
|
<p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted] sm:text-[16px]">
|
||||||
From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready
|
From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready
|
||||||
for Claude, Cursor and ChatGPT.
|
for Claude, Cursor and ChatGPT.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-7 flex flex-wrap items-center gap-3">
|
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||||
<PulseLink
|
<PulseLink
|
||||||
href="/login"
|
href="/login"
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
|
className="btn-brand inline-flex h-11 items-center justify-center rounded-md px-5 text-[14px] font-medium"
|
||||||
>
|
>
|
||||||
Start building free
|
Start building free →
|
||||||
</PulseLink>
|
</PulseLink>
|
||||||
<PulseLink
|
<PulseLink
|
||||||
href="/docs"
|
href="#flow"
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-4 text-[13px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
className="inline-flex h-11 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-5 text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
||||||
>
|
>
|
||||||
Read the docs
|
See a live build
|
||||||
</PulseLink>
|
</PulseLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-subtle]">
|
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-subtle]">
|
||||||
@ -208,28 +215,17 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/* Scroll cue — fixed at the bottom of the loadscreen rather than
|
|
||||||
inside the hero, so it sits at the natural lower edge of the
|
|
||||||
first viewport regardless of how tall the hero ends up. Fades
|
|
||||||
out once the user has scrolled past the loadscreen. */}
|
|
||||||
<ScrollCue targetId="flow" />
|
<ScrollCue targetId="flow" />
|
||||||
|
|
||||||
{/* Flow video — full-width edge-to-edge under the hero. The clip
|
{/* Flow video — full-width edge-to-edge under the hero. Plays when
|
||||||
shows the real flow (prompt → server schematic → live connection
|
scrolled into view (see hero-video.tsx); preload=metadata keeps the
|
||||||
to Claude Desktop) in three smooth phases. autoplay-muted-loop +
|
2.6 MB mp4 off the critical path on mobile. */}
|
||||||
playsInline satisfies every mobile browser autoplay policy; the
|
|
||||||
`poster` carries first paint while the video decodes. */}
|
|
||||||
<section
|
<section
|
||||||
id="flow"
|
id="flow"
|
||||||
className="relative w-full overflow-hidden border-b border-[--color-border] bg-black"
|
className="relative w-full overflow-hidden border-b border-[--color-border] bg-black"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-video w-full">
|
<div className="relative aspect-video w-full">
|
||||||
{/* HeroVideo: native <video> with autoplay+muted+loop, plus a
|
|
||||||
frosted mute toggle pinned bottom-right so visitors can
|
|
||||||
switch the narration on. See components/hero-video.tsx for
|
|
||||||
the autoplay/unmute mechanics. */}
|
|
||||||
<HeroVideo />
|
<HeroVideo />
|
||||||
{/* Subtle vignette to integrate edges into the rest of the page */}
|
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="pointer-events-none absolute inset-0"
|
className="pointer-events-none absolute inset-0"
|
||||||
@ -241,95 +237,101 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* How it works */}
|
{/* How it works — three pipeline nodes joined by a gradient connector.
|
||||||
|
Desktop: horizontal line behind the numbered nodes. Mobile: the grid
|
||||||
|
stacks and each card keeps its own node, joined by a left rail. */}
|
||||||
<section id="how" className="border-b border-[--color-border] py-14 sm:py-20">
|
<section id="how" className="border-b border-[--color-border] py-14 sm:py-20">
|
||||||
<div className="mx-auto max-w-6xl px-6">
|
<div className="mx-auto max-w-6xl px-6">
|
||||||
<div className="mb-10 max-w-2xl">
|
<div className="mb-10 max-w-2xl">
|
||||||
<h2 className="text-[28px] font-semibold tracking-tight">How it works</h2>
|
<Kicker>how_it_works</Kicker>
|
||||||
<p className="mt-2 text-[14px] text-[--color-fg-muted]">
|
<h2 className="mt-3 text-[28px] font-semibold tracking-tight sm:text-[32px]">
|
||||||
Three steps. No JSON to write, no Docker to manage.
|
Three steps. No JSON to write, no Docker to manage.
|
||||||
</p>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The same video used to live here; it now has its own
|
<div className="relative">
|
||||||
full-width section directly under the hero so it's teased
|
{/* Gradient connector — desktop only, sits behind the node row. */}
|
||||||
above the fold and gets edge-to-edge real estate. This
|
<div
|
||||||
section keeps the three explanatory cards as supporting
|
aria-hidden
|
||||||
copy under the video. */}
|
className="absolute left-0 right-0 top-[22px] hidden h-px md:block"
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
style={{ background: 'var(--gradient-brand)', opacity: 0.35 }}
|
||||||
{[
|
/>
|
||||||
{
|
{/* Mobile left rail joining the stacked nodes. */}
|
||||||
n: '01',
|
<div
|
||||||
t: 'Describe your tool',
|
aria-hidden
|
||||||
d: 'A sentence is enough. List your secrets and which APIs to call.',
|
className="absolute bottom-6 left-[22px] top-[22px] w-px md:hidden"
|
||||||
},
|
style={{ background: 'var(--gradient-brand)', opacity: 0.35 }}
|
||||||
{
|
/>
|
||||||
n: '02',
|
<div className="grid gap-8 md:grid-cols-3 md:gap-6">
|
||||||
t: 'We generate, check, deploy',
|
{PIPELINE_STEPS.map((s) => (
|
||||||
d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.',
|
<div key={s.n} className="relative flex gap-4 md:block">
|
||||||
},
|
<div
|
||||||
{
|
className="mono relative z-10 flex size-11 shrink-0 items-center justify-center rounded-full border border-[--color-border-strong] text-[12px] tracking-widest text-[--color-fg]"
|
||||||
n: '03',
|
style={{
|
||||||
t: 'Install in your client',
|
background: 'var(--color-bg)',
|
||||||
d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.',
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05)',
|
||||||
},
|
}}
|
||||||
].map((s) => (
|
>
|
||||||
<div key={s.n} className="panel p-5">
|
|
||||||
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">
|
|
||||||
{s.n}
|
{s.n}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-4 text-[15px] font-semibold tracking-tight">{s.t}</h3>
|
<div className="min-w-0 md:mt-5">
|
||||||
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">{s.d}</p>
|
<h3 className="text-[15px] font-semibold tracking-tight">{s.t}</h3>
|
||||||
</div>
|
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">
|
||||||
))}
|
{s.d}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ── Clients · trust-signal row ─────────────────────────────────
|
|
||||||
Archetype: typographic logo row. No panels, no bullets, just
|
|
||||||
well-spaced word-marks with a small symbolic mark. The whole
|
|
||||||
section breathes — generous py — so it reads as a moment of
|
|
||||||
trust, not a feature card. */}
|
|
||||||
<section className="border-b border-[--color-border] bg-[--color-bg] py-20 sm:py-24">
|
|
||||||
<div className="mx-auto max-w-6xl px-6">
|
|
||||||
<p className="text-center text-[11px] uppercase tracking-[0.24em] text-[--color-fg-subtle]">
|
|
||||||
Connects everywhere your AI lives
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-14 gap-y-7 sm:gap-x-20">
|
</div>
|
||||||
{CLIENTS.map((c) => (
|
|
||||||
<div
|
|
||||||
key={c.name}
|
|
||||||
className="group flex items-center gap-3 transition-colors"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className="mono flex size-7 items-center justify-center rounded-md border border-[--color-border] text-[12px] text-[--color-fg-muted] transition-colors group-hover:border-[--color-accent] group-hover:text-[--color-accent]"
|
|
||||||
>
|
|
||||||
{c.mark}
|
|
||||||
</span>
|
|
||||||
<span className="text-[17px] font-medium tracking-tight text-[--color-fg-muted] transition-colors group-hover:text-[--color-fg]">
|
|
||||||
{c.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── Examples · integration grid with brand-coloured marks ──────
|
{/* Proof by specificity — three verifiable claims, each one click from
|
||||||
Archetype: 2-col header (title left, supporting copy right) +
|
its evidence. Replaces the old pseudo-logo row: no invented marks,
|
||||||
asymmetric 3-col card grid where each card carries a coloured
|
just a plain-text compatibility line. */}
|
||||||
square brand mark for the service. The marks give each card
|
<section className="border-b border-[--color-border] py-16 sm:py-24">
|
||||||
its own visual identity, breaking the "every card looks the
|
<div className="mx-auto max-w-6xl px-6">
|
||||||
same" pattern of the previous version. */}
|
<Kicker>verify_it_yourself</Kicker>
|
||||||
|
<h2 className="mt-3 max-w-2xl text-[28px] font-semibold leading-[1.1] tracking-tight sm:text-[32px]">
|
||||||
|
No testimonials yet — we're new.
|
||||||
|
<br />
|
||||||
|
<span className="text-[--color-fg-muted]">So check the claims instead.</span>
|
||||||
|
</h2>
|
||||||
|
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
||||||
|
{PROOF_POINTS.map((p) => {
|
||||||
|
const Icon = p.Icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={p.t}
|
||||||
|
href={p.href}
|
||||||
|
className="panel-raised group flex flex-col p-5 transition-colors hover:border-[--color-border-strong]"
|
||||||
|
>
|
||||||
|
<Icon size={20} className="text-[--color-accent]" />
|
||||||
|
<h3 className="mt-4 text-[15px] font-semibold tracking-tight">{p.t}</h3>
|
||||||
|
<p className="mt-2 flex-1 text-[13px] leading-relaxed text-[--color-fg-muted]">
|
||||||
|
{p.d}
|
||||||
|
</p>
|
||||||
|
<span className="mt-4 text-[13px] font-medium text-[--color-accent] transition-colors group-hover:text-[--color-fg]">
|
||||||
|
{p.linkLabel} →
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="mt-10 text-center text-[13px] text-[--color-fg-subtle]">
|
||||||
|
Works with Claude Desktop, Cursor, ChatGPT, VS Code Copilot and Continue.dev — anything
|
||||||
|
that speaks MCP.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Use cases — brand-coloured integration grid. */}
|
||||||
<section className="border-b border-[--color-border] py-20 sm:py-28">
|
<section className="border-b border-[--color-border] py-20 sm:py-28">
|
||||||
<div className="mx-auto max-w-6xl px-6">
|
<div className="mx-auto max-w-6xl px-6">
|
||||||
<div className="mb-12 grid gap-6 md:grid-cols-[1fr_auto] md:items-end md:gap-12">
|
<div className="mb-12 grid gap-6 md:grid-cols-[1fr_auto] md:items-end md:gap-12">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]">
|
<Kicker>use_cases</Kicker>
|
||||||
Use cases
|
|
||||||
</p>
|
|
||||||
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
|
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
|
||||||
Wrap any HTTP API.
|
Wrap any HTTP API.
|
||||||
<br />
|
<br />
|
||||||
@ -337,7 +339,7 @@ export default function Landing() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]">
|
<p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||||
Real integrations our customers ship today. Built from one prompt each.
|
Ship integrations like these from one prompt each.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -347,7 +349,7 @@ export default function Landing() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={e.title}
|
key={e.title}
|
||||||
className="group flex items-start gap-4 rounded-xl border border-[--color-border] bg-[--color-bg-elevated] p-5 transition-colors hover:border-[--color-border-strong]"
|
className="panel-raised group flex items-start gap-4 p-5 transition-colors hover:border-[--color-border-strong]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
@ -372,27 +374,20 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── Marketplace · two-column with product preview ──────────────
|
{/* Marketplace — split layout: selling points left, honest preview
|
||||||
Archetype: split layout. Left column carries the headline,
|
frame right. */}
|
||||||
three bullet-form selling points, and the CTA. Right column
|
|
||||||
carries a mock browser frame containing real-looking template
|
|
||||||
cards — so the visitor SEES the marketplace, not just reads
|
|
||||||
a description of it. */}
|
|
||||||
<section className="border-b border-[--color-border] py-20 sm:py-28">
|
<section className="border-b border-[--color-border] py-20 sm:py-28">
|
||||||
<div className="mx-auto grid max-w-6xl gap-14 px-6 md:grid-cols-[5fr_6fr] md:items-center md:gap-16">
|
<div className="mx-auto grid max-w-6xl gap-14 px-6 md:grid-cols-[5fr_6fr] md:items-center md:gap-16">
|
||||||
{/* Left — text + points + CTA */}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]">
|
<Kicker>marketplace</Kicker>
|
||||||
Marketplace
|
|
||||||
</p>
|
|
||||||
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
|
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
|
||||||
Skip the prompt.
|
Skip the prompt.
|
||||||
<br />
|
<br />
|
||||||
<span className="text-[--color-fg-muted]">Fork what works.</span>
|
<span className="text-[--color-fg-muted]">Fork what works.</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-5 max-w-md text-[14px] leading-relaxed text-[--color-fg-muted]">
|
<p className="mt-5 max-w-md text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||||
The marketplace is a library of working MCP servers the community already built.
|
The marketplace is a library of working MCP servers. Fork one, paste your credentials,
|
||||||
Fork one, paste your credentials, deploy. Or publish yours and let others build on it.
|
deploy. Or publish yours and let others build on it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-8 space-y-5">
|
<ul className="mt-8 space-y-5">
|
||||||
@ -414,29 +409,22 @@ export default function Landing() {
|
|||||||
|
|
||||||
<PulseLink
|
<PulseLink
|
||||||
href="/templates"
|
href="/templates"
|
||||||
className="mt-8 inline-flex h-9 items-center gap-2 rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
|
className="btn-brand mt-8 inline-flex h-11 items-center gap-2 rounded-md px-5 text-[14px] font-medium"
|
||||||
>
|
>
|
||||||
Browse the marketplace →
|
Browse the marketplace →
|
||||||
</PulseLink>
|
</PulseLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right — mock marketplace browser frame */}
|
<MarketplacePreview />
|
||||||
<MarketplaceMock />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── Pricing · 4 cards with Pro highlighted as recommended ─────
|
{/* Pricing teaser — Hobby + Pro only; the full matrix lives on
|
||||||
Archetype: centred header + 4-card row where the Pro tier is
|
/pricing. Data is shared via lib/pricing.ts so it can't drift. */}
|
||||||
visually distinct (accent border, indigo glow shadow, floating
|
|
||||||
"recommended" pill, slightly elevated). Other tiers stay calm
|
|
||||||
so the eye lands on Pro first. Big price typography (text-
|
|
||||||
[40px]) replaces the previous flat 26px. */}
|
|
||||||
<section id="pricing" className="border-b border-[--color-border] py-20 sm:py-28">
|
<section id="pricing" className="border-b border-[--color-border] py-20 sm:py-28">
|
||||||
<div className="mx-auto max-w-6xl px-6">
|
<div className="mx-auto max-w-4xl px-6">
|
||||||
<div className="mb-14 text-center">
|
<div className="mb-12 text-center">
|
||||||
<p className="text-[11px] uppercase tracking-[0.22em] text-[--color-fg-subtle]">
|
<Kicker>pricing</Kicker>
|
||||||
Pricing
|
|
||||||
</p>
|
|
||||||
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
|
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
|
||||||
Pay for tool calls.
|
Pay for tool calls.
|
||||||
<br />
|
<br />
|
||||||
@ -444,16 +432,16 @@ export default function Landing() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-5 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-5 sm:grid-cols-2">
|
||||||
{TIERS.map((t) => {
|
{teaserTiers.map((t) => {
|
||||||
const featured = t.name === 'Pro';
|
const featured = Boolean(t.highlight);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={t.name}
|
key={t.name}
|
||||||
className={`relative flex flex-col gap-6 rounded-xl border p-6 transition-colors ${
|
className={`relative flex flex-col gap-6 rounded-xl border p-6 ${
|
||||||
featured
|
featured
|
||||||
? 'border-[--color-accent] bg-[--color-bg-elevated]'
|
? 'border-[--color-accent] bg-[--color-bg-elevated]'
|
||||||
: 'border-[--color-border] bg-[--color-bg-elevated] hover:border-[--color-border-strong]'
|
: 'border-[--color-border] bg-[--color-bg-elevated]'
|
||||||
}`}
|
}`}
|
||||||
style={
|
style={
|
||||||
featured
|
featured
|
||||||
@ -461,7 +449,7 @@ export default function Landing() {
|
|||||||
boxShadow:
|
boxShadow:
|
||||||
'0 0 0 4px rgba(99, 102, 241, 0.12), 0 24px 50px rgba(0, 0, 0, 0.35)',
|
'0 0 0 4px rgba(99, 102, 241, 0.12), 0 24px 50px rgba(0, 0, 0, 0.35)',
|
||||||
}
|
}
|
||||||
: undefined
|
: { boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04)' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{featured && (
|
{featured && (
|
||||||
@ -483,6 +471,9 @@ export default function Landing() {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
|
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-3 text-[13px] leading-relaxed text-[--color-fg-muted]">
|
||||||
|
{t.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="flex flex-1 flex-col gap-2.5 border-t border-[--color-border] pt-5 text-[13px] text-[--color-fg-muted]">
|
<ul className="flex flex-1 flex-col gap-2.5 border-t border-[--color-border] pt-5 text-[13px] text-[--color-fg-muted]">
|
||||||
@ -498,23 +489,42 @@ export default function Landing() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={t.href}
|
||||||
|
className={`inline-flex h-10 items-center justify-center rounded-md px-4 text-[13px] font-medium transition-colors ${
|
||||||
|
featured
|
||||||
|
? 'btn-brand'
|
||||||
|
: 'border border-[--color-border] bg-[--color-bg-subtle] text-[--color-fg] hover:border-[--color-border-strong]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.cta}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center">
|
||||||
|
<Link
|
||||||
|
href="/pricing"
|
||||||
|
className="text-[14px] font-medium text-[--color-accent] transition-colors hover:text-[--color-fg]"
|
||||||
|
>
|
||||||
|
See all plans — Team, Enterprise →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* FAQ — collapsible accordion using native <details>. Crawlers
|
{/* FAQ — collapsible accordion using native <details>. Crawlers and
|
||||||
and screen readers still see the full Q+A in the HTML; users
|
screen readers see the full Q+A in the HTML. */}
|
||||||
see one question at a time and expand on demand. No JS, no
|
<section className="border-b border-[--color-border] py-14 sm:py-20">
|
||||||
state, semantically correct. `list-none` + the WebKit-marker
|
|
||||||
pseudo-class suppress the default disclosure triangle so we
|
|
||||||
can render our own chevron that rotates via `group-open`. */}
|
|
||||||
<section className="py-14 sm:py-20">
|
|
||||||
<JsonLd data={faqJsonLd()} />
|
<JsonLd data={faqJsonLd()} />
|
||||||
<div className="mx-auto max-w-3xl px-6">
|
<div className="mx-auto max-w-3xl px-6">
|
||||||
<h2 className="text-[28px] font-semibold tracking-tight">FAQ</h2>
|
<Kicker>faq</Kicker>
|
||||||
|
<h2 className="mt-3 text-[28px] font-semibold tracking-tight">
|
||||||
|
Questions, answered straight.
|
||||||
|
</h2>
|
||||||
<div className="mt-8 border-t border-[--color-border]">
|
<div className="mt-8 border-t border-[--color-border]">
|
||||||
{FAQ.map((f) => (
|
{FAQ.map((f) => (
|
||||||
<details
|
<details
|
||||||
@ -536,28 +546,58 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Final CTA — the page must not end on FAQ. Gradient top border keeps
|
||||||
|
the brand mark scarce but present at the exit point. */}
|
||||||
|
<section className="relative py-20 sm:py-28">
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-x-0 top-0 h-px"
|
||||||
|
style={{ background: 'var(--gradient-brand)', opacity: 0.6 }}
|
||||||
|
/>
|
||||||
|
<div className="mx-auto max-w-3xl px-6 text-center">
|
||||||
|
<h2 className="text-balance text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[44px]">
|
||||||
|
Your first server is <span className="text-brand-gradient">one prompt away</span>.
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mt-4 max-w-md text-[14.5px] leading-relaxed text-[--color-fg-muted]">
|
||||||
|
Free tier, full source export, no lock-in. If it doesn't work for you, take the
|
||||||
|
TypeScript and leave.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||||
|
<PulseLink
|
||||||
|
href="/login"
|
||||||
|
className="btn-brand inline-flex h-11 items-center justify-center rounded-md px-6 text-[14px] font-medium"
|
||||||
|
>
|
||||||
|
Start building free →
|
||||||
|
</PulseLink>
|
||||||
|
<PulseLink
|
||||||
|
href="/docs"
|
||||||
|
className="inline-flex h-11 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-6 text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
||||||
|
>
|
||||||
|
Read the docs
|
||||||
|
</PulseLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock marketplace preview.
|
* Honest marketplace preview.
|
||||||
*
|
*
|
||||||
* Static, server-rendered. Renders a faux-browser frame containing four
|
* Static, server-rendered browser frame showing what a template card looks
|
||||||
* realistic template cards so the visitor sees what the marketplace
|
* like. Card data mirrors the first-party starter templates — names, author
|
||||||
* actually looks like rather than reading marketing copy about it.
|
* and tool counts only. No fork counts, no "verified" badges: the frame is
|
||||||
*
|
* explicitly labelled a preview, not live marketplace traffic.
|
||||||
* The card data lives in `MOCK_TEMPLATES` at the top of this file —
|
|
||||||
* those are real template-flavoured names, modest fork counts, and the
|
|
||||||
* `core` / `community` author tag. Numbers are deliberately small and
|
|
||||||
* truthful-looking for a young marketplace; nothing here pretends to be
|
|
||||||
* GitHub-trending traffic.
|
|
||||||
*/
|
*/
|
||||||
function MarketplaceMock() {
|
function MarketplacePreview() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="overflow-hidden rounded-xl border border-[--color-border-strong] bg-[--color-bg-elevated]"
|
className="overflow-hidden rounded-xl border border-[--color-border-strong] bg-[--color-bg-elevated]"
|
||||||
style={{ boxShadow: '0 24px 60px rgba(0, 0, 0, 0.45)' }}
|
style={{
|
||||||
|
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.04), 0 24px 60px rgba(0, 0, 0, 0.45)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Browser chrome */}
|
{/* Browser chrome */}
|
||||||
<div className="flex items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3">
|
<div className="flex items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3">
|
||||||
@ -573,27 +613,21 @@ function MarketplaceMock() {
|
|||||||
|
|
||||||
{/* Toolbar inside the page chrome */}
|
{/* Toolbar inside the page chrome */}
|
||||||
<div className="flex items-center justify-between gap-4 border-b border-[--color-border] px-5 py-3">
|
<div className="flex items-center justify-between gap-4 border-b border-[--color-border] px-5 py-3">
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-[13px] font-semibold tracking-tight text-[--color-fg]">
|
<span className="text-[13px] font-semibold tracking-tight text-[--color-fg]">
|
||||||
Templates
|
Templates
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-[--color-fg-subtle]">·</span>
|
<span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
|
||||||
<span className="text-[11px] text-[--color-fg-subtle]">{MOCK_TEMPLATES.length} live</span>
|
preview
|
||||||
</div>
|
</span>
|
||||||
<div className="hidden items-center gap-1.5 rounded-md border border-[--color-border] px-2 py-1 sm:flex">
|
|
||||||
<span className="text-[11px] text-[--color-fg-subtle]">trending</span>
|
|
||||||
<ChevronDown size={11} className="text-[--color-fg-subtle]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Template cards grid */}
|
{/* Template cards grid */}
|
||||||
<div className="grid gap-3 p-4 sm:grid-cols-2">
|
<div className="grid gap-3 p-4 sm:grid-cols-2">
|
||||||
{MOCK_TEMPLATES.map((t) => (
|
{PREVIEW_TEMPLATES.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t.name}
|
key={t.name}
|
||||||
className="rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-4 transition-colors hover:border-[--color-accent]/40"
|
className="rounded-md border border-[--color-border] bg-[--color-bg-subtle] p-4 transition-colors hover:border-[--color-accent]/40"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="mono truncate text-[13px] font-semibold tracking-tight text-[--color-fg]">
|
<div className="mono truncate text-[13px] font-semibold tracking-tight text-[--color-fg]">
|
||||||
{t.name}
|
{t.name}
|
||||||
@ -602,21 +636,14 @@ function MarketplaceMock() {
|
|||||||
<span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
|
<span className="mono text-[10px] uppercase tracking-wider text-[--color-fg-subtle]">
|
||||||
{t.author}
|
{t.author}
|
||||||
</span>
|
</span>
|
||||||
{t.verified && (
|
<span className="inline-flex items-center rounded-full border border-[--color-border-strong] px-1.5 py-px text-[9.5px] font-medium uppercase tracking-wider text-[--color-fg-subtle]">
|
||||||
<span className="inline-flex items-center gap-0.5 rounded-full border border-[--color-accent]/50 bg-[--color-accent]/10 px-1.5 py-px text-[9.5px] font-medium uppercase tracking-wider text-[--color-accent]">
|
template
|
||||||
✓ verified
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center justify-between text-[11px] text-[--color-fg-subtle]">
|
<div className="mt-4 text-[11px] text-[--color-fg-subtle]">
|
||||||
<span className="mono">{t.tools} tools</span>
|
<span className="mono">{t.tools} tools</span>
|
||||||
<span className="mono inline-flex items-center gap-1">
|
|
||||||
<ForkGlyph />
|
|
||||||
{t.forks}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -624,20 +651,3 @@ function MarketplaceMock() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForkGlyph() {
|
|
||||||
return (
|
|
||||||
<svg width="11" height="11" viewBox="0 0 14 14" fill="none" aria-hidden>
|
|
||||||
<circle cx="3.5" cy="3" r="1.4" stroke="currentColor" strokeWidth={1.2} />
|
|
||||||
<circle cx="10.5" cy="3" r="1.4" stroke="currentColor" strokeWidth={1.2} />
|
|
||||||
<circle cx="7" cy="11" r="1.4" stroke="currentColor" strokeWidth={1.2} />
|
|
||||||
<path
|
|
||||||
d="M 3.5 4.5 L 3.5 6.5 Q 3.5 7.5 4.5 7.5 L 9.5 7.5 Q 10.5 7.5 10.5 6.5 L 10.5 4.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.2}
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
<path d="M 7 7.5 L 7 9.5" stroke="currentColor" strokeWidth={1.2} />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { TIERS } from '@/lib/pricing';
|
||||||
import { pageMetadata } from '@/lib/seo';
|
import { pageMetadata } from '@/lib/seo';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@ -8,80 +9,6 @@ export const metadata = pageMetadata({
|
|||||||
path: '/pricing',
|
path: '/pricing',
|
||||||
});
|
});
|
||||||
|
|
||||||
const TIERS = [
|
|
||||||
{
|
|
||||||
name: 'Hobby',
|
|
||||||
price: '€0',
|
|
||||||
tag: 'Forever free',
|
|
||||||
description: 'For trying things out and shipping single-user tools.',
|
|
||||||
model: 'Open-tier AI',
|
|
||||||
modelDetail: 'Free-tier model · ~30-60s analyze',
|
|
||||||
features: [
|
|
||||||
'1 MCP server',
|
|
||||||
'100,000 tool calls / month',
|
|
||||||
'5 prompt analyses / day',
|
|
||||||
'BuildMyMCP subdomain',
|
|
||||||
'Community support',
|
|
||||||
],
|
|
||||||
cta: 'Start free',
|
|
||||||
href: '/login',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Pro',
|
|
||||||
price: '€49',
|
|
||||||
tag: '/ month',
|
|
||||||
description: 'For solo founders and small teams shipping production tools.',
|
|
||||||
model: 'Claude AI',
|
|
||||||
modelDetail: 'Powered by Anthropic Claude · ~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 = [
|
const FAQ = [
|
||||||
{
|
{
|
||||||
q: 'What counts as a tool call?',
|
q: 'What counts as a tool call?',
|
||||||
@ -93,7 +20,7 @@ const FAQ = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: 'Annual billing?',
|
q: 'Annual billing?',
|
||||||
a: 'Yes — save 20% on Pro and Team paying annually. Enterprise is annual by default.',
|
a: 'Annual plans are coming — contact us for annual invoicing today. Enterprise is annual by default.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: 'Plan changes?',
|
q: 'Plan changes?',
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-bg: #0a0a0b;
|
--color-bg: #0a0a0b;
|
||||||
--color-bg-elevated: #111114;
|
/* Elevated surfaces carry a ~1% indigo tint so panels read as part of the
|
||||||
--color-bg-subtle: #16161a;
|
brand instead of flat gray. Kept close enough to the old #111114/#16161a
|
||||||
|
that dashboard contrast ratios are unaffected. */
|
||||||
|
--color-bg-elevated: #121218;
|
||||||
|
--color-bg-subtle: #17171d;
|
||||||
--color-fg: #fafafa;
|
--color-fg: #fafafa;
|
||||||
--color-fg-muted: #a1a1aa;
|
--color-fg-muted: #a1a1aa;
|
||||||
--color-fg-subtle: #71717a;
|
--color-fg-subtle: #71717a;
|
||||||
--color-border: #1f1f22;
|
--color-border: #1f1f22;
|
||||||
--color-border-strong: #2a2a2e;
|
--color-border-strong: #2a2a2e;
|
||||||
--color-accent: #6366f1;
|
--color-accent: #6366f1;
|
||||||
|
/* Secondary brand hue — cyan endpoint of the identity gradient. Reserved
|
||||||
|
for the gradient, kickers and "live" accents; never a surface color. */
|
||||||
|
--color-accent-2: #22d3ee;
|
||||||
--color-accent-fg: #ffffff;
|
--color-accent-fg: #ffffff;
|
||||||
--color-success: #22c55e;
|
--color-success: #22c55e;
|
||||||
--color-warn: #f59e0b;
|
--color-warn: #f59e0b;
|
||||||
@ -39,7 +45,7 @@
|
|||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
color: var(--color-fg);
|
color: var(--color-fg);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-feature-settings: 'cv11', 'ss01';
|
font-feature-settings: "cv11", "ss01";
|
||||||
}
|
}
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(99, 102, 241, 0.3);
|
background: rgba(99, 102, 241, 0.3);
|
||||||
@ -100,7 +106,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Identity gradient — indigo → cyan. Defined outside @theme (it's not a
|
||||||
|
Tailwind token, just a shared value) and used ONLY for: primary CTAs,
|
||||||
|
section kickers, the featured pricing card and one hero glow. Scarcity is
|
||||||
|
what keeps it feeling like a brand mark instead of decoration. */
|
||||||
|
:root {
|
||||||
|
--gradient-brand: linear-gradient(135deg, #6366f1 0%, #22d3ee 100%);
|
||||||
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
/* Gradient-filled primary CTA. A dark inner overlay on hover (instead of a
|
||||||
|
second gradient) keeps the transition GPU-cheap. */
|
||||||
|
.btn-brand {
|
||||||
|
position: relative;
|
||||||
|
background: var(--gradient-brand);
|
||||||
|
color: #ffffff;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
.btn-brand::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: rgba(10, 10, 11, 0);
|
||||||
|
transition: background-color 0.2s ease-out;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
.btn-brand:hover::after {
|
||||||
|
background: rgba(10, 10, 11, 0.18);
|
||||||
|
}
|
||||||
|
/* Gradient text for the rare display moment (final CTA heading). */
|
||||||
|
.text-brand-gradient {
|
||||||
|
background: var(--gradient-brand);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
/* Mono section kicker, shell-comment style: `## how_it_works` */
|
||||||
|
.kicker {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
/* Elevated panel with an inner top highlight — reads as machined edge. */
|
||||||
|
.panel-raised {
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--color-bg-elevated);
|
background: var(--color-bg-elevated);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@ -139,7 +194,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-dot {
|
@keyframes pulse-dot {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,10 +174,20 @@ export function HeroStepRotator() {
|
|||||||
y: glowDy,
|
y: glowDy,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Terminal chrome: traffic lights + filename tab. The tile IS
|
||||||
|
the product's aesthetic (build logs, configs), so it should
|
||||||
|
look like a real terminal window, not a generic card. */}
|
||||||
<div className="relative flex items-center justify-between border-b border-[--color-border] px-4 py-2.5">
|
<div className="relative flex items-center justify-between border-b border-[--color-border] px-4 py-2.5">
|
||||||
|
<span className="flex items-center gap-3">
|
||||||
|
<span aria-hidden className="flex gap-1.5">
|
||||||
|
<span className="size-2.5 rounded-full bg-[#ff5f57]/80" />
|
||||||
|
<span className="size-2.5 rounded-full bg-[#febc2e]/80" />
|
||||||
|
<span className="size-2.5 rounded-full bg-[#28c840]/80" />
|
||||||
|
</span>
|
||||||
<span className="mono text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
|
<span className="mono text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
|
||||||
{current.label}
|
{current.label}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
<span className="mono text-[10.5px] tracking-[0.16em] text-[--color-accent]">
|
<span className="mono text-[10.5px] tracking-[0.16em] text-[--color-accent]">
|
||||||
{current.badge}
|
{current.badge}
|
||||||
</span>
|
</span>
|
||||||
@ -189,23 +199,28 @@ export function HeroStepRotator() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step indicator — accent dot is wider + glows so the active step
|
{/* Step indicator — the visible dot stays small, but each button
|
||||||
reads at a glance. Buttons stay clickable so users can jump. */}
|
carries generous padding so the effective touch target is ≥44px.
|
||||||
<div className="flex items-center gap-2" role="tablist" aria-label="Hero flow steps">
|
Plain buttons with aria-current (not a tablist — no arrow-key
|
||||||
|
semantics to honour, they're simple jump buttons). */}
|
||||||
|
<div className="flex items-center" aria-label="Hero flow steps">
|
||||||
{STEPS.map((s, i) => (
|
{STEPS.map((s, i) => (
|
||||||
<button
|
<button
|
||||||
key={s.badge}
|
key={s.badge}
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
aria-current={i === step}
|
||||||
aria-selected={i === step}
|
|
||||||
aria-label={`Jump to ${s.badge}`}
|
aria-label={`Jump to ${s.badge}`}
|
||||||
onClick={() => setStep(i)}
|
onClick={() => setStep(i)}
|
||||||
|
className="flex min-h-11 min-w-11 items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
className={`h-1.5 rounded-full transition-all duration-300 ${
|
className={`h-1.5 rounded-full transition-all duration-300 ${
|
||||||
i === step
|
i === step
|
||||||
? 'w-9 bg-[--color-accent] shadow-[0_0_10px_rgba(99,102,241,0.65)]'
|
? 'w-9 bg-[--color-accent] shadow-[0_0_10px_rgba(99,102,241,0.65)]'
|
||||||
: 'w-1.5 bg-[--color-border-strong] hover:bg-[--color-fg-subtle]'
|
: 'w-1.5 bg-[--color-border-strong] hover:bg-[--color-fg-subtle]'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -47,6 +47,9 @@ function formatTime(seconds: number): string {
|
|||||||
export function HeroVideo() {
|
export function HeroVideo() {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
// Set when the user explicitly pauses — the IntersectionObserver must not
|
||||||
|
// restart a video the user chose to stop.
|
||||||
|
const userPausedRef = useRef(false);
|
||||||
const [muted, setMuted] = useState(true);
|
const [muted, setMuted] = useState(true);
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
const [playFailed, setPlayFailed] = useState(false);
|
const [playFailed, setPlayFailed] = useState(false);
|
||||||
@ -69,10 +72,26 @@ export function HeroVideo() {
|
|||||||
v.addEventListener('timeupdate', onTime);
|
v.addEventListener('timeupdate', onTime);
|
||||||
v.addEventListener('loadedmetadata', onMeta);
|
v.addEventListener('loadedmetadata', onMeta);
|
||||||
v.addEventListener('durationchange', onMeta);
|
v.addEventListener('durationchange', onMeta);
|
||||||
// Best-effort autoplay attempt — silently fail on browsers that
|
// Play only while scrolled into view: with preload="metadata" the 2.6 MB
|
||||||
// block it; the overlay is the user's escape hatch.
|
// mp4 stays off the critical path until the section is actually visible,
|
||||||
v.play().catch(() => undefined);
|
// and playback pauses again off-screen (saves battery + decode). The
|
||||||
|
// .play() attempt is still best-effort — browsers that block autoplay
|
||||||
|
// leave the overlay as the user's escape hatch. `userPaused` tracks an
|
||||||
|
// explicit pause so the observer doesn't fight the user's choice.
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (!entry) return;
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
if (!userPausedRef.current && v.paused) v.play().catch(() => undefined);
|
||||||
|
} else if (!v.paused) {
|
||||||
|
v.pause();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.35 },
|
||||||
|
);
|
||||||
|
io.observe(v);
|
||||||
return () => {
|
return () => {
|
||||||
|
io.disconnect();
|
||||||
v.removeEventListener('play', onPlay);
|
v.removeEventListener('play', onPlay);
|
||||||
v.removeEventListener('pause', onPause);
|
v.removeEventListener('pause', onPause);
|
||||||
v.removeEventListener('timeupdate', onTime);
|
v.removeEventListener('timeupdate', onTime);
|
||||||
@ -82,9 +101,12 @@ export function HeroVideo() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Clear any pending hide timer on unmount.
|
// Clear any pending hide timer on unmount.
|
||||||
useEffect(() => () => {
|
useEffect(
|
||||||
|
() => () => {
|
||||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const scheduleHide = useCallback(() => {
|
const scheduleHide = useCallback(() => {
|
||||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||||
@ -106,6 +128,7 @@ export function HeroVideo() {
|
|||||||
if (!v) return;
|
if (!v) return;
|
||||||
if (v.paused) {
|
if (v.paused) {
|
||||||
setPlayFailed(false);
|
setPlayFailed(false);
|
||||||
|
userPausedRef.current = false;
|
||||||
try {
|
try {
|
||||||
// .load() resets the media element's resource selection — required
|
// .load() resets the media element's resource selection — required
|
||||||
// when an earlier autoplay attempt was blocked before the source
|
// when an earlier autoplay attempt was blocked before the source
|
||||||
@ -116,6 +139,7 @@ export function HeroVideo() {
|
|||||||
setPlayFailed(true);
|
setPlayFailed(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
userPausedRef.current = true;
|
||||||
v.pause();
|
v.pause();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -165,11 +189,10 @@ export function HeroVideo() {
|
|||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
autoPlay
|
|
||||||
muted
|
muted
|
||||||
loop
|
loop
|
||||||
playsInline
|
playsInline
|
||||||
preload="auto"
|
preload="metadata"
|
||||||
poster="/videos/hero-poster.jpg"
|
poster="/videos/hero-poster.jpg"
|
||||||
onClick={onVideoClick}
|
onClick={onVideoClick}
|
||||||
className="size-full cursor-pointer object-cover"
|
className="size-full cursor-pointer object-cover"
|
||||||
|
|||||||
@ -2,27 +2,52 @@
|
|||||||
|
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
const LINKS = [
|
const LINKS = [
|
||||||
{ href: '/#how', label: 'How it works' },
|
{ href: '/#how', label: 'How it works' },
|
||||||
{ href: '/templates', label: 'Templates' },
|
{ href: '/templates', label: 'Templates' },
|
||||||
{ href: '/pricing', label: 'Pricing' },
|
{ href: '/pricing', label: 'Pricing' },
|
||||||
{ href: '/docs', label: 'Docs' },
|
{ href: '/docs', label: 'Docs' },
|
||||||
|
{ href: '/guides', label: 'Guides' },
|
||||||
{ href: '/changelog', label: 'Changelog' },
|
{ href: '/changelog', label: 'Changelog' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Hamburger menu shown below the md breakpoint, where the inline nav is hidden. */
|
/** Hamburger menu shown below the md breakpoint, where the inline nav is hidden. */
|
||||||
export function MarketingMobileMenu() {
|
export function MarketingMobileMenu() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// While the panel is open: lock body scroll, close on Escape and on any
|
||||||
|
// pointerdown outside the menu subtree. All three effects unwind together
|
||||||
|
// when the panel closes or the component unmounts.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const prevOverflow = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setOpen(false);
|
||||||
|
};
|
||||||
|
const onPointerDown = (e: PointerEvent) => {
|
||||||
|
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
document.addEventListener('pointerdown', onPointerDown);
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = prevOverflow;
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
document.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:hidden">
|
<div ref={rootRef} className="md:hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={open ? 'Close menu' : 'Open menu'}
|
aria-label={open ? 'Close menu' : 'Open menu'}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="flex size-8 items-center justify-center rounded-md text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
className="flex size-11 items-center justify-center rounded-md text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
||||||
>
|
>
|
||||||
{open ? <X size={18} /> : <Menu size={18} />}
|
{open ? <X size={18} /> : <Menu size={18} />}
|
||||||
</button>
|
</button>
|
||||||
@ -34,20 +59,27 @@ export function MarketingMobileMenu() {
|
|||||||
// browser falls back to transparent. Inline `var()` is unambiguous.
|
// browser falls back to transparent. Inline `var()` is unambiguous.
|
||||||
className="absolute inset-x-0 top-12 z-40 border-b border-[--color-border] shadow-lg shadow-black/40 backdrop-blur-md"
|
className="absolute inset-x-0 top-12 z-40 border-b border-[--color-border] shadow-lg shadow-black/40 backdrop-blur-md"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'color-mix(in oklab, var(--color-bg) 80%, transparent)',
|
backgroundColor: 'color-mix(in oklab, var(--color-bg) 92%, transparent)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<nav className="mx-auto flex max-w-6xl flex-col px-6">
|
<nav className="mx-auto flex max-w-6xl flex-col px-6 pb-5">
|
||||||
{LINKS.map((l) => (
|
{LINKS.map((l) => (
|
||||||
<Link
|
<Link
|
||||||
key={l.href}
|
key={l.href}
|
||||||
href={l.href}
|
href={l.href}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className="border-b border-[--color-border] py-3.5 text-[14px] text-[--color-fg-muted] transition-colors last:border-0 hover:text-[--color-fg]"
|
className="flex min-h-11 items-center border-b border-[--color-border] text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
||||||
>
|
>
|
||||||
{l.label}
|
{l.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="btn-brand mt-4 inline-flex h-11 w-full items-center justify-center rounded-md text-[14px] font-medium"
|
||||||
|
>
|
||||||
|
Start building free →
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
91
apps/web/lib/pricing.ts
Normal file
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