Add theme toggle, hero snippet, and reorganize homepage components

- Set up next-themes with a light/dark toggle in the header and move all
  sections onto theme tokens; dark mode stays pure black
- Replace the hero terminal line with the AI Elements Snippet component
- Restructure components into layout/, sections/, and shared/ with
  site-wide config in lib/site.ts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rami Bitar
2026-09-18 09:29:12 -04:00
co-authored by Claude Fable 5.1
parent a40edbffa9
commit 07a99b441c
28 changed files with 812 additions and 437 deletions
+145
View File
@@ -0,0 +1,145 @@
"use client";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import type { ComponentProps } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
interface SnippetContextType {
code: string;
}
const SnippetContext = createContext<SnippetContextType>({
code: "",
});
export type SnippetProps = ComponentProps<typeof InputGroup> & {
code: string;
};
export const Snippet = ({
code,
className,
children,
...props
}: SnippetProps) => {
const contextValue = useMemo(() => ({ code }), [code]);
return (
<SnippetContext.Provider value={contextValue}>
<InputGroup className={cn("font-mono", className)} {...props}>
{children}
</InputGroup>
</SnippetContext.Provider>
);
};
export type SnippetAddonProps = ComponentProps<typeof InputGroupAddon>;
export const SnippetAddon = (props: SnippetAddonProps) => (
<InputGroupAddon {...props} />
);
export type SnippetTextProps = ComponentProps<typeof InputGroupText>;
export const SnippetText = ({ className, ...props }: SnippetTextProps) => (
<InputGroupText
className={cn("pl-2 font-normal text-muted-foreground", className)}
{...props}
/>
);
export type SnippetInputProps = Omit<
ComponentProps<typeof InputGroupInput>,
"readOnly" | "value"
>;
export const SnippetInput = ({ className, ...props }: SnippetInputProps) => {
const { code } = useContext(SnippetContext);
return (
<InputGroupInput
className={cn("text-foreground", className)}
readOnly
value={code}
{...props}
/>
);
};
export type SnippetCopyButtonProps = ComponentProps<typeof InputGroupButton> & {
onCopy?: () => void;
onError?: (error: Error) => void;
timeout?: number;
};
export const SnippetCopyButton = ({
onCopy,
onError,
timeout = 2000,
children,
className,
...props
}: SnippetCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const timeoutRef = useRef<number>(0);
const { code } = useContext(SnippetContext);
const copyToClipboard = useCallback(async () => {
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
onError?.(new Error("Clipboard API not available"));
return;
}
try {
if (!isCopied) {
await navigator.clipboard.writeText(code);
setIsCopied(true);
onCopy?.();
timeoutRef.current = window.setTimeout(
() => setIsCopied(false),
timeout
);
}
} catch (error) {
onError?.(error as Error);
}
}, [code, onCopy, onError, timeout, isCopied]);
useEffect(
() => () => {
window.clearTimeout(timeoutRef.current);
},
[]
);
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<InputGroupButton
aria-label="Copy"
className={className}
onClick={copyToClipboard}
size="icon-sm"
title="Copy"
{...props}
>
{children ?? <Icon className="size-3.5" size={14} />}
</InputGroupButton>
);
};
@@ -3,7 +3,7 @@
import { useEffect, useRef, useState } from 'react';
import { RiCloseLine, RiMenuLine } from '@remixicon/react';
import { Logo } from '@/components/site/logo';
import { Brand } from '@/components/shared/brand';
import {
Sheet,
SheetClose,
@@ -12,12 +12,7 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
export type NavigationItem = {
label: string;
href: string;
description: string;
};
import { siteConfig, type NavigationItem } from '@/lib/site';
export function MobileNavigation({ items }: { items: NavigationItem[] }) {
const [open, setOpen] = useState(false);
@@ -39,7 +34,7 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
<button
type="button"
aria-label="Open navigation menu"
className="inline-flex size-10 items-center justify-center rounded-md text-white/70 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 md:hidden"
className="inline-flex size-10 items-center justify-center rounded-md text-foreground/70 transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30 md:hidden"
>
<RiMenuLine className="size-5" aria-hidden="true" />
</button>
@@ -48,8 +43,8 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
<SheetContent
side="top"
showCloseButton={false}
overlayClassName="z-[60] bg-black/60 duration-300 supports-backdrop-filter:backdrop-blur-md md:hidden"
className="z-[61] max-h-dvh w-full gap-0 overflow-hidden rounded-none border-white/10 bg-black text-white outline-none duration-300 data-[side=top]:data-open:slide-in-from-top data-[side=top]:data-closed:slide-out-to-top md:hidden"
overlayClassName="z-[60] bg-black/45 duration-300 supports-backdrop-filter:backdrop-blur-md md:hidden"
className="z-[61] max-h-dvh w-full gap-0 overflow-hidden rounded-none border-border bg-background text-foreground outline-none duration-300 data-[side=top]:data-open:slide-in-from-top data-[side=top]:data-closed:slide-out-to-top md:hidden"
onCloseAutoFocus={(event) => {
// Let the anchor move focus to its section without the trigger
// interrupting the page's smooth scroll when the sheet closes.
@@ -57,13 +52,13 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
navigating.current = false;
}}
>
<div className="flex h-16 shrink-0 items-center justify-between border-b border-white/10 px-6">
<Logo />
<div className="flex h-16 shrink-0 items-center justify-between border-b border-border px-6">
<Brand />
<SheetClose asChild>
<button
type="button"
aria-label="Close navigation menu"
className="inline-flex size-10 items-center justify-center rounded-md text-white/60 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
className="inline-flex size-10 items-center justify-center rounded-md text-foreground/60 transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30"
>
<RiCloseLine className="size-5" aria-hidden="true" />
</button>
@@ -71,8 +66,8 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
</div>
<div className="min-h-0 overflow-y-auto overscroll-contain p-3">
<SheetTitle className="px-3 pb-2 pt-3 text-[10px] font-medium uppercase tracking-[0.2em] text-white/40">
Explore Vertex
<SheetTitle className="px-3 pb-2 pt-3 text-[10px] font-medium uppercase tracking-[0.2em] text-foreground/40">
Explore {siteConfig.name}
</SheetTitle>
<SheetDescription className="sr-only">
Explore our products, solutions, resources, documentation, and pricing.
@@ -87,13 +82,13 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
onClick={() => {
navigating.current = true;
}}
className="flex items-center rounded-xl px-3 py-3 transition-colors hover:bg-white/5 focus-visible:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/30"
className="flex items-center rounded-xl px-3 py-3 transition-colors hover:bg-foreground/5 focus-visible:bg-foreground/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-foreground/30"
>
<span className="min-w-0">
<span className="block text-xl font-medium tracking-tight">
{label}
</span>
<span className="mt-0.5 block text-xs leading-5 text-white/45">
<span className="mt-0.5 block text-xs leading-5 text-foreground/45">
{description}
</span>
</span>
@@ -105,25 +100,25 @@ export function MobileNavigation({ items }: { items: NavigationItem[] }) {
</nav>
</div>
<div className="shrink-0 space-y-2 border-t border-white/10 bg-white/[0.02] p-5">
<div className="shrink-0 space-y-2 border-t border-border bg-foreground/[0.02] p-5">
<SheetClose asChild>
<a
href="#"
href={siteConfig.links.signup}
onClick={() => {
navigating.current = true;
}}
className="flex h-12 items-center justify-center rounded-full bg-white px-4 text-sm font-medium text-black transition-colors hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 focus-visible:ring-offset-2 focus-visible:ring-offset-black"
className="flex h-12 items-center justify-center rounded-full bg-foreground px-4 text-sm font-medium text-background transition-colors hover:bg-foreground/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Sign Up
</a>
</SheetClose>
<SheetClose asChild>
<a
href="#"
href={siteConfig.links.login}
onClick={() => {
navigating.current = true;
}}
className="flex h-12 items-center justify-center rounded-full border border-white/10 px-4 text-sm font-medium text-white/70 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
className="flex h-12 items-center justify-center rounded-full border border-border px-4 text-sm font-medium text-foreground/70 transition-colors hover:bg-foreground/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/30"
>
Log In
</a>
+63
View File
@@ -0,0 +1,63 @@
import { RiGithubFill, RiTwitterXFill } from '@remixicon/react';
import { Brand } from '@/components/shared/brand';
import { Container } from '@/components/shared/container';
import { footerColumns, siteConfig } from '@/lib/site';
const socialLinks = [
{ label: 'GitHub', href: siteConfig.links.github, Icon: RiGithubFill },
{ label: 'X (Twitter)', href: siteConfig.links.twitter, Icon: RiTwitterXFill },
];
export function SiteFooter() {
return (
<footer className="border-t border-border">
<Container className="py-16">
<div className="grid gap-10 md:grid-cols-[1.5fr_repeat(4,1fr)]">
<div>
<Brand />
<p className="mt-4 max-w-xs text-sm text-foreground/45">
{siteConfig.tagline}
</p>
</div>
{footerColumns.map((column) => (
<div key={column.heading}>
<h4 className="text-sm font-medium text-foreground/80">
{column.heading}
</h4>
<ul className="mt-4 space-y-3">
{column.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="text-sm text-foreground/45 transition-colors hover:text-foreground"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-14 flex flex-col items-start justify-between gap-4 border-t border-border pt-8 text-sm text-foreground/40 sm:flex-row sm:items-center">
<span>
© {new Date().getFullYear()} {siteConfig.name}, Inc. All rights reserved.
</span>
<div className="flex items-center gap-2">
{socialLinks.map(({ label, href, Icon }) => (
<a
key={label}
href={href}
aria-label={label}
className="inline-flex size-8 items-center justify-center rounded-md text-foreground/45 transition-colors hover:bg-foreground/5 hover:text-foreground"
>
<Icon className="size-4" aria-hidden="true" />
</a>
))}
</div>
</div>
</Container>
</footer>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { MobileNavigation } from '@/components/layout/mobile-navigation';
import { ThemeToggle } from '@/components/layout/theme-toggle';
import { Brand } from '@/components/shared/brand';
import { Container } from '@/components/shared/container';
import { Button } from '@/components/ui/button';
import { navigationItems, siteConfig } from '@/lib/site';
export function SiteHeader() {
return (
<header className="sticky top-0 z-50 border-b border-border bg-background/70 backdrop-blur-xl">
<Container className="flex h-16 items-center justify-between">
<div className="flex items-center gap-8">
<a href="#top" aria-label={`${siteConfig.name} home`}>
<Brand />
</a>
<nav aria-label="Main navigation" className="hidden md:block">
<ul className="flex items-center gap-6">
{navigationItems.map(({ label, href }) => (
<li key={label}>
<a
href={href}
className="text-sm text-foreground/60 transition-colors hover:text-foreground"
>
{label}
</a>
</li>
))}
</ul>
</nav>
</div>
<div className="flex items-center gap-2">
<ThemeToggle />
<Button
asChild
size="lg"
variant="ghost"
className="hidden text-foreground/70 hover:bg-foreground/5 hover:text-foreground sm:inline-flex"
>
<a href={siteConfig.links.login}>Log In</a>
</Button>
<Button
asChild
size="lg"
className="hidden rounded-full bg-foreground text-background hover:bg-foreground/90 sm:inline-flex"
>
<a href={siteConfig.links.signup}>Sign Up</a>
</Button>
<MobileNavigation items={navigationItems} />
</div>
</Container>
</header>
);
}
+11
View File
@@ -0,0 +1,11 @@
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+43
View File
@@ -0,0 +1,43 @@
'use client';
import { useSyncExternalStore } from 'react';
import { RiMoonLine, RiSunLine } from '@remixicon/react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
const emptySubscribe = () => () => {};
export function ThemeToggle() {
const mounted = useSyncExternalStore(
emptySubscribe,
() => true,
() => false,
);
const { resolvedTheme, setTheme } = useTheme();
const isDark = mounted && resolvedTheme === 'dark';
return (
<Button
type="button"
variant="ghost"
size="icon-lg"
aria-label={
mounted ? `Switch to ${isDark ? 'light' : 'dark'} theme` : 'Toggle theme'
}
className="text-foreground/70 hover:bg-foreground/5 hover:text-foreground"
onClick={() => setTheme(isDark ? 'light' : 'dark')}
>
{mounted ? (
isDark ? (
<RiSunLine className="size-4" />
) : (
<RiMoonLine className="size-4" />
)
) : (
<span className="size-4" aria-hidden="true" />
)}
</Button>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { ActionButtons } from '@/components/shared/action-buttons';
import { Container } from '@/components/shared/container';
import { GradientGlow } from '@/components/shared/gradient-glow';
import { SectionHeading } from '@/components/shared/section-heading';
export function CtaSection() {
return (
<section className="relative overflow-hidden">
<GradientGlow className="left-1/2 top-1/2 h-[300px] w-[600px] -translate-x-1/2 -translate-y-1/2 opacity-25" />
<Container className="relative py-28">
<SectionHeading
align="center"
title="Ship your first project in minutes."
description="Import a repository or start from a template. No credit card required to deploy your first preview."
className="[&>h2]:text-4xl sm:[&>h2]:text-5xl"
/>
<ActionButtons
className="mt-9"
primary={{ label: 'Start Deploying', href: '#' }}
secondary={{ label: 'Talk to Sales', href: '#' }}
/>
</Container>
</section>
);
}
+87
View File
@@ -0,0 +1,87 @@
import { Container } from '@/components/shared/container';
import { SectionHeading } from '@/components/shared/section-heading';
const features = [
{
step: '01',
title: 'Build',
description:
'Native support for 35+ frameworks. Push your code and Vertex handles the build pipeline, caching, and dependencies automatically.',
icon: <path d="M8 6L2 12l6 6M16 6l6 6-6 6" strokeWidth="1.5" fill="none" />,
},
{
step: '02',
title: 'Preview',
description:
'Every push gets a live preview URL. Share it, comment on it, and ship with confidence — no environment drift, no surprises.',
icon: (
<>
<rect
x="3"
y="4"
width="18"
height="14"
rx="2"
strokeWidth="1.5"
fill="none"
/>
<path d="M9 21h6M12 18v3" strokeWidth="1.5" fill="none" />
</>
),
},
{
step: '03',
title: 'Ship',
description:
'Deploy to a global edge network in seconds. Instant rollbacks, zero-downtime releases, and traffic that lands close to every user.',
icon: (
<path
d="M4 13l1.5-5.5A2 2 0 0 1 7.4 6h9.2a2 2 0 0 1 1.9 1.5L20 13M4 13v5h16v-5M4 13h16M8 17h.01M16 17h.01"
strokeWidth="1.5"
fill="none"
/>
),
},
];
export function FeaturesSection() {
return (
<section>
<Container className="py-24">
<SectionHeading
eyebrow="The workflow"
title="From commit to production, without the ceremony."
/>
<div className="mt-14 grid gap-px overflow-hidden rounded-2xl border border-border bg-border md:grid-cols-3">
{features.map((feature) => (
<div
key={feature.step}
className="group flex flex-col bg-background p-8 transition-colors hover:bg-foreground/[0.03]"
>
<div className="flex items-center justify-between">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
stroke="currentColor"
className="text-foreground/80"
aria-hidden="true"
>
{feature.icon}
</svg>
<span className="font-mono text-xs text-foreground/30">
{feature.step}
</span>
</div>
<h3 className="mt-6 text-xl font-medium">{feature.title}</h3>
<p className="mt-3 text-sm leading-6 text-foreground/50">
{feature.description}
</p>
</div>
))}
</div>
</Container>
</section>
);
}
+82
View File
@@ -0,0 +1,82 @@
import {
Snippet,
SnippetAddon,
SnippetCopyButton,
SnippetInput,
SnippetText,
} from '@/components/ai-elements/snippet';
import { ActionButtons } from '@/components/shared/action-buttons';
import { Container } from '@/components/shared/container';
import { GradientGlow } from '@/components/shared/gradient-glow';
const hero = {
announcement: { label: 'Vertex Edge is now generally available', href: '#' },
title: 'Build. Preview. Ship.',
description:
'Vertex is the frontend cloud. Give your team the tooling and global infrastructure to build, preview, and ship a faster web — from your first commit to production traffic.',
primary: { label: 'Start Deploying', href: '#' },
secondary: { label: 'Get a Demo', href: '#' },
command: 'vertex deploy --prod',
};
function GridBackdrop() {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,black,transparent)]"
style={{
backgroundImage:
'linear-gradient(to right, color-mix(in oklab, var(--foreground) 6%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 6%, transparent) 1px, transparent 1px)',
backgroundSize: '56px 56px',
}}
/>
);
}
export function HeroSection() {
return (
<section className="relative overflow-hidden">
<GridBackdrop />
<GradientGlow className="left-1/2 top-[-120px] h-[420px] w-[820px] -translate-x-1/2 opacity-40" />
<Container className="relative pt-24 pb-20 text-center sm:pt-32">
<a
href={hero.announcement.href}
className="group inline-flex items-center gap-2 rounded-full border border-foreground/15 bg-foreground/5 px-3.5 py-1.5 text-xs text-foreground/70 transition-colors hover:border-foreground/25 hover:text-foreground"
>
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-400" />
{hero.announcement.label}
<span className="transition-transform group-hover:translate-x-0.5">
&rarr;
</span>
</a>
<h1 className="mx-auto mt-8 max-w-4xl bg-gradient-to-b from-foreground to-foreground/60 bg-clip-text text-5xl font-semibold leading-[1.05] tracking-tight text-transparent sm:text-7xl">
{hero.title}
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-foreground/55">
{hero.description}
</p>
<ActionButtons
className="mt-10"
primary={hero.primary}
secondary={hero.secondary}
/>
<Snippet
className="mx-auto mt-14 h-12 max-w-md rounded-xl border-border bg-foreground/[0.03] text-sm"
code={hero.command}
>
<SnippetAddon className="pl-1">
<SnippetText>$</SnippetText>
</SnippetAddon>
<SnippetInput />
<SnippetAddon align="inline-end" className="pr-2">
<SnippetCopyButton />
</SnippetAddon>
</Snippet>
</Container>
</section>
);
}
@@ -1,5 +1,6 @@
import Image from 'next/image';
import { Container } from '@/components/shared/container';
import { Marquee } from '@/components/ui/marquee';
const customerLogos = [
@@ -53,11 +54,11 @@ const customerLogos = [
},
];
export function LogoCloud() {
export function LogosSection() {
return (
<section className="border-y border-white/10 bg-white/[0.02]">
<div className="mx-auto max-w-6xl px-6 py-12">
<p className="text-center text-xs font-medium uppercase tracking-[0.2em] text-white/40">
<section className="border-y border-border bg-foreground/[0.02]">
<Container className="py-12">
<p className="text-center text-xs font-medium uppercase tracking-[0.2em] text-foreground/40">
Trusted by fast-moving teams shipping to millions
</p>
<div className="relative mt-8 overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_15%,black_85%,transparent)]">
@@ -73,13 +74,13 @@ export function LogoCloud() {
width={logo.width}
height={logo.height}
unoptimized
className="h-7 w-[120px] max-w-full object-contain opacity-40 brightness-0 invert transition-opacity duration-200 group-hover:opacity-80"
className="h-7 w-[120px] max-w-full object-contain opacity-40 brightness-0 transition-opacity duration-200 group-hover:opacity-80 dark:invert"
/>
</div>
))}
</Marquee>
</div>
</div>
</Container>
</section>
);
}
@@ -1,4 +1,5 @@
import { StatsGlobe } from '@/components/site/stats-globe';
import { StatsGlobe } from '@/components/sections/stats/stats-globe';
import { Container } from '@/components/shared/container';
const stats = [
{ value: '45ms', label: 'Median edge response, worldwide' },
@@ -7,25 +8,25 @@ const stats = [
{ value: '1.2T', label: 'Requests served every month' },
];
export function Stats() {
export function StatsSection() {
return (
<section className="relative overflow-hidden border-y border-white/10 bg-black py-16 sm:py-24">
<section className="relative overflow-hidden border-y border-border bg-background py-16 sm:py-24">
<StatsGlobe />
<div className="relative z-10 mx-auto grid max-w-6xl grid-cols-2 gap-px lg:grid-cols-4">
<Container className="relative z-10 grid grid-cols-2 gap-px px-0 lg:grid-cols-4">
{stats.map((stat) => (
<div
key={stat.label}
className="px-6 py-12 text-center ring-1 ring-white/10"
className="px-6 py-12 text-center ring-1 ring-border"
>
<div className="text-4xl font-semibold tracking-tight sm:text-5xl">
{stat.value}
</div>
<div className="mx-auto mt-3 max-w-[16ch] text-sm text-white/45">
<div className="mx-auto mt-3 max-w-[16ch] text-sm text-foreground/45">
{stat.label}
</div>
</div>
))}
</div>
</Container>
</section>
);
}
+67
View File
@@ -0,0 +1,67 @@
'use client';
import { type COBEOptions } from 'cobe';
import { useTheme } from 'next-themes';
import { Globe } from '@/components/ui/globe';
// Rotation per frame. Default globe is 0.005; lower is slower, 0 stops it.
const GLOBE_SPEED = 0.003;
const markers: COBEOptions['markers'] = [
{ location: [37.7749, -122.4194], size: 0.08 },
{ location: [40.7128, -74.006], size: 0.06 },
{ location: [51.5072, -0.1276], size: 0.07 },
{ location: [48.8566, 2.3522], size: 0.05 },
{ location: [1.3521, 103.8198], size: 0.08 },
{ location: [35.6762, 139.6503], size: 0.06 },
{ location: [-33.8688, 151.2093], size: 0.05 },
{ location: [-23.5505, -46.6333], size: 0.06 },
];
const darkGlobeConfig: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.22,
dark: 1,
diffuse: 1.2,
mapSamples: 20000,
mapBrightness: 5,
baseColor: [0.22, 0.22, 0.22],
markerColor: [1, 1, 1],
glowColor: [0.1, 0.1, 0.1],
markers,
};
const lightGlobeConfig: COBEOptions = {
...darkGlobeConfig,
dark: 0,
diffuse: 0.4,
mapBrightness: 1.2,
baseColor: [0.88, 0.88, 0.88],
markerColor: [0.1, 0.1, 0.1],
glowColor: [0.96, 0.96, 0.96],
};
export function StatsGlobe() {
const { resolvedTheme } = useTheme();
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden [mask-image:radial-gradient(ellipse_80%_110%_at_50%_90%,black_45%,transparent_100%)]"
>
<div className="absolute left-1/2 top-[6%] aspect-square w-[760px] -translate-x-1/2 opacity-90 sm:w-[1040px] lg:w-[1280px]">
<Globe
className="max-w-none"
config={resolvedTheme === 'light' ? lightGlobeConfig : darkGlobeConfig}
speed={GLOBE_SPEED}
/>
</div>
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-background/80 to-transparent" />
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type Action = { label: string; href: string };
export function ActionButtons({
primary,
secondary,
className,
}: {
primary: Action;
secondary?: Action;
className?: string;
}) {
return (
<div
className={cn(
'flex flex-col items-center justify-center gap-3 sm:flex-row',
className,
)}
>
<Button
asChild
size="lg"
className="h-11 rounded-full bg-foreground px-6 text-[15px] text-background hover:bg-foreground/90"
>
<a href={primary.href}>{primary.label}</a>
</Button>
{secondary && (
<Button
asChild
size="lg"
variant="outline"
className="h-11 rounded-full border-foreground/20 bg-transparent px-6 text-[15px] text-foreground hover:bg-foreground/5 hover:text-foreground"
>
<a href={secondary.href}>{secondary.label}</a>
</Button>
)}
</div>
);
}
@@ -1,6 +1,7 @@
import { cn } from '@/lib/utils';
import { siteConfig } from '@/lib/site';
export function Logo({ className }: { className?: string }) {
export function Brand({ className }: { className?: string }) {
return (
<span className={cn('flex items-center gap-2 font-medium', className)}>
<svg
@@ -13,7 +14,7 @@ export function Logo({ className }: { className?: string }) {
>
<path d="M12 2L23 21H1L12 2Z" fill="currentColor" />
</svg>
<span className="text-[17px] tracking-tight">Vertex</span>
<span className="text-[17px] tracking-tight">{siteConfig.name}</span>
</span>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { type ComponentProps } from 'react';
import { cn } from '@/lib/utils';
export function Container({ className, ...props }: ComponentProps<'div'>) {
return (
<div className={cn('mx-auto w-full max-w-6xl px-6', className)} {...props} />
);
}
+21
View File
@@ -0,0 +1,21 @@
import { cn } from '@/lib/utils';
/**
* Soft conic-gradient bloom used behind hero and CTA copy.
* Position and size it with `className`; it never captures pointer events.
*/
export function GradientGlow({ className }: { className?: string }) {
return (
<div
aria-hidden="true"
className={cn(
'pointer-events-none absolute rounded-full blur-3xl',
className,
)}
style={{
background:
'conic-gradient(from 180deg at 50% 50%, #7c3aed, #2563eb, #06b6d4, #ec4899, #7c3aed)',
}}
/>
);
}
+44
View File
@@ -0,0 +1,44 @@
import { cn } from '@/lib/utils';
export function SectionHeading({
eyebrow,
title,
description,
align = 'left',
className,
}: {
eyebrow?: string;
title: string;
description?: string;
align?: 'left' | 'center';
className?: string;
}) {
return (
<div
className={cn(
'max-w-2xl',
align === 'center' && 'mx-auto text-center',
className,
)}
>
{eyebrow && (
<p className="font-mono text-xs uppercase tracking-[0.2em] text-foreground/40">
{eyebrow}
</p>
)}
<h2
className={cn(
'text-3xl font-semibold tracking-tight sm:text-4xl',
eyebrow && 'mt-4',
)}
>
{title}
</h2>
{description && (
<p className="mt-5 text-lg leading-8 text-foreground/55">
{description}
</p>
)}
</div>
);
}
-40
View File
@@ -1,40 +0,0 @@
import { Button } from '@/components/ui/button';
export function Cta() {
return (
<section className="relative mx-auto max-w-6xl overflow-hidden px-6 py-28 text-center">
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 h-[300px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-25 blur-3xl"
style={{
background:
'conic-gradient(from 180deg at 50% 50%, #7c3aed, #2563eb, #06b6d4, #ec4899, #7c3aed)',
}}
/>
<h2 className="relative mx-auto max-w-2xl text-4xl font-semibold tracking-tight sm:text-5xl">
Ship your first project in minutes.
</h2>
<p className="relative mx-auto mt-5 max-w-xl text-lg text-white/55">
Import a repository or start from a template. No credit card required to
deploy your first preview.
</p>
<div className="relative mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Button
asChild
size="lg"
className="h-11 rounded-full bg-white px-6 text-[15px] text-black hover:bg-white/90"
>
<a href="#">Start Deploying</a>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="h-11 rounded-full border-white/20 bg-transparent px-6 text-[15px] text-white hover:bg-white/5 hover:text-white"
>
<a href="#">Talk to Sales</a>
</Button>
</div>
</section>
);
}
-86
View File
@@ -1,86 +0,0 @@
const features = [
{
step: '01',
title: 'Build',
description:
'Native support for 35+ frameworks. Push your code and Vertex handles the build pipeline, caching, and dependencies automatically.',
icon: <path d="M8 6L2 12l6 6M16 6l6 6-6 6" strokeWidth="1.5" fill="none" />,
},
{
step: '02',
title: 'Preview',
description:
'Every push gets a live preview URL. Share it, comment on it, and ship with confidence — no environment drift, no surprises.',
icon: (
<>
<rect
x="3"
y="4"
width="18"
height="14"
rx="2"
strokeWidth="1.5"
fill="none"
/>
<path d="M9 21h6M12 18v3" strokeWidth="1.5" fill="none" />
</>
),
},
{
step: '03',
title: 'Ship',
description:
'Deploy to a global edge network in seconds. Instant rollbacks, zero-downtime releases, and traffic that lands close to every user.',
icon: (
<path
d="M4 13l1.5-5.5A2 2 0 0 1 7.4 6h9.2a2 2 0 0 1 1.9 1.5L20 13M4 13v5h16v-5M4 13h16M8 17h.01M16 17h.01"
strokeWidth="1.5"
fill="none"
/>
),
},
];
export function Features() {
return (
<section className="mx-auto max-w-6xl px-6 py-24">
<div className="max-w-2xl">
<p className="font-mono text-xs uppercase tracking-[0.2em] text-white/40">
The workflow
</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">
From commit to production, without the ceremony.
</h2>
</div>
<div className="mt-14 grid gap-px overflow-hidden rounded-2xl border border-white/10 bg-white/10 md:grid-cols-3">
{features.map((feature) => (
<div
key={feature.step}
className="group flex flex-col bg-black p-8 transition-colors hover:bg-white/[0.03]"
>
<div className="flex items-center justify-between">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
stroke="currentColor"
className="text-white/80"
aria-hidden="true"
>
{feature.icon}
</svg>
<span className="font-mono text-xs text-white/30">
{feature.step}
</span>
</div>
<h3 className="mt-6 text-xl font-medium">{feature.title}</h3>
<p className="mt-3 text-sm leading-6 text-white/50">
{feature.description}
</p>
</div>
))}
</div>
</section>
);
}
-77
View File
@@ -1,77 +0,0 @@
import { RiGithubFill, RiTwitterXFill } from '@remixicon/react';
import { Logo } from '@/components/site/logo';
const columns = [
{
heading: 'Products',
links: ['Previews', 'Edge Network', 'Analytics', 'Storage', 'Firewall'],
},
{
heading: 'Resources',
links: ['Docs', 'Guides', 'Changelog', 'Templates', 'Status'],
},
{
heading: 'Company',
links: ['About', 'Careers', 'Blog', 'Customers', 'Contact'],
},
{
heading: 'Legal',
links: ['Privacy', 'Terms', 'Security', 'DPA', 'Cookies'],
},
];
export function Footer() {
return (
<footer className="border-t border-white/10">
<div className="mx-auto max-w-6xl px-6 py-16">
<div className="grid gap-10 md:grid-cols-[1.5fr_repeat(4,1fr)]">
<div>
<Logo />
<p className="mt-4 max-w-xs text-sm text-white/45">
The frontend cloud for teams that ship.
</p>
</div>
{columns.map((column) => (
<div key={column.heading}>
<h4 className="text-sm font-medium text-white/80">
{column.heading}
</h4>
<ul className="mt-4 space-y-3">
{column.links.map((link) => (
<li key={link}>
<a
href="#"
className="text-sm text-white/45 transition-colors hover:text-white"
>
{link}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-14 flex flex-col items-start justify-between gap-4 border-t border-white/10 pt-8 text-sm text-white/40 sm:flex-row sm:items-center">
<span>© 2026 Vertex, Inc. All rights reserved.</span>
<div className="flex items-center gap-2">
<a
href="https://github.com"
aria-label="GitHub"
className="inline-flex size-8 items-center justify-center rounded-md text-white/45 transition-colors hover:bg-white/5 hover:text-white"
>
<RiGithubFill className="size-4" aria-hidden="true" />
</a>
<a
href="https://x.com"
aria-label="X (Twitter)"
className="inline-flex size-8 items-center justify-center rounded-md text-white/45 transition-colors hover:bg-white/5 hover:text-white"
>
<RiTwitterXFill className="size-4" aria-hidden="true" />
</a>
</div>
</div>
</div>
</footer>
);
}
-73
View File
@@ -1,73 +0,0 @@
import { Button } from '@/components/ui/button';
export function Hero() {
return (
<section className="relative overflow-hidden">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,black,transparent)]"
style={{
backgroundImage:
'linear-gradient(to right, rgba(255,255,255,0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.06) 1px, transparent 1px)',
backgroundSize: '56px 56px',
}}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-[-120px] h-[420px] w-[820px] -translate-x-1/2 rounded-full opacity-40 blur-3xl"
style={{
background:
'conic-gradient(from 180deg at 50% 50%, #7c3aed, #2563eb, #06b6d4, #ec4899, #7c3aed)',
}}
/>
<div className="relative mx-auto max-w-6xl px-6 pt-24 pb-20 text-center sm:pt-32">
<a
href="#"
className="group inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3.5 py-1.5 text-xs text-white/70 transition-colors hover:border-white/25 hover:text-white"
>
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-400" />
Vertex Edge is now generally available
<span className="transition-transform group-hover:translate-x-0.5">
&rarr;
</span>
</a>
<h1 className="mx-auto mt-8 max-w-4xl bg-gradient-to-b from-white to-white/60 bg-clip-text text-5xl font-semibold leading-[1.05] tracking-tight text-transparent sm:text-7xl">
Build. Preview. Ship.
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-white/55">
Vertex is the frontend cloud. Give your team the tooling and global
infrastructure to build, preview, and ship a faster web — from your
first commit to production traffic.
</p>
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Button
asChild
size="lg"
className="h-11 rounded-full bg-white px-6 text-[15px] text-black hover:bg-white/90"
>
<a href="#">Start Deploying</a>
</Button>
<Button
asChild
size="lg"
variant="outline"
className="h-11 rounded-full border-white/20 bg-transparent px-6 text-[15px] text-white hover:bg-white/5 hover:text-white"
>
<a href="#">Get a Demo</a>
</Button>
</div>
<div className="mx-auto mt-14 flex max-w-md items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-4 py-3 font-mono text-sm text-white/70">
<span className="text-white/30">$</span>
<span>
<span className="text-white">vertex</span> deploy --prod
</span>
<span className="ml-auto h-4 w-px animate-pulse bg-white/40" />
</div>
</div>
</section>
);
}
-53
View File
@@ -1,53 +0,0 @@
import { Logo } from '@/components/site/logo';
import { MobileNavigation } from '@/components/site/mobile-navigation';
import { Button } from '@/components/ui/button';
const navigationItems = [
{ label: 'Products', href: '#', description: 'Everything you need to ship' },
{ label: 'Solutions', href: '#', description: 'Built for teams of every size' },
{ label: 'Resources', href: '#', description: 'Guides, templates, and changelog' },
{ label: 'Docs', href: '#', description: 'Your first deploy starts here' },
{ label: 'Pricing', href: '#', description: 'Start free. Scale at your pace.' },
];
export function Navbar() {
return (
<header className="sticky top-0 z-50 border-b border-white/10 bg-black/70 backdrop-blur-xl">
<nav className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<div className="flex items-center gap-8">
<Logo />
<ul className="hidden items-center gap-6 md:flex">
{navigationItems.map(({ label, href }) => (
<li key={label}>
<a
href={href}
className="text-sm text-white/60 transition-colors hover:text-white"
>
{label}
</a>
</li>
))}
</ul>
</div>
<div className="flex items-center gap-2">
<Button
asChild
size="lg"
variant="ghost"
className="hidden text-white/70 hover:bg-white/5 hover:text-white sm:inline-flex"
>
<a href="#">Log In</a>
</Button>
<Button
asChild
size="lg"
className="hidden rounded-full bg-white text-black hover:bg-white/90 sm:inline-flex"
>
<a href="#">Sign Up</a>
</Button>
<MobileNavigation items={navigationItems} />
</div>
</nav>
</header>
);
}
-48
View File
@@ -1,48 +0,0 @@
'use client';
import { type COBEOptions } from 'cobe';
import { Globe } from '@/components/ui/globe';
// Rotation per frame. Default globe is 0.005; lower is slower, 0 stops it.
const GLOBE_SPEED = 0.003;
const globeConfig: COBEOptions = {
width: 800,
height: 800,
onRender: () => {},
devicePixelRatio: 2,
phi: 0,
theta: 0.22,
dark: 1,
diffuse: 1.2,
mapSamples: 20000,
mapBrightness: 5,
baseColor: [0.22, 0.22, 0.22],
markerColor: [1, 1, 1],
glowColor: [0.1, 0.1, 0.1],
markers: [
{ location: [37.7749, -122.4194], size: 0.08 },
{ location: [40.7128, -74.006], size: 0.06 },
{ location: [51.5072, -0.1276], size: 0.07 },
{ location: [48.8566, 2.3522], size: 0.05 },
{ location: [1.3521, 103.8198], size: 0.08 },
{ location: [35.6762, 139.6503], size: 0.06 },
{ location: [-33.8688, 151.2093], size: 0.05 },
{ location: [-23.5505, -46.6333], size: 0.06 },
],
};
export function StatsGlobe() {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden [mask-image:radial-gradient(ellipse_80%_110%_at_50%_90%,black_45%,transparent_100%)]"
>
<div className="absolute left-1/2 top-[6%] aspect-square w-[760px] -translate-x-1/2 opacity-90 sm:w-[1040px] lg:w-[1280px]">
<Globe className="max-w-none" config={globeConfig} speed={GLOBE_SPEED} />
</div>
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-black/80 to-transparent" />
</div>
);
}