1---
2/**
3 * The API Reference landing: a directory of every provider, linking each
4 * one to its home — the docs hub for providers that have one, the first
5 * reference entry otherwise. Resource counts come from the generated
6 * sidebar (`bun run build:reference`); the directory renders without
7 * counts if it hasn't been generated yet.
8 */
9import { sidebarGroupIcon } from "../docs-icons";
10
11interface SidebarNode {
12 label: string;
13 link?: string;
14 items?: SidebarNode[];
15}
16
17/**
18 * Resolved at BUILD time via Vite's eager glob — a runtime fs read
19 * relative to import.meta.url breaks once this component is bundled
20 * into dist/ (the production /providers page rendered zero cards).
21 * The glob stays graceful when the file hasn't been generated yet
22 * (fresh `astro dev` before `bun run build:reference`).
23 */
24const generatedModules = import.meta.glob<{ default: SidebarNode[] }>(
25 "../generated/providers-sidebar.json",
26 { eager: true },
27);
28
29function generatedSidebar(): SidebarNode[] | undefined {
30 return Object.values(generatedModules)[0]?.default;
31}
32
33const countLinks = (nodes: SidebarNode[]): number =>
34 nodes.reduce(
35 (n, node) =>
36 n + (node.link ? 1 : 0) + (node.items ? countLinks(node.items) : 0),
37 0,
38 );
39
40const firstLink = (nodes: SidebarNode[]): string | undefined => {
41 for (const node of nodes) {
42 if (node.link) return node.link;
43 if (node.items) {
44 const found = firstLink(node.items);
45 if (found) return found;
46 }
47 }
48 return undefined;
49};
50
51/** Every provider has a docs hub: each card links to it. */
52const HUBS: Record<string, { href: string; blurb: string }> = {
53 ACME: {
54 href: "/acme",
55 blurb: "Certificate authority accounts, DNS-01 validation, and TLS certificates",
56 },
57 Cloudflare: {
58 href: "/cloudflare",
59 blurb: "Workers, Durable Objects, D1, R2, Queues, Hyperdrive",
60 },
61 AWS: {
62 href: "/aws",
63 blurb: "Lambda, ECS, EKS, EC2, DynamoDB, S3, SQS, Kinesis",
64 },
65 Hetzner: {
66 href: "/hetzner",
67 blurb: "Servers running Services over SSH, Volumes, Load Balancers, DNS",
68 },
69 Fly: {
70 href: "/fly",
71 blurb: "Machines, Services, Volumes, Secrets, and fly.dev on Fly.io",
72 },
73 Railway: {
74 href: "/railway",
75 blurb: "Projects, Services, Postgres, Redis, Volumes, and Buckets",
76 },
77 Planetscale: {
78 href: "/planetscale",
79 blurb: "Serverless MySQL & Postgres with branching workflows",
80 },
81 Neon: { href: "/neon", blurb: "Serverless Postgres with branches per PR" },
82 Axiom: { href: "/axiom", blurb: "Logs, traces, dashboards, and monitors" },
83 BetterAuth: {
84 href: "/better-auth",
85 blurb: "Typed Better Auth with a database layer per platform",
86 },
87 GitHub: { href: "/github", blurb: "Repos, secrets, and webhook events" },
88 Docker: {
89 href: "/docker",
90 blurb: "Images, containers, networks, and volumes via the Docker CLI",
91 },
92 Kubernetes: {
93 href: "/kubernetes",
94 blurb: "Deployments, Jobs, manifests, and Helm charts on any cluster",
95 },
96 Drizzle: {
97 href: "/sql",
98 blurb: "Schemas and migrations as deploy-graph resources",
99 },
100 SQL: {
101 href: "/sql",
102 blurb: "Effect SQL, Drizzle, and Prisma across cloud and database providers",
103 },
104 Command: {
105 href: "/command",
106 blurb: "Memoized builds, dev servers, and one-off commands",
107 },
108};
109
110const BLURBS: Record<string, string> = {};
111
112const DISPLAY_LABELS: Record<string, string> = {
113 Planetscale: "PlanetScale",
114 BetterAuth: "Better Auth",
115};
116
117const providers = generatedSidebar() ?? [];
118const cards = providers.map((p) => {
119 const hub = HUBS[p.label];
120 return {
121 label: DISPLAY_LABELS[p.label] ?? p.label,
122 icon: sidebarGroupIcon(p.label),
123 href: hub?.href ?? firstLink(p.items ?? []) ?? "/providers",
124 blurb: hub?.blurb ?? BLURBS[p.label] ?? "Generated resource reference",
125 count: p.items ? countLinks(p.items) : 0,
126 hasHub: !!hub,
127 };
128});
129---
130
131<div class="alc-provider-directory">
132 {
133 cards.map((card) => (
134 <a class="alc-provider-directory__card" href={card.href}>
135 <span class="alc-provider-directory__title">
136 {card.icon && (
137 <svg
138 aria-hidden="true"
139 width="20"
140 height="20"
141 viewBox="0 0 24 24"
142 set:html={card.icon}
143 />
144 )}
145 {card.label}
146 </span>
147 <span class="alc-provider-directory__blurb">{card.blurb}</span>
148 <span class="alc-provider-directory__meta">
149 {card.count > 0 && <span>{card.count} resources</span>}
150 <span class="alc-provider-directory__cta">
151 {card.hasHub ? "Open hub →" : "Browse reference →"}
152 </span>
153 </span>
154 </a>
155 ))
156 }
157</div>
158
159<style>
160 .alc-provider-directory {
161 display: grid;
162 grid-template-columns: repeat(2, minmax(0, 1fr));
163 gap: 12px;
164 margin-top: 1.5rem;
165 }
166 @media (max-width: 640px) {
167 .alc-provider-directory {
168 grid-template-columns: 1fr;
169 }
170 }
171 .alc-provider-directory__card {
172 display: flex;
173 flex-direction: column;
174 gap: 6px;
175 padding: 16px 18px;
176 border: 1px solid var(--sl-color-hairline, var(--sl-color-gray-5));
177 border-radius: 10px;
178 text-decoration: none;
179 transition:
180 border-color 0.15s ease,
181 transform 0.15s ease;
182 }
183 .alc-provider-directory__card:hover {
184 border-color: var(--sl-color-accent);
185 transform: translateY(-1px);
186 }
187 .alc-provider-directory__title {
188 display: inline-flex;
189 align-items: center;
190 gap: 9px;
191 font-size: var(--sl-text-lg);
192 font-weight: 600;
193 color: var(--sl-color-white);
194 }
195 .alc-provider-directory__title svg {
196 flex-shrink: 0;
197 opacity: 0.9;
198 }
199 .alc-provider-directory__blurb {
200 font-size: var(--sl-text-sm);
201 line-height: 1.5;
202 color: var(--sl-color-gray-3);
203 }
204 .alc-provider-directory__meta {
205 display: flex;
206 justify-content: space-between;
207 align-items: baseline;
208 margin-top: 2px;
209 font-size: var(--sl-text-xs);
210 color: var(--sl-color-gray-3);
211 }
212 .alc-provider-directory__cta {
213 color: var(--sl-color-accent-high);
214 font-weight: 500;
215 }
216</style>