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 ? (
{o.recentProjects.slice(0, 3).map((p) => (
-
{p.name}
{p.plannedMw !== null ? `≈ ${fmtMw(p.plannedMw)}` : "MW —"}
{p.countryIso2 ? ` · ${p.countryIso2}` : ""}
))}
) : (
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(" · ")}). >} />
) : (
)}
>
)}
);
}