1/**
2 * The horizontal docs tab bar (bun.com-style primary navigation).
3 *
4 * Each tab owns a URL prefix and a sidebar: the tab bar renders in the docs
5 * header (see `components/starlight/DocsTabs.astro`) and the route middleware
6 * (`docs-tabs-sidebar.ts`) swaps in the sidebar group whose label matches the
7 * active tab. The `label` here MUST match the top-level group label in
8 * `astro.config.mjs`'s `sidebar` array.
9 *
10 * Placement (`slot`):
11 * - "primary" — always-visible tabs on the left. Reserved for providers with
12 * runtimes you build on (plus the flagship databases) — scarce real estate.
13 * - "more" — residents of the "More ▾" dropdown, grouped by `category`.
14 * Full hubs, identical skeleton; only the entry point differs. Promotion
15 * is moving an entry from "more" to "primary".
16 * - "end" — meta sections pinned to the right (Reference, Blog).
17 */
18export interface DocsTab {
19 label: string;
20 href: string;
21 /** URL path prefixes this tab owns (matched on segment boundaries). */
22 prefixes: string[];
23 slot: "primary" | "more" | "end";
24 /** Dropdown category heading (only for slot: "more"). */
25 category?: string;
26 /** One-line scope hint shown in the dropdown. */
27 hint?: string;
28}
29
30export const DOCS_TABS: DocsTab[] = [
31 { label: "Core", href: "/getting-started", prefixes: [], slot: "primary" },
32 { label: "CLI", href: "/cli", prefixes: ["/cli"], slot: "primary" },
33 {
34 label: "Cloudflare",
35 href: "/cloudflare",
36 prefixes: ["/cloudflare", "/providers/cloudflare"],
37 slot: "primary",
38 },
39 {
40 label: "AWS",
41 href: "/aws",
42 prefixes: ["/aws", "/providers/aws"],
43 slot: "primary",
44 },
45 {
46 label: "GCP",
47 href: "/gcp",
48 prefixes: ["/gcp", "/providers/gcp"],
49 slot: "primary",
50 },
51 {
52 label: "Hetzner",
53 href: "/hetzner",
54 prefixes: ["/hetzner", "/providers/hetzner"],
55 slot: "primary",
56 },
57 {
58 label: "Fly",
59 href: "/fly",
60 prefixes: ["/fly", "/providers/fly"],
61 slot: "primary",
62 },
63 {
64 label: "Railway",
65 href: "/railway",
66 prefixes: ["/railway", "/providers/railway"],
67 slot: "primary",
68 },
69 {
70 label: "PlanetScale",
71 href: "/planetscale",
72 prefixes: ["/planetscale", "/providers/planetscale"],
73 slot: "primary",
74 },
75 {
76 label: "Neon",
77 href: "/neon",
78 prefixes: ["/neon", "/providers/neon"],
79 slot: "primary",
80 },
81 {
82 label: "Prisma",
83 href: "/prisma",
84 prefixes: ["/prisma", "/providers/prisma"],
85 slot: "primary",
86 },
87 {
88 label: "Better Auth",
89 href: "/better-auth",
90 prefixes: ["/better-auth", "/providers/betterauth"],
91 slot: "more",
92 category: "Auth",
93 hint: "sessions · plugins · database layers",
94 },
95 {
96 label: "Axiom",
97 href: "/axiom",
98 prefixes: ["/axiom", "/providers/axiom"],
99 slot: "more",
100 category: "Observability",
101 hint: "logs · traces · alerts",
102 },
103 {
104 label: "GitHub",
105 href: "/github",
106 prefixes: ["/github", "/providers/github"],
107 slot: "more",
108 category: "Source & CI",
109 hint: "repos · secrets · events",
110 },
111 {
112 label: "Git",
113 href: "/git",
114 prefixes: ["/git", "/providers/git"],
115 slot: "more",
116 category: "Source & CI",
117 hint: "self-hosted git · clone · push · pull requests",
118 },
119 {
120 label: "Stripe",
121 href: "/stripe",
122 prefixes: ["/stripe", "/providers/stripe"],
123 slot: "more",
124 category: "Payments",
125 hint: "products · prices · webhooks",
126 },
127 {
128 label: "Docker",
129 href: "/docker",
130 prefixes: ["/docker", "/providers/docker"],
131 slot: "more",
132 category: "Containers",
133 hint: "images · containers · networks",
134 },
135 {
136 label: "Kubernetes",
137 href: "/kubernetes",
138 prefixes: ["/kubernetes", "/providers/kubernetes"],
139 slot: "more",
140 category: "Containers",
141 hint: "deployments · jobs · manifests · helm",
142 },
143 {
144 label: "SQL",
145 href: "/sql",
146 prefixes: ["/sql", "/drizzle", "/providers/drizzle", "/providers/sql"],
147 slot: "more",
148 category: "Data",
149 hint: "effect-sql · drizzle · migrations",
150 },
151 {
152 label: "Command",
153 href: "/command",
154 prefixes: ["/command", "/providers/command"],
155 slot: "more",
156 category: "Toolchain",
157 hint: "build · dev · exec",
158 },
159 {
160 label: "ACME",
161 href: "/acme",
162 prefixes: ["/acme", "/providers/acme"],
163 slot: "more",
164 category: "Certificates",
165 hint: "accounts · DNS-01 · certificate issuance",
166 },
167 // A provider's reference pages belong to its hub tab (the prefixes
168 // above claim /providers/{provider} first) — the Reference tab owns
169 // only the directory landing plus providers without a hub.
170 {
171 label: "Reference",
172 href: "/providers",
173 prefixes: ["/providers"],
174 slot: "end",
175 },
176 { label: "Blog", href: "/blog", prefixes: ["/blog"], slot: "end" },
177 // A marketing page, so it never becomes the active tab.
178 { label: "Compare", href: "/compare", prefixes: ["/compare"], slot: "end" },
179];
180
181const matches = (pathname: string, prefix: string) =>
182 pathname === prefix || pathname.startsWith(`${prefix}/`);
183
184/**
185 * Resolve the active tab for a pathname. Core (the platform tab) is the
186 * fallback for every docs page that no provider/reference/blog prefix claims
187 * (what-is-alchemy, getting-started, concepts, guides).
188 */
189export function activeTab(pathname: string): DocsTab {
190 const normalized = pathname.replace(/\/$/, "") || "/";
191 for (const tab of DOCS_TABS) {
192 if (tab.prefixes.some((p) => matches(normalized, p))) return tab;
193 }
194 return DOCS_TABS[0];
195}