spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { Dashboard } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { Metadata } from "next";4import Link from "next/link";5import { EventList } from "@/components/event-list";6import { CapacityOverTime } from "@/components/home/charts";7import { HeroSearch } from "@/components/home/hero";8import { MapPreview } from "@/components/home/map-preview";9import { PipelineFunnel } from "@/components/home/pipeline-funnel";10import { PulseBlock } from "@/components/home/pulse";11import { GlobalTicker } from "@/components/home/ticker";12import { ProjectList } from "@/components/project-list";13import { AiMark, Badge } from "@/components/ui/badge";14import { MoreLink } from "@/components/ui/button";15import { Page, Section } from "@/components/ui/card";16import { BarList } from "@/components/ui/charts";17import { CountryFlag } from "@/components/ui/country-flag";18import { RelativeTime } from "@/components/ui/relative-time";19import { SourceBadge } from "@/components/ui/source-badge";20import { EmptyState, OfflineNotice } from "@/components/ui/states";21import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";22import { api } from "@/lib/api";23import { fmtDate, fmtInt, fmtMw, fmtPct, splitMw } from "@/lib/format";24import { isSiteScopeLabel } from "@/lib/labels";25import { routes } from "@/lib/routes";2627export const revalidate = 60;2829export const metadata: Metadata = {30 title: { absolute: "DataCenterIndex — Global data center infrastructure, indexed and versioned" },31 description: "Facilities, operators, cloud regions, markets, countries, projects, power and connectivity — every figure sourced, versioned and tracked in a live change feed. Known operational capacity, construction pipeline, AI expansion, coverage.",32 alternates: { canonical: "/" },33};3435function Cell({ label, value, unit, note, href }: { label: string; value: string; unit?: string; note?: string; href?: string }) {36 const body = (37 <>38 <div className="label-caps mb-1 truncate">{label}</div>39 <div className="flex items-baseline gap-1 leading-none">40 <span className="figure text-[20px] font-medium tracking-[-0.02em] text-ink">{value}</span>41 {unit && <span className="text-[11px] font-medium text-ink-3">{unit}</span>}42 </div>43 {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}44 </>45 );46 return href ? (47 <Link href={href} className="block min-w-0 py-2 pr-3 hover:[&_.figure]:text-accent">48 {body}49 </Link>50 ) : (51 <div className="min-w-0 py-2 pr-3">{body}</div>52 );53}5455export default async function HomePage() {56 const r0 = await api.dashboard();57 // tolerate an older API payload (image builds prerender against the live API): every 2.0 block is optional58 const r = r0.ok && r0.data ? { ...r0, data: { ...r0.data, pulse: r0.data.pulse ?? null, fastestGrowingMetros: r0.data.fastestGrowingMetros ?? [], majorProjects: r0.data.majorProjects ?? [], operatorExpansion: r0.data.operatorExpansion ?? [], powerEvents: r0.data.powerEvents ?? [], gridConstraints: r0.data.gridConstraints ?? [], coverage: r0.data.coverage ?? null, ingestion: r0.data.ingestion ?? null } as typeof r0.data } : r0;59 const d: Dashboard | null = r.ok ? r.data : null;60 const stats = d?.stats ?? null;61 const cov = d?.coverage ?? null;62 const ing = d?.ingestion ?? null;63 const major = (d?.majorProjects ?? []).filter((p) => p.plannedMw !== null && p.plannedMw >= 100 && (p.capacityScope == null || isSiteScopeLabel(p.capacityScope)));64 const aiMw = splitMw(d?.aiExpansion.plannedMw);6566 return (67 <Page>68 {/* ---------------------------------------------------------------- hero + live map */}69 <section className="relative -mx-3 border-b border-hair px-3 pt-6 sm:-mx-5 sm:px-5 sm:pt-9 lg:-mx-8 lg:px-8">70 <div aria-hidden className="graticule pointer-events-none absolute inset-0" />71 <div className="relative mx-auto max-w-[1280px]">72 <div className="label-caps mb-3 flex items-center gap-2">73 <span className="inline-block size-1.5 rounded-full bg-accent animate-pulse-soft" /> Live index74 {ing?.lastRunAt && (75 <span className="normal-case tracking-normal text-ink-4">76 · last ingestion <RelativeTime iso={ing.lastRunAt} />77 </span>78 )}79 </div>80 <h1 className="max-w-4xl text-[26px] font-semibold uppercase leading-[1.05] tracking-[0.01em] text-ink sm:text-[38px] lg:text-[44px]">Global data center infrastructure</h1>81 <p className="mt-3 max-w-2xl text-[14px] text-ink-2 sm:text-[15.5px]">The structured index of facilities, campuses, operators, cloud regions, markets, projects, power and connectivity — every figure published, sourced, versioned and tracked in a live change feed.</p>82 <HeroSearch />83 <div className="mt-6">84 {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}85 <GlobalTicker stats={stats} className="bg-paper/70 backdrop-blur-[1px]" />86 </div>87 </div>88 <div className="relative mx-auto max-w-[1280px] pb-6 pt-3 sm:pb-8">89 <MapPreview interactive className="h-[300px] sm:h-[420px] lg:h-[460px]">90 <div className="pointer-events-none absolute left-3 top-3 z-[2] flex flex-wrap gap-1.5 text-[11.5px]">91 <span className="rounded-xs border border-hair bg-surface/95 px-2 py-1 text-ink-2 backdrop-blur">{stats ? `${fmtInt(stats.facilities)} facilities · ${fmtInt(stats.countries)} countries · ${fmtInt(stats.metros)} markets` : "Clustered by market"}</span>92 <span className="rounded-xs border border-hair bg-surface/95 px-2 py-1 text-ink-3 backdrop-blur">size = known MW · hollow ring = city-level location</span>93 </div>94 </MapPreview>95 </div>96 </section>9798 <div className="flex flex-col gap-10 pt-8">99 {/* ---------------------------------------------------------------- what changed today */}100 <Section title="What changed today" description={d?.pulse ? `Deterministic counts over the last ${d.pulse.window === "24h" ? "24 hours" : d.pulse.window}, from ${fmtInt(d.pulse.eventsTotal)} deduplicated events.` : "Deterministic counts from the deduplicated event feed."} aside={<MoreLink href={routes.pulse()}>Full pulse</MoreLink>}>101 {d?.pulse ? <PulseBlock pulse={d.pulse} /> : <EmptyState compact title="Pulse unavailable" />}102 </Section>103104 <div className="grid gap-x-8 gap-y-10 lg:grid-cols-2 [&>*]:min-w-0">105 {/* ---------------------------------------------------------------- AI expansion */}106 <Section107 title={108 <span className="inline-flex items-center gap-2">109 AI infrastructure expansion <AiMark size="sm" />110 </span>111 }112 description="Facilities and projects with AI / HPC evidence (confirmed, likely or associated) — published MW only."113 aside={<MoreLink href={routes.ai()}>AI index</MoreLink>}114 >115 <div className="grid grid-cols-3 divide-x divide-hair [&>*+*]:pl-3">116 <Cell label="AI facilities" value={d ? fmtInt(d.aiExpansion.facilities) : "—"} href={routes.datacenters("ai=1")} />117 <Cell label="AI projects" value={d ? fmtInt(d.aiExpansion.projects) : "—"} href={routes.projects("ai=1")} />118 <Cell label="Planned (≈)" value={aiMw?.value ?? "—"} unit={aiMw?.unit ?? "MW"} note="published figures only" />119 </div>120 <div className="mt-3">121 <ProjectList items={(d?.aiExpansion.recent ?? []).slice(0, 5)} emptyTitle="No AI project yet" emptyDescription="Projects with AI / HPC evidence will list here." />122 </div>123 </Section>124125 {/* ---------------------------------------------------------------- construction pipeline */}126 <Section title="Construction pipeline" description="Projects by lifecycle stage with the sum of published MW — company-wide totals are never summed." aside={<MoreLink href={routes.projects()}>Pipeline</MoreLink>}>127 {d ? <PipelineFunnel data={{ byStatus: d.projectsByStatus, openedLast12m: null }} compact /> : <EmptyState compact title="Pipeline unavailable" />}128 </Section>129130 {/* ---------------------------------------------------------------- fastest-growing markets */}131 <Section title="Fastest-growing markets" description="Momentum components over 12 months, shown separately — never collapsed into a score." aside={<MoreLink href={routes.markets()}>Markets</MoreLink>}>132 {d?.fastestGrowingMetros.length ? (133 <Table dense>134 <THead>135 <tr>136 <Th>Market</Th>137 <Th numeric>Announced</Th>138 <Th numeric>In constr.</Th>139 <Th numeric className="hidden sm:table-cell">140 Announced MW141 </Th>142 <Th numeric className="hidden md:table-cell">143 Entrants144 </Th>145 <Th numeric className="hidden md:table-cell">146 Grid ev.147 </Th>148 </tr>149 </THead>150 <TBody>151 {d.fastestGrowingMetros.slice(0, 8).map((m) => (152 <Tr key={m.id}>153 <Td primary>154 <Link href={routes.market(m.slug)} className="inline-flex items-center gap-1.5 font-medium text-ink hover:text-accent hover:underline">155 <CountryFlag iso2={m.countryIso2} size={12} /> {m.name}156 </Link>157 </Td>158 <Td label="Announced" numeric lead>159 {fmtInt(m.momentum.projectsAnnounced)}160 </Td>161 <Td label="In construction" numeric>162 {fmtInt(m.momentum.projectsEnteredConstruction)}163 </Td>164 <Td label="Announced MW" numeric className="hidden sm:table-cell">165 {m.momentum.announcedMw !== null ? `≈ ${fmtMw(m.momentum.announcedMw)}` : "—"}166 </Td>167 <Td label="New entrants" numeric className="hidden md:table-cell">168 {fmtInt(m.momentum.newEntrants.length)}169 </Td>170 <Td label="Grid events" numeric className="hidden md:table-cell">171 {fmtInt(m.momentum.gridEvents)}172 </Td>173 </Tr>174 ))}175 </TBody>176 </Table>177 ) : (178 <EmptyState compact title="Momentum not computed yet" />179 )}180 </Section>181182 {/* ---------------------------------------------------------------- major projects */}183 <Section title="Major projects" description="Site-scoped published planned capacity of 100 MW or more (≈ as announced)." aside={<MoreLink href={routes.projects("min_mw=100&sort=mw")}>All ≥ 100 MW</MoreLink>}>184 <ProjectList items={major.slice(0, 8)} emptyTitle="No ≥ 100 MW project with a site-scoped figure" emptyDescription="Company-wide totals are kept as evidence and never shown as a project's MW." />185 </Section>186187 {/* ---------------------------------------------------------------- operator expansion */}188 <Section title="Operator expansion" description="Operators entering new countries or markets, with projects announced over 12 months." aside={<MoreLink href={routes.operators()}>Operators</MoreLink>}>189 {d?.operatorExpansion.length ? (190 <ul className="divide-y divide-hair">191 {d.operatorExpansion.slice(0, 8).map((o) => (192 <li key={o.operator.id} className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-3 py-2">193 <div className="min-w-0">194 <Link href={routes.operator(o.operator.slug)} className="text-[13.5px] font-medium text-ink hover:text-accent hover:underline">195 {o.operator.name}196 </Link>197 <div className="mt-0.5 truncate text-[12px] text-ink-3">198 {o.newCountries.length ? (199 <span className="inline-flex items-center gap-1">200 {o.newCountries.slice(0, 6).map((c) => (201 <CountryFlag key={c} iso2={c} size={11} />202 ))}203 {o.newCountries.length > 6 && <span>+{o.newCountries.length - 6}</span>}204 </span>205 ) : null}206 {o.newMetros.length > 0 && <span className="ml-1.5">{o.newMetros.slice(0, 3).join(", ")}{o.newMetros.length > 3 ? ` +${o.newMetros.length - 3}` : ""}</span>}207 </div>208 </div>209 <div className="figure text-right text-[12px] text-ink-2">210 <div>211 {fmtInt(o.newCountries.length)} <span className="text-ink-4">countries</span>212 </div>213 <div>214 {fmtInt(o.newMetros.length)} <span className="text-ink-4">markets</span> · {fmtInt(o.projects12m)} <span className="text-ink-4">prj</span>215 </div>216 </div>217 </li>218 ))}219 </ul>220 ) : (221 <EmptyState compact title="No expansion detected yet" />222 )}223 </Section>224225 {/* ---------------------------------------------------------------- power / grid */}226 <Section title="Power / grid events" description="Power agreements, utility events and public grid constraints — attached to markets or countries, never to a site." aside={<MoreLink href={routes.power()}>Power & grid</MoreLink>}>227 <EventList events={(d?.powerEvents ?? []).slice(0, 5)} dense emptyTitle="No power event yet" />228 {d?.gridConstraints.length ? (229 <div className="mt-3 border-t border-hair pt-3">230 <div className="label-caps mb-1.5">Grid constraints</div>231 <ul className="divide-y divide-hair text-[12.5px]">232 {d.gridConstraints.slice(0, 4).map((g) => (233 <li key={g.id} className="flex flex-wrap items-baseline gap-x-2 py-1.5">234 <Badge tone="outline" size="xs">235 {g.kind.replace(/_/g, " ")}236 </Badge>237 <a href={g.url} target="_blank" rel="noopener noreferrer" className="min-w-0 flex-1 truncate font-medium text-ink hover:text-accent">238 {g.title}239 </a>240 {g.sourceKind && <SourceBadge kind={g.sourceKind} name={g.sourceName} url={g.url} />}241 <span className="figure text-[11px] text-ink-3">{fmtDate(g.effectiveDate)}</span>242 </li>243 ))}244 </ul>245 </div>246 ) : null}247 </Section>248249 {/* ---------------------------------------------------------------- cloud / connectivity */}250 <Section title="Cloud / connectivity" description="Public cloud regions and internet exchanges — providers publish the metro, not the building." aside={<MoreLink href={routes.connectivity()}>Connectivity</MoreLink>}>251 <div className="grid grid-cols-3 divide-x divide-hair [&>*+*]:pl-3">252 <Cell label="Cloud regions" value={stats ? fmtInt(stats.cloudRegions) : "—"} href={routes.cloudRegions()} />253 <Cell label="Internet exchanges" value={stats ? fmtInt(stats.ixps) : "—"} href={routes.ixps()} />254 <Cell label="Connectivity known" value={cov ? fmtPct(cov.connectivityCoverage) : "—"} note="facilities with carriers / IXPs" href={routes.coverage()} />255 </div>256 <div className="mt-3">257 <div className="label-caps mb-2">Largest markets · known operational MW</div>258 {d?.topMetros?.length ? (259 <BarList260 items={d.topMetros.slice(0, 6).map((m) => ({ rank: m.rank, label: <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={m.countryIso2} size={12} />{m.name}</span>, value: m.value, href: m.href || routes.market(m.slug), coverage: m.coverage, secondary: m.coverage != null ? `${fmtPct(m.coverage)} MW coverage` : undefined, display: fmtMw(m.value) }))}261 />262 ) : (263 <EmptyState compact title="Rankings not computed yet" />264 )}265 </div>266 </Section>267 </div>268269 {/* ---------------------------------------------------------------- capacity over time */}270 <Section title="Known operational capacity over time" description="Cumulative published MW by opening year — only facilities with both a figure and an opening date; earlier years are lower bounds." aside={<MoreLink href={routes.timeMachine()}>Time machine</MoreLink>}>271 <CapacityOverTime data={d?.capacityOverTime ?? []} />272 </Section>273274 {/* ---------------------------------------------------------------- coverage · transparency · ingestion */}275 <div className="grid gap-x-8 gap-y-8 border-t border-hair pt-6 lg:grid-cols-3">276 <div>277 <div className="mb-2 flex items-baseline justify-between">278 <h2 className="text-[14px] font-semibold text-ink">Data coverage</h2>279 <MoreLink href={routes.coverage()}>Coverage</MoreLink>280 </div>281 {cov ? (282 <dl className="divide-y divide-hair text-[12.5px]">283 {[284 ["Capacity (MW) published", cov.capacityCoverage],285 ["Operator known", cov.operatorCoverage],286 ["Precise location", cov.preciseLocationCoverage],287 ["Any location", cov.anyLocationCoverage],288 ["Status known", cov.statusCoverage],289 ["Opening date known", cov.openingDateCoverage],290 ["Multiple sources", cov.multiSourceCoverage],291 ["Connectivity known", cov.connectivityCoverage],292 ].map(([k, v]) => (293 <div key={String(k)} className="flex items-center justify-between gap-3 py-1.5">294 <dt className="text-ink-2">{k}</dt>295 <dd className="flex items-center gap-2">296 <span className="inline-block h-[3px] w-16 bg-surface-3">297 <span className="block h-full bg-ink-2" style={{ width: `${Math.max(1, Math.round((v as number) * 100))}%` }} />298 </span>299 <span className="figure w-10 text-right text-ink">{fmtPct(v as number, (v as number) < 0.1 ? 1 : 0)}</span>300 </dd>301 </div>302 ))}303 </dl>304 ) : (305 <EmptyState compact title="Coverage unavailable" />306 )}307 <p className="mt-2 text-[11px] text-ink-4">Share of {cov ? fmtInt(cov.facilities) : ""} indexed facilities. Gaps are shown, never filled.</p>308 </div>309 <div>310 <div className="mb-2 flex items-baseline justify-between">311 <h2 className="text-[14px] font-semibold text-ink">Source transparency</h2>312 <MoreLink href={routes.sources()}>Sources</MoreLink>313 </div>314 <div className="grid grid-cols-2 divide-x divide-hair [&>*+*]:pl-3">315 <Cell label="Sources" value={stats ? fmtInt(stats.sources) : "—"} href={routes.sources()} />316 <Cell label="Archived documents" value={stats ? fmtInt(stats.documents) : "—"} />317 <Cell label="Primary-source share" value={cov ? fmtPct(cov.primarySourceShare) : "—"} note="operator, government, utility, filing" />318 <Cell label="Projects located" value={cov ? `${fmtInt(cov.projectsWithLocation)} / ${fmtInt(cov.projects)}` : "—"} note="with published coordinates" />319 </div>320 <p className="mt-2 text-[12px] text-ink-3">321 Every field carries its source, dates, confidence and extraction method; every figure opens an evidence drawer with the supporting sentence.{" "}322 <Link href={routes.methodology()} className="underline decoration-hair-2 hover:text-ink">323 Methodology324 </Link>{" "}325 ·{" "}326 <Link href={routes.download()} className="underline decoration-hair-2 hover:text-ink">327 Downloads328 </Link>{" "}329 ·{" "}330 <Link href={routes.apiDocsPage()} className="underline decoration-hair-2 hover:text-ink">331 API332 </Link>333 </p>334 </div>335 <div>336 <div className="mb-2 flex items-baseline justify-between">337 <h2 className="text-[14px] font-semibold text-ink">Latest ingestion</h2>338 <MoreLink href={routes.live()}>Live feed</MoreLink>339 </div>340 {ing ? (341 <>342 <div className="grid grid-cols-2 divide-x divide-hair [&>*+*]:pl-3">343 <Cell label="Last run" value={ing.lastRunAt ? fmtDate(ing.lastRunAt) : "—"} note={ing.lastRunAt ? "UTC" : undefined} />344 <Cell label="Runs · 24 h" value={fmtInt(ing.runs24h)} />345 <Cell label="Documents · 24 h" value={fmtInt(ing.documents24h)} note="fetched, archived, diffed" />346 <Cell label="Connectors healthy" value={`${fmtInt(ing.connectorsHealthy)} / ${fmtInt(ing.connectorsTotal)}`} note={ing.connectorsTotal ? fmtPct(ing.connectorsHealthy / ing.connectorsTotal) : undefined} />347 </div>348 <p className="mt-2 text-[12px] text-ink-3">349 {fmtInt(ing.events24h)} events detected in 24 h. Pipeline: discover → fetch → archive → extract → normalize → reconcile → validate → version → detect changes → publish.{" "}350 <Link href={routes.bot()} className="inline-flex items-center gap-1 underline decoration-hair-2 hover:text-ink">351 Crawler policy <ExternalLink className="size-3" />352 </Link>353 </p>354 </>355 ) : (356 <EmptyState compact title="Ingestion status unavailable" />357 )}358 </div>359 </div>360 </div>361362 <p className="mt-8 border-t border-hair pt-4 text-[11.5px] text-ink-3">363 {stats ? <>MW figures are published for {fmtPct(stats.mwCoverage)} of indexed facilities; every total is a lower bound computed from sourced figures — nothing is extrapolated or imputed. </> : <>Totals are lower bounds computed from sourced figures only; nothing is extrapolated. </>}364 <Link href={routes.methodology()} className="text-ink-2 underline decoration-hair-2 hover:text-ink">365 How the index is built366 </Link>367 .368 </p>369 </Page>370 );371}372