spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { ApiEndpointDoc } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { Breadcrumbs } from "@/components/entity/breadcrumbs";5import { ExtLink, KeyValueList } from "@/components/entity/kv";6import { SectionNav } from "@/components/entity/section-nav";7import { SourcesFooter } from "@/components/entity/sources-footer";8import { CodeTabs } from "@/components/intelligence/developers/code-tabs";9import { Badge } from "@/components/ui/badge";10import { Button } from "@/components/ui/button";11import { Page, PageHeader, Section } from "@/components/ui/card";12import { EmptyState, OfflineNotice } from "@/components/ui/states";13import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";14import { api } from "@/lib/api";15import { fmtInt } from "@/lib/format";16import { routes, SITE_URL } from "@/lib/routes";17import { pageMetadata } from "@/lib/seo";1819export const revalidate = 600;2021export const metadata: Metadata = pageMetadata({22 title: "API",23 description: "DataCenterIndex public REST API: every endpoint with parameters, response types and copy-ready curl / JavaScript / Python examples. Envelope { data, meta, sources }, weak ETags, per-IP rate limits, OpenAPI 3.1.",24 path: routes.apiDocsPage(),25});2627const GROUP_LABEL: Record<string, string> = {28 facilities: "Facilities",29 projects: "Projects",30 operators: "Operators",31 countries: "Countries",32 metros: "Markets",33 "cloud-regions": "Cloud regions",34 ixps: "Internet exchanges",35 events: "Events",36 rankings: "Rankings",37 dashboard: "Dashboard",38 stats: "Stats",39 map: "Map",40 search: "Search",41 nearby: "Nearby",42 explore: "Explore",43 compare: "Compare",44 ai: "AI infrastructure",45 pulse: "Pulse",46 power: "Power",47 connectivity: "Connectivity",48 coverage: "Coverage",49 "time-machine": "Time machine",50 sources: "Sources",51 download: "Download",52 watchlist: "Watchlist",53 sitemap: "Sitemap",54 docs: "Docs",55};56const GROUP_ORDER = ["facilities", "projects", "operators", "countries", "metros", "cloud-regions", "ixps", "events", "rankings", "dashboard", "stats", "pulse", "ai", "power", "connectivity", "coverage", "explore", "compare", "search", "map", "nearby", "time-machine", "sources", "download", "watchlist", "sitemap", "docs"];5758const linkCls = "text-ink underline decoration-hair-2 hover:text-accent";5960function rewrite(s: string): string {61 return s.replace(/https?:\/\/localhost:8310/g, SITE_URL).replace(/https?:\/\/127\.0\.0\.1:8311/g, SITE_URL);62}6364function MethodBadge({ method }: { method: ApiEndpointDoc["method"] }) {65 return (66 <Badge tone={method === "GET" ? "ok" : method === "POST" ? "accent" : "danger"} size="xs" mono>67 {method}68 </Badge>69 );70}7172export default async function DevelopersPage() {73 const r = await api.docsMeta();74 const docs = r.ok ? r.data : [];75 const groups = Array.from(new Set(docs.map((d) => d.group))).sort((a, b) => {76 const ia = GROUP_ORDER.indexOf(a);77 const ib = GROUP_ORDER.indexOf(b);78 return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);79 });80 const byGroup = new Map(groups.map((g) => [g, docs.filter((d) => d.group === g).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))]));8182 return (83 <Page>84 <Breadcrumbs items={[{ name: "API", href: routes.apiDocsPage() }]} className="pt-3" />85 <PageHeader86 eyebrow="Developers"87 title="Public API"88 description={`${docs.length ? `${fmtInt(docs.length)} endpoints. ` : ""}Read-only JSON over HTTPS, the same data the site renders — every figure carries its sources.`}89 className="pt-3"90 aside={91 <div className="flex flex-wrap gap-2">92 <Button size="sm" href={routes.apiDocs()} external>93 Swagger UI94 </Button>95 <Button size="sm" variant="ghost" href="/api/v1/openapi.json" external>96 OpenAPI 3.1 JSON97 </Button>98 </div>99 }100 />101 {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}102103 <div className="rounded-sm border border-hair bg-surface px-3 py-3 sm:px-4">104 <KeyValueList105 items={[106 { label: "Base URL", value: <span className="figure text-[12.5px]">{SITE_URL}/api/v1</span> },107 {108 label: "Envelope",109 value: (110 <span>111 Every response is <span className="figure text-[12.5px]">{"{ data, meta, sources }"}</span> — <em>data</em> is the typed payload, <em>meta</em> carries pagination (<span className="figure text-[12px]">total, page, perPage</span>), <span className="figure text-[12px]">generatedAt</span> and a <span className="figure text-[12px]">methodology</span> sentence when a figure needs one, <em>sources</em> lists the sources (licence, attribution) behind the rows. Downloads stream without an envelope.112 </span>113 ),114 },115 { label: "Caching", value: "Weak ETags + s-maxage on every GET — send If-None-Match and expect 304 on unchanged data; lists revalidate within minutes, the change feed within 30 s." },116 { label: "Rate limits", value: "Rate-limited per IP; the current window is reported in the x-ratelimit-limit and x-ratelimit-remaining response headers. Bulk use should go through the download endpoints." },117 { label: "Errors", value: <span className="figure text-[12.5px]">{'{ "error": "…", "statusCode": 400 | 404 | 429 }'}</span> },118 { label: "Partial dates", value: <span className="figure text-[12.5px]">2027 · 2027-06 · 2027-06-15 · 2027-Q2 · 2027-H1</span> },119 { label: "Honesty", value: "null means not published — never zero. Estimates carry isEstimate; MW figures come with capacityScope / capacitySemantics; coverage fields say how much of an aggregate is backed by a figure." },120 ]}121 />122 <p className="mt-2 text-[11.5px] text-ink-3">123 Types are named after the shared contract (<span className="figure">packages/core/src/api-types.ts</span>); the <Link href={routes.methodology()} className={linkCls}>methodology</Link> explains how each value is produced. Re-use terms per source on <Link href={routes.sources()} className={linkCls}>/sources</Link>.124 </p>125 </div>126127 {docs.length === 0 ? (128 <EmptyState className="mt-6" title="Endpoint catalogue unavailable" description="The API did not return /docs-meta; the Swagger UI link above still works when the API is up." />129 ) : (130 <>131 <SectionNav items={groups.map((g) => ({ id: `g-${g}`, label: GROUP_LABEL[g] ?? g, count: byGroup.get(g)?.length ?? 0 }))} className="mt-4" />132 <div className="mt-6 flex flex-col gap-10">133 {groups.map((g) => (134 <Section key={g} id={`g-${g}`} title={GROUP_LABEL[g] ?? g} description={`${byGroup.get(g)?.length ?? 0} ${(byGroup.get(g)?.length ?? 0) === 1 ? "endpoint" : "endpoints"}`} className="scroll-mt-28">135 <div className="flex flex-col gap-6">136 {(byGroup.get(g) ?? []).map((d) => (137 <Endpoint key={`${d.method} ${d.path}`} d={d} />138 ))}139 </div>140 </Section>141 ))}142 </div>143 </>144 )}145146 <SourcesFooter sources={r.ok ? r.sources : undefined} note="The API serves reconciled, validated records with their provenance; the same limits (published figures only, coverage caveats) apply to every payload." />147 </Page>148 );149}150151function Endpoint({ d }: { d: ApiEndpointDoc }) {152 const id = `${d.method}-${d.path.replace(/[^a-z0-9]+/gi, "-")}`;153 return (154 <article id={id} className="rounded-sm border border-hair bg-surface" aria-labelledby={`${id}-h`}>155 <div className="flex flex-wrap items-center gap-2 border-b border-hair px-3 py-2.5 sm:px-4">156 <MethodBadge method={d.method} />157 <h3 id={`${id}-h`} className="figure min-w-0 break-all text-[13.5px] font-semibold text-ink">158 {d.path}159 </h3>160 <span className="ml-auto figure text-[11.5px] text-ink-3" title="Response type (data)">161 → {d.responseSchema}162 </span>163 </div>164 <div className="grid gap-4 px-3 py-3 sm:px-4 lg:grid-cols-2">165 <div className="min-w-0">166 <p className="text-[13px] leading-relaxed text-ink-2">{d.summary}</p>167 {d.params.length > 0 ? (168 <div className="mt-3">169 <Table dense>170 <THead>171 <tr>172 <Th>Parameter</Th>173 <Th className="hidden sm:table-cell">In</Th>174 <Th>Type</Th>175 <Th className="hidden md:table-cell">Description</Th>176 </tr>177 </THead>178 <TBody>179 {d.params.map((p) => (180 <Tr key={`${p.in}-${p.name}`}>181 <Td primary>182 <span className="figure text-[12.5px] font-medium">{p.name}</span>183 {p.example && (184 <div className="figure text-[11px] text-ink-4 md:hidden">185 e.g. {p.example}186 </div>187 )}188 </Td>189 <Td label="In" className="hidden text-[12px] text-ink-3 sm:table-cell">190 {p.in}191 </Td>192 <Td label="Type" className="figure text-[12px] text-ink-2">193 {p.type}194 </Td>195 <Td label="Description" className="hidden text-[12px] text-ink-2 md:table-cell">196 {p.description || <span className="text-ink-4">—</span>}197 {p.example && <span className="figure text-ink-4"> · e.g. {p.example}</span>}198 </Td>199 </Tr>200 ))}201 </TBody>202 </Table>203 </div>204 ) : (205 <p className="mt-2 text-[11.5px] text-ink-4">No parameters.</p>206 )}207 <p className="mt-2 text-[11.5px] text-ink-3">208 Try it: <ExtLink href={`${SITE_URL}${d.path.replace(/:[a-z]+/gi, (m) => (d.params.find((p) => p.in === "path" && `:${p.name}` === m)?.example ?? m))}`} className="figure text-[11.5px]" />209 </p>210 </div>211 <div className="min-w-0">212 <CodeTabs id={d.path} example={{ curl: rewrite(d.example.curl), js: rewrite(d.example.js), python: rewrite(d.example.python) }} />213 </div>214 </div>215 </article>216 );217}218