docs: migrate products page from query params to sub-routes (#8299)

This commit is contained in:
Taesu
2026-03-05 15:19:58 -08:00
committed by Maxwell
parent c32302cf52
commit fa4e882843
10 changed files with 503 additions and 465 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ function EnterpriseHero() {
{/* CTA */}
<div className="flex items-center gap-3 pt-1">
<Link
href="/products?tab=infrastructure"
href="/products/infrastructure"
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/60 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
>
View Products
@@ -0,0 +1,156 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
export function FrameworkContent() {
const frameworkFeatures = [
{
category: "Authentication",
items: [
"Email & password",
"Social sign-in (OAuth)",
"Magic link / OTP",
"Passkeys (WebAuthn)",
"Multi-factor authentication",
"Anonymous sessions",
],
},
{
category: "Authorization",
items: [
"Role-based access control",
"Organization / teams",
"Admin management",
"Session management",
"Account linking",
"Impersonation",
],
},
{
category: "Platform",
items: [
"Framework agnostic",
"20+ database adapters",
"TypeScript-first",
"Edge runtime support",
"Rate limiting",
"CSRF protection",
],
},
];
return (
<div className="px-5 sm:px-6 lg:px-8 pb-32 space-y-8">
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.05 }}
>
<p className="text-[13px] text-foreground/55 dark:text-foreground/45 max-w-lg mb-5">
Everything you need to build production-ready auth. Free and open
source under the MIT license.
</p>
<div className="relative border border-dashed border-foreground/[0.12] bg-foreground/[0.02] overflow-hidden">
<div className="px-5 py-5">
<div className="flex items-start justify-between gap-6">
<div className="space-y-3 flex-1">
<div className="flex items-center gap-2">
<h3 className="text-[11px] font-mono uppercase tracking-widest text-foreground/85">
Community
</h3>
<span className="text-[9px] font-mono uppercase tracking-widest text-foreground/75 dark:text-foreground/60 border border-dashed border-foreground/15 px-1.5 py-0.5 leading-none">
free forever
</span>
</div>
<ul className="space-y-2">
{[
"Unlimited users",
"All auth features",
"50+ plugins",
"Self-hosted",
"MIT license",
].map((item) => (
<li
key={item}
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
>
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
+
</span>
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className="text-right shrink-0 flex flex-col items-end gap-3">
<Link
href="/docs"
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-foreground/[0.12] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 hover:border-foreground/25 hover:bg-foreground/[0.02] transition-all"
>
<span className="font-mono text-[11px] uppercase tracking-widest">
Read Docs
</span>
<svg
className="h-2.5 w-2.5 opacity-50"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 9L9 1M9 1H3M9 1V7"
stroke="currentColor"
strokeWidth="1.2"
/>
</svg>
</Link>
</div>
</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.15 }}
>
<p className="text-[11px] uppercase tracking-widest text-foreground/55 dark:text-foreground/40 font-mono mb-5">
# what&apos;s included
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0">
{frameworkFeatures.map((group, gi) => (
<motion.div
key={group.category}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: 0.2 + gi * 0.08,
}}
className="border border-dashed border-foreground/[0.08] -mt-px -ml-px p-5"
>
<h4 className="text-[10px] font-mono uppercase tracking-widest text-foreground/70 dark:text-foreground/50 mb-4">
{group.category}
</h4>
<ul className="space-y-2">
{group.items.map((item) => (
<li
key={item}
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
>
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
+
</span>
<span>{item}</span>
</li>
))}
</ul>
</motion.div>
))}
</div>
</motion.div>
</div>
);
}
@@ -1,16 +1,10 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { motion } from "framer-motion";
import { Check, Minus } from "lucide-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { HalftoneBackground } from "@/components/landing/halftone-bg";
const ctaHref = `https://dash.better-auth.com/sign-in?callbackUrl=${encodeURIComponent("/?redirectTo=/settings/billing")}`;
type Tab = "framework" | "infrastructure";
const pricingTiers = [
{
name: "Starter",
@@ -260,193 +254,6 @@ function ComparisonCell({ value }: { value: string | boolean }) {
);
}
function FrameworkHero() {
const highlights = [{ label: "License", value: "MIT" }];
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
>
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-200 tracking-tight leading-tight">
<span className="underline underline-offset-4 decoration-foreground/30">
Open Source
</span>
</h1>
<p className="text-[11px] text-foreground/70 dark:text-foreground/50 leading-relaxed max-w-[260px]">
A comprehensive, framework-agnostic authentication library for
TypeScript. Free and open source under the MIT license.
</p>
</div>
<div className="border-t border-foreground/10 pt-4 space-y-0">
{highlights.map((item, i) => (
<motion.div
key={item.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.25,
delay: 0.3 + i * 0.06,
ease: "easeOut",
}}
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.06] last:border-0"
>
<span className="text-[11px] text-foreground/70 dark:text-foreground/50 uppercase tracking-wider">
{item.label}
</span>
<span className="text-[11px] text-foreground/85 dark:text-foreground/75 font-mono">
{item.value}
</span>
</motion.div>
))}
</div>
<div className="flex items-center gap-3 pt-1">
<Link
href="/docs"
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
>
Read Docs
</Link>
<Link
href="https://github.com/better-auth/better-auth"
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
>
GitHub
<svg
className="h-2.5 w-2.5 opacity-50"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 9L9 1M9 1H3M9 1V7"
stroke="currentColor"
strokeWidth="1.2"
/>
</svg>
</Link>
</div>
</div>
</motion.div>
);
}
function InfrastructureHero() {
const principles = [
{ label: "Framework", value: "Open source" },
{ label: "Users", value: "Unlimited" },
{ label: "Audit logs", value: "From 10k/mo" },
{ label: "Security", value: "$0.0001/event overage" },
{ label: "SSO", value: "Business+" },
];
const tiers = [
{ name: "Starter", price: "$0", note: "/mo" },
{ name: "Pro", price: "$20", note: "/mo", highlight: true },
{ name: "Business", price: "$299", note: "/mo" },
{ name: "Enterprise", price: "Custom", note: null },
];
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
>
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-100 tracking-tight leading-tight">
<span className="underline underline-offset-4 decoration-foreground/40">
Infrastructure
</span>
</h1>
<p className="text-[11px] text-foreground/75 dark:text-foreground/65 leading-relaxed max-w-[260px]">
Dashboard, security, audit logs, and more for your auth.
</p>
</div>
<div className="flex items-stretch gap-0 border border-foreground/[0.12]">
{tiers.map((tier) => (
<div
key={tier.name}
className={`flex-1 px-3 py-2.5 text-center border-r border-foreground/[0.12] last:border-r-0 ${
tier.highlight ? "bg-foreground/[0.04]" : ""
}`}
>
<p className="text-[8px] font-mono uppercase tracking-widest text-foreground/65 dark:text-foreground/55 mb-1">
{tier.name}
</p>
<p className="text-sm font-light text-foreground/85 dark:text-foreground/80 tabular-nums flex items-baseline justify-center gap-0.5">
<span>{tier.price}</span>
{tier.note && (
<span className="text-[9px] text-foreground/55 dark:text-foreground/60 font-mono">
{tier.note}
</span>
)}
</p>
</div>
))}
</div>
<div className="border-t border-foreground/[0.12] pt-4 space-y-0">
{principles.map((item, i) => (
<motion.div
key={item.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.25,
delay: 0.3 + i * 0.06,
ease: "easeOut",
}}
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.08] last:border-0"
>
<span className="text-[11px] text-foreground/75 dark:text-foreground/60 uppercase tracking-wider">
{item.label}
</span>
<span className="text-[11px] text-foreground/90 dark:text-foreground/80 font-mono">
{item.value}
</span>
</motion.div>
))}
</div>
<div className="flex items-center gap-3 pt-1">
<Link
href="https://dash.better-auth.com/sign-in"
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
>
Get Started
</Link>
<Link
href="/enterprise"
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
>
Contact Sales
<svg
className="h-2.5 w-2.5 opacity-50"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 9L9 1M9 1H3M9 1V7"
stroke="currentColor"
strokeWidth="1.2"
/>
</svg>
</Link>
</div>
</div>
</motion.div>
);
}
function PricingCard({
tier,
index,
@@ -541,159 +348,7 @@ function PricingCard({
);
}
function FrameworkContent() {
const frameworkFeatures = [
{
category: "Authentication",
items: [
"Email & password",
"Social sign-in (OAuth)",
"Magic link / OTP",
"Passkeys (WebAuthn)",
"Multi-factor authentication",
"Anonymous sessions",
],
},
{
category: "Authorization",
items: [
"Role-based access control",
"Organization / teams",
"Admin management",
"Session management",
"Account linking",
"Impersonation",
],
},
{
category: "Platform",
items: [
"Framework agnostic",
"20+ database adapters",
"TypeScript-first",
"Edge runtime support",
"Rate limiting",
"CSRF protection",
],
},
];
return (
<div className="px-5 sm:px-6 lg:px-8 pb-32 space-y-8">
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.05 }}
>
<p className="text-[13px] text-foreground/55 dark:text-foreground/45 max-w-lg mb-5">
Everything you need to build production-ready auth. Free and open
source under the MIT license.
</p>
<div className="relative border border-dashed border-foreground/[0.12] bg-foreground/[0.02] overflow-hidden">
<div className="px-5 py-5">
<div className="flex items-start justify-between gap-6">
<div className="space-y-3 flex-1">
<div className="flex items-center gap-2">
<h3 className="text-[11px] font-mono uppercase tracking-widest text-foreground/85">
Community
</h3>
<span className="text-[9px] font-mono uppercase tracking-widest text-foreground/75 dark:text-foreground/60 border border-dashed border-foreground/15 px-1.5 py-0.5 leading-none">
free forever
</span>
</div>
<ul className="space-y-2">
{[
"Unlimited users",
"All auth features",
"50+ plugins",
"Self-hosted",
"MIT license",
].map((item) => (
<li
key={item}
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
>
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
+
</span>
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className="text-right shrink-0 flex flex-col items-end gap-3">
<Link
href="/docs"
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-foreground/[0.12] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 hover:border-foreground/25 hover:bg-foreground/[0.02] transition-all"
>
<span className="font-mono text-[11px] uppercase tracking-widest">
Read Docs
</span>
<svg
className="h-2.5 w-2.5 opacity-50"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 9L9 1M9 1H3M9 1V7"
stroke="currentColor"
strokeWidth="1.2"
/>
</svg>
</Link>
</div>
</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.15 }}
>
<p className="text-[11px] uppercase tracking-widest text-foreground/55 dark:text-foreground/40 font-mono mb-5">
# what&apos;s included
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-0">
{frameworkFeatures.map((group, gi) => (
<motion.div
key={group.category}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.3,
delay: 0.2 + gi * 0.08,
}}
className="border border-dashed border-foreground/[0.08] -mt-px -ml-px p-5"
>
<h4 className="text-[10px] font-mono uppercase tracking-widest text-foreground/70 dark:text-foreground/50 mb-4">
{group.category}
</h4>
<ul className="space-y-2">
{group.items.map((item) => (
<li
key={item}
className="flex items-start gap-2 text-[13px] text-foreground/75 dark:text-foreground/60"
>
<span className="text-foreground/50 dark:text-foreground/55 mt-0.5 font-mono text-[10px] leading-none select-none shrink-0">
+
</span>
<span>{item}</span>
</li>
))}
</ul>
</motion.div>
))}
</div>
</motion.div>
</div>
);
}
function InfrastructureContent() {
export function InfrastructureContent() {
const tierKeys = ["starter", "pro", "business", "enterprise"] as const;
const tierLabels = ["Starter", "Pro", "Business", "Enterprise"];
@@ -933,101 +588,3 @@ function InfrastructureContent() {
</div>
);
}
export function ProductsPageClient() {
const searchParams = useSearchParams();
const tabParam = searchParams.get("tab");
const initialTab: Tab =
tabParam === "infrastructure" ? "infrastructure" : "framework";
const [activeTab, setActiveTab] = useState<Tab>(initialTab);
useEffect(() => {
const next: Tab =
tabParam === "infrastructure" ? "infrastructure" : "framework";
setActiveTab(next);
}, [tabParam]);
return (
<div className="relative h-full overflow-x-hidden">
<div className="relative text-foreground h-full">
<div className="flex flex-col lg:flex-row h-full">
{/* Left side — Hero */}
<div className="hidden lg:block relative w-full lg:w-[30%] border-b lg:border-b-0 lg:border-r border-foreground/[0.06] overflow-hidden px-5 sm:px-6 lg:px-10">
<div className="hidden lg:block">
<HalftoneBackground />
</div>
<AnimatePresence mode="wait">
{activeTab === "framework" ? (
<motion.div
key="framework-hero"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.25 }}
className="h-full"
>
<FrameworkHero />
</motion.div>
) : (
<motion.div
key="infrastructure-hero"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.25 }}
className="h-full"
>
<InfrastructureHero />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Right side — Content */}
<div className="relative w-full lg:w-[70%] overflow-y-auto overflow-x-hidden no-scrollbar">
<div className="px-5 sm:px-6 lg:px-8 pt-16 lg:pt-16 pb-4">
<AnimatePresence mode="wait">
<motion.h2
key={activeTab}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.2 }}
className="text-base text-foreground/90 tracking-tight"
>
{activeTab === "framework"
? "Better Auth Framework"
: "Infrastructure"}
</motion.h2>
</AnimatePresence>
</div>
<AnimatePresence mode="wait">
{activeTab === "framework" ? (
<motion.div
key="framework-content"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.25 }}
>
<FrameworkContent />
</motion.div>
) : (
<motion.div
key="infrastructure-content"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.25 }}
>
<InfrastructureContent />
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,264 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import { HalftoneBackground } from "@/components/landing/halftone-bg";
function FrameworkHero() {
const highlights = [{ label: "License", value: "MIT" }];
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
>
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-200 tracking-tight leading-tight">
<span className="underline underline-offset-4 decoration-foreground/30">
Open Source
</span>
</h1>
<p className="text-[11px] text-foreground/70 dark:text-foreground/50 leading-relaxed max-w-[260px]">
A comprehensive, framework-agnostic authentication library for
TypeScript. Free and open source under the MIT license.
</p>
</div>
<div className="border-t border-foreground/10 pt-4 space-y-0">
{highlights.map((item, i) => (
<motion.div
key={item.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.25,
delay: 0.3 + i * 0.06,
ease: "easeOut",
}}
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.06] last:border-0"
>
<span className="text-[11px] text-foreground/70 dark:text-foreground/50 uppercase tracking-wider">
{item.label}
</span>
<span className="text-[11px] text-foreground/85 dark:text-foreground/75 font-mono">
{item.value}
</span>
</motion.div>
))}
</div>
<div className="flex items-center gap-3 pt-1">
<Link
href="/docs"
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
>
Read Docs
</Link>
<Link
href="https://github.com/better-auth/better-auth"
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
>
GitHub
<svg
className="h-2.5 w-2.5 opacity-50"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 9L9 1M9 1H3M9 1V7"
stroke="currentColor"
strokeWidth="1.2"
/>
</svg>
</Link>
</div>
</div>
</motion.div>
);
}
function InfrastructureHero() {
const principles = [
{ label: "Framework", value: "Open source" },
{ label: "Users", value: "Unlimited" },
{ label: "Audit logs", value: "From 10k/mo" },
{ label: "Security", value: "$0.0001/event overage" },
{ label: "SSO", value: "Business+" },
];
const tiers = [
{ name: "Starter", price: "$0", note: "/mo" },
{ name: "Pro", price: "$20", note: "/mo", highlight: true },
{ name: "Business", price: "$299", note: "/mo" },
{ name: "Enterprise", price: "Custom", note: null },
];
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="relative w-full pt-6 md:pt-10 pb-6 lg:pb-0 flex flex-col justify-center lg:h-full"
>
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-lg md:text-xl lg:text-2xl text-neutral-800 dark:text-neutral-100 tracking-tight leading-tight">
<span className="underline underline-offset-4 decoration-foreground/40">
Infrastructure
</span>
</h1>
<p className="text-[11px] text-foreground/75 dark:text-foreground/65 leading-relaxed max-w-[260px]">
Dashboard, security, audit logs, and more for your auth.
</p>
</div>
<div className="flex items-stretch gap-0 border border-foreground/[0.12]">
{tiers.map((tier) => (
<div
key={tier.name}
className={`flex-1 px-3 py-2.5 text-center border-r border-foreground/[0.12] last:border-r-0 ${
tier.highlight ? "bg-foreground/[0.04]" : ""
}`}
>
<p className="text-[8px] font-mono uppercase tracking-widest text-foreground/65 dark:text-foreground/55 mb-1">
{tier.name}
</p>
<p className="text-sm font-light text-foreground/85 dark:text-foreground/80 tabular-nums flex items-baseline justify-center gap-0.5">
<span>{tier.price}</span>
{tier.note && (
<span className="text-[9px] text-foreground/55 dark:text-foreground/60 font-mono">
{tier.note}
</span>
)}
</p>
</div>
))}
</div>
<div className="border-t border-foreground/[0.12] pt-4 space-y-0">
{principles.map((item, i) => (
<motion.div
key={item.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.25,
delay: 0.3 + i * 0.06,
ease: "easeOut",
}}
className="flex items-baseline justify-between py-1.5 border-b border-dashed border-foreground/[0.08] last:border-0"
>
<span className="text-[11px] text-foreground/75 dark:text-foreground/60 uppercase tracking-wider">
{item.label}
</span>
<span className="text-[11px] text-foreground/90 dark:text-foreground/80 font-mono">
{item.value}
</span>
</motion.div>
))}
</div>
<div className="flex items-center gap-3 pt-1">
<Link
href="/sign-in"
className="inline-flex items-center gap-1.5 px-5 py-2 bg-foreground text-background text-sm font-medium hover:opacity-90 transition-colors"
>
Get Started
</Link>
<Link
href="/enterprise"
className="inline-flex items-center gap-1.5 text-[12px] text-foreground/70 dark:text-foreground/50 hover:text-foreground/80 font-mono uppercase tracking-wider transition-colors"
>
Contact Sales
<svg
className="h-2.5 w-2.5 opacity-50"
viewBox="0 0 10 10"
fill="none"
>
<path
d="M1 9L9 1M9 1H3M9 1V7"
stroke="currentColor"
strokeWidth="1.2"
/>
</svg>
</Link>
</div>
</div>
</motion.div>
);
}
export function ProductsShell({
tab,
children,
}: {
tab: string;
children: React.ReactNode;
}) {
const isInfrastructure = tab === "infrastructure";
return (
<div className="relative h-full overflow-x-hidden">
<div className="relative text-foreground h-full">
<div className="flex flex-col lg:flex-row h-full">
{/* Left side — Hero */}
<div className="hidden lg:block relative w-full lg:w-[30%] border-b lg:border-b-0 lg:border-r border-foreground/[0.06] overflow-hidden px-5 sm:px-6 lg:px-10">
<div className="hidden lg:block">
<HalftoneBackground />
</div>
<AnimatePresence mode="wait">
{isInfrastructure ? (
<motion.div
key="infrastructure-hero"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.25 }}
className="h-full"
>
<InfrastructureHero />
</motion.div>
) : (
<motion.div
key="framework-hero"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.25 }}
className="h-full"
>
<FrameworkHero />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Right side — Content */}
<div className="relative w-full lg:w-[70%] overflow-y-auto overflow-x-hidden no-scrollbar">
<div className="px-5 sm:px-6 lg:px-8 pt-16 lg:pt-16 pb-4">
<AnimatePresence mode="wait">
<motion.h2
key={tab}
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.2 }}
className="text-base text-foreground/90 tracking-tight"
>
{isInfrastructure
? "Infrastructure"
: "Better Auth Framework"}
</motion.h2>
</AnimatePresence>
</div>
{children}
</div>
</div>
</div>
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { ProductsShell } from "./_components/products-shell";
export default async function TabLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ tab: string }>;
}) {
const { tab } = await params;
return <ProductsShell tab={tab}>{children}</ProductsShell>;
}
+47
View File
@@ -0,0 +1,47 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { FrameworkContent } from "./_components/framework-content";
import { InfrastructureContent } from "./_components/infrastructure-content";
const tabs = {
framework: {
title: "Framework — Products",
description:
"Better Auth — a comprehensive, framework-agnostic authentication library for TypeScript. Free and open source under the MIT license.",
},
infrastructure: {
title: "Infrastructure — Products",
description:
"Better Auth Infrastructure — dashboard, audit logs, security detection, and more for your auth.",
},
} as const;
type Tab = keyof typeof tabs;
export function generateStaticParams() {
return Object.keys(tabs).map((tab) => ({ tab }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ tab: string }>;
}): Promise<Metadata> {
const { tab } = await params;
const meta = tabs[tab as Tab];
if (!meta) return {};
return { title: meta.title, description: meta.description };
}
export default async function TabPage({
params,
}: {
params: Promise<{ tab: string }>;
}) {
const { tab } = await params;
if (tab === "framework") return <FrameworkContent />;
if (tab === "infrastructure") return <InfrastructureContent />;
redirect("/products/framework");
}
+2 -14
View File
@@ -1,17 +1,5 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { ProductsPageClient } from "./products-client";
export const metadata: Metadata = {
title: "Products",
description:
"Better Auth — free open-source framework with optional managed infrastructure.",
};
import { redirect } from "next/navigation";
export default function ProductsPage() {
return (
<Suspense>
<ProductsPageClient />
</Suspense>
);
redirect("/products/framework");
}
+3 -3
View File
@@ -1621,7 +1621,7 @@ export function HeroReadMe({
headline: "Security & observability.",
desc: "Bot detection, real-time behavior analysis, IP blocking, email validation, and more.",
security: true,
href: "/products?tab=infrastructure",
href: "/products/infrastructure",
managed: true,
},
{
@@ -1629,7 +1629,7 @@ export function HeroReadMe({
headline: "User management.",
desc: "Manage users, sessions, and organizations. Track sign-ups, active users, and growth.",
dashboard: true,
href: "/products?tab=infrastructure",
href: "/products/infrastructure",
managed: true,
},
].map((feature, i) => (
@@ -2499,7 +2499,7 @@ export function HeroReadMe({
</p>
</div>
<Link
href="/products?tab=infrastructure"
href="/products/infrastructure"
className="inline-flex items-center gap-1.5 shrink-0 ml-4 px-4 py-2 border border-dashed border-foreground/[0.14] text-foreground dark:text-foreground/80 hover:text-foreground hover:border-foreground/25 hover:bg-foreground/[0.02] transition-all"
>
<span className="font-mono text-[11px] uppercase tracking-widest">
@@ -26,8 +26,8 @@ const navFiles: NavFileItem[] = [
];
const productFiles: NavFileItem[] = [
{ name: "framework", href: "/products?tab=framework" },
{ name: "infrastructure", href: "/products?tab=infrastructure" },
{ name: "framework", href: "/products/framework" },
{ name: "infrastructure", href: "/products/infrastructure" },
];
const resourceFiles: NavFileItem[] = [
+13
View File
@@ -38,6 +38,19 @@ const nextConfig = {
destination: "/docs/introduction",
permanent: false,
},
// Legacy query string based redirects
{
source: "/products",
has: [{ type: "query", key: "tab", value: "framework" }],
destination: "/products/framework",
permanent: true,
},
{
source: "/products",
has: [{ type: "query", key: "tab", value: "infrastructure" }],
destination: "/products/infrastructure",
permanent: true,
},
];
},
};