import type { OperatorComparison } from "@dci/core"; import type { Metadata } from "next"; import Link from "next/link"; import type { ReactNode } from "react"; import { Breadcrumbs } from "@/components/entity/breadcrumbs"; import { SourcesFooter } from "@/components/entity/sources-footer"; import { OperatorPicker } from "@/components/intelligence/compare/operator-picker"; import { CoverageBar, HonestEmpty, PublishedOnlyNote } from "@/components/intelligence/shared"; import { Badge } from "@/components/ui/badge"; import { Page, PageHeader, Section } from "@/components/ui/card"; import { CountryFlag } from "@/components/ui/country-flag"; import { EmptyState, OfflineNotice } from "@/components/ui/states"; import { api } from "@/lib/api"; import { fmtInt, fmtMw, fmtPct } from "@/lib/format"; import { OPERATOR_KIND_LABEL } from "@/lib/labels"; import { routes } from "@/lib/routes"; import { pageMetadata } from "@/lib/seo"; import type { SearchParams } from "@/lib/sort"; export const revalidate = 120; const MAX = 5; export const metadata: Metadata = pageMetadata({ title: "Compare operators", description: "Side-by-side comparison of 2–5 data center operators: facilities, countries, markets, known MW with its coverage, pipeline, AI facilities, new markets entered and recent projects — published figures only.", path: routes.compareOperators(), }); /* Presets verified against the index (top operators by facility count on 2026-09-12). */ const PRESETS: Array<{ label: string; slugs: string[] }> = [ { label: "Equinix vs Digital Realty", slugs: ["equinix", "digital-realty"] }, { label: "US wholesale: STACK · CyrusOne · Vantage · QTS", slugs: ["stack-infrastructure", "cyrusone", "vantage-data-centers", "qts"] }, { label: "DataBank vs EdgeConneX", slugs: ["databank", "edgeconnex"] }, ]; type Op = OperatorComparison["operators"][number]; function parseSlugs(sp: SearchParams): string[] { const raw = sp.slugs; const s = Array.isArray(raw) ? raw.join(",") : (raw ?? ""); const out: string[] = []; for (const part of s.split(",")) { const v = part.trim().toLowerCase(); if (v && /^[a-z0-9][a-z0-9-]*$/.test(v) && !out.includes(v)) out.push(v); } return out.slice(0, MAX); } const dash = —; const mw = (v: number | null | undefined, est?: boolean) => (v === null || v === undefined ? dash : {est ? "≈ " : ""}{fmtMw(v)}); const int = (v: number | null | undefined) => (v === null || v === undefined ? dash : {fmtInt(v)}); interface RowDef { key: string; label: string; hint?: string; cell: (o: Op) => ReactNode; /** row belongs to a group header */ group?: string; } function rows(): RowDef[] { const win = (o: Op, label: "12m" | "3y" | "5y") => o.velocity.windows.find((w) => w.label === label); return [ { key: "kind", label: "Kind", cell: (o) => (o.kind ? OPERATOR_KIND_LABEL[o.kind] : dash) }, { key: "hq", label: "Headquarters", cell: (o) => (o.hqCountryIso2 ? : dash) }, { key: "facilities", label: "Facilities", group: "Footprint", cell: (o) => int(o.facilityCount) }, { key: "countries", label: "Countries", cell: (o) => int(o.countryCount) }, { key: "metros", label: "Markets", cell: (o) => int(o.metroCount) }, { key: "known", label: "Known operational MW", group: "Capacity (published figures only)", hint: "Σ IT / total power over operational facilities", cell: (o) => mw(o.knownMw) }, { key: "coverage", label: "Capacity coverage", hint: "share of facilities with any MW figure", cell: (o) => }, { key: "construction", label: "Under construction MW", cell: (o) => mw(o.constructionMw ?? o.pipeline.construction.mw, true) }, { key: "planned", label: "Planned MW", cell: (o) => mw(o.plannedMw ?? (o.pipeline.approved.mw === null && o.pipeline.announced.mw === null ? null : (o.pipeline.approved.mw ?? 0) + (o.pipeline.announced.mw ?? 0)), true) }, { key: "p-op", label: "Operational sites", group: "Pipeline buckets", cell: (o) => int(o.pipeline.operational.count) }, { key: "p-con", label: "Under construction sites", cell: (o) => int(o.pipeline.construction.count) }, { key: "p-app", label: "Approved / permitting sites", cell: (o) => int(o.pipeline.approved.count) }, { key: "p-ann", label: "Announced / proposed sites", cell: (o) => int(o.pipeline.announced.count) }, { key: "projects", label: "Project records", cell: (o) => int(o.projectCount ?? o.pipeline.projects.reduce((a, p) => a + p.count, 0)) }, { key: "pmw", label: "Project planned MW", cell: (o) => mw(o.projectPlannedMw, true) }, { key: "ai", label: "AI facilities", group: "AI & cloud", hint: "confirmed + likely evidence", cell: (o) => int(o.aiCount) }, { key: "cloud", label: "Cloud regions", cell: (o) => int(o.cloudRegionCount) }, { key: "newmarkets", label: "New markets entered (12 m)", group: "Expansion", cell: (o) => o.newMarkets12m.length ? ( {o.newMarkets12m.slice(0, 8).map((m) => ( {m.name} ))} {o.newMarkets12m.length > 8 && +{o.newMarkets12m.length - 8}} ) : ( none ), }, { key: "v12", label: "New facilities · 12 m", cell: (o) => int(win(o, "12m")?.newFacilities) }, { key: "v3", label: "New facilities · 3 y", cell: (o) => int(win(o, "3y")?.newFacilities) }, { key: "v5", label: "New facilities · 5 y", cell: (o) => int(win(o, "5y")?.newFacilities) }, { key: "c12", label: "New countries · 12 m", cell: (o) => int(win(o, "12m")?.newCountries) }, { key: "np12", label: "New projects · 12 m", cell: (o) => int(win(o, "12m")?.newProjects) }, { key: "recent", label: "Recent projects", group: "Projects", cell: (o) => o.recentProjects.length ? ( ) : ( none ), }, ]; } function ComparisonGrid({ ops }: { ops: Op[] }) { const defs = rows(); const cols = `minmax(150px,200px) repeat(${ops.length}, minmax(150px, 1fr))`; return ( <> {/* ≥ 640 px: one column per operator (scrolls horizontally inside its own container for 4–5 operators) */}
Metric
{ops.map((o) => (
{o.name}
{o.kind && ( {OPERATOR_KIND_LABEL[o.kind]} )} {o.hqCountryIso2 && }
))}
{defs.map((d) => (
{d.group && (
{d.group}
)}
{d.label} {d.hint &&
{d.hint}
}
{ops.map((o) => (
{d.cell(o)}
))}
))}
{/* < 640 px: stacked per-operator cards */}
{ops.map((o) => (
{o.name}
{o.kind && ( {OPERATOR_KIND_LABEL[o.kind]} )} {o.hqCountryIso2 && }
{defs .filter((d) => d.key !== "kind" && d.key !== "hq") .map((d) => (
{d.label}
{d.cell(o)}
))}
))}
); } export default async function CompareOperatorsPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const slugs = parseSlugs(sp); const r = slugs.length >= 2 ? await api.compareOperators(slugs) : null; const ops = r?.ok ? r.data.operators : []; const names: Record = {}; for (const o of ops) names[o.slug] = o.name; const missing = slugs.filter((s) => !names[s]); // slugs picked but not yet resolved (< 2 selected): resolve names for the chips if (slugs.length === 1) { const one = await api.operator(slugs[0]!); if (one.ok) names[slugs[0]!] = one.data.name; } return (
{slugs.length < 2 ? ( {PRESETS.map((p) => (
  • {p.label}
  • ))} } /> ) : r && !r.ok ? ( <> ) : ( <> {missing.length > 0 && } {ops.length >= 2 ? (
    Capacity coverage = facilities with any MW figure ÷ facilities ({ops.map((o) => `${o.name} ${fmtPct(o.coverage ?? o.mwCoverage ?? 0)}`).join(" · ")}). } />
    ) : ( )} )}
    ); }