spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import { load } from "cheerio";2import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors";3import { embeddedJson, jsonPath } from "@dci/connectors";4import { certificationsFrom, cleanText, countryFromText, description, parseAddress, record, statusFromText } from "./shared.js";56/**7 * Digital Realty facility pages (www.digitalrealty.com/data-centers/<region>/<metro>/<code>). The page is Next.js:8 * `__NEXT_DATA__.props.pageProps.data` is the Drupal node (content_type "location") with typed fields:9 * field_site_code_location, field_address_location (html), field_latitude/longitude, field_total_building_size_in_m2,10 * field_certifications_compliance[].name, field_certifications_sustainabil[].name, field_available_clouds[].name,11 * field_design_pue, field_ups_redundancy, field_carbon_free_energy (%), field_renewable_energy (%), metro, country.12 * `field_utility_power_capacity` has inconsistent units across pages (16000 vs 120) → deliberately NOT used.13 */14type Node = Record<string, unknown>;15const first = (n: Node, k: string): unknown => { const a = n[k]; return Array.isArray(a) && a[0] && typeof a[0] === "object" ? (a[0] as { value?: unknown }).value : undefined; };16const names = (n: Node, k: string): string[] => { const a = n[k]; return Array.isArray(a) ? a.map((o) => cleanText(String((o as { name?: unknown }).name ?? "")) ?? "").filter(Boolean) : []; };17const num = (v: unknown): number | null => { const x = Number(String(v ?? "").replace(/[^\d.-]/g, "")); return Number.isFinite(x) && x !== 0 ? x : null; };1819export const digitalRealtyFacility: Parser = {20 name: "digitalrealty_facility_v1",21 version: "1.0.0",22 pageTypes: ["facility_page"],23 parse(doc: RawDocument): ExtractedRecord[] {24 const html = doc.text;25 const blob = embeddedJson(html).find((b) => b.id === "__NEXT_DATA__");26 const node = blob ? (jsonPath(blob.data, "props.pageProps.data") as Node | undefined) : undefined;27 if (!node || node.content_type !== "location") return [];28 const code = cleanText(String(first(node, "field_site_code_location") ?? node.title ?? ""))?.toUpperCase();29 if (!code) return [];30 const $ = load(html);31 const h1 = cleanText($("h1.hero-title").first().text()) ?? cleanText($("h1").first().text());32 const metro = cleanText(String(node.metro ?? "")) ;33 const country = cleanText(String(node.country ?? ""));34 const addrText = cleanText(String(first(node, "field_address_location") ?? "").replace(/<[^>]+>/g, " "));35 const pa = parseAddress(addrText);36 const lat = num(first(node, "field_latitude")), lng = num(first(node, "field_longitude"));37 const m2 = num(first(node, "field_total_building_size_in_m2"));38 const ft2 = num(first(node, "field_total_building_size_in_ft2"));39 const buildingSqm = m2 ?? (ft2 ? Math.round(ft2 * 0.09290304) : null);40 const pue = num(first(node, "field_design_pue"));41 const certifications = certificationsFrom([...names(node, "field_certifications_compliance"), ...names(node, "field_certifications_sustainabil")]);42 const cloudProviders = [...new Set(names(node, "field_available_clouds").map((n) => n.replace(/\s*\(.*?\)\s*/g, "").trim()).filter(Boolean))];43 const cfe = num(first(node, "field_carbon_free_energy"));44 const ren = num(first(node, "field_renewable_energy"));45 const renewableClaim = cfe ? `${cfe}% carbon-free energy${ren ? `; ${ren}% renewable energy` : ""} (operator-reported)` : ren ? `${ren}% renewable energy (operator-reported)` : null;46 const metaTags = Array.isArray(node.field_meta_tags) ? (node.field_meta_tags[0] as { description?: string } | undefined) : undefined;47 const desc = cleanText(metaTags?.description) ?? description(html);48 const intro = cleanText(String(first(node, "field_intro_location") ?? "").replace(/<[^>]+>/g, " "));49 const ups = cleanText(String(first(node, "field_ups_redundancy") ?? ""));50 const coolingRed = cleanText(String(first(node, "field_cooling_plant_redundancy") ?? ""));51 const extra = [ups ? `UPS redundancy: ${ups}.` : null, coolingRed ? `Cooling redundancy: ${coolingRed}.` : null].filter(Boolean).join(" ");52 // node.country is usually a country name ("USA", "Germany") but is sometimes junk ("Bogota") → fall back to the page text53 const countryIso2 = (country ? countryFromText(country) : null) ?? pa?.countryIso2 ?? countryFromText(`${intro ?? ""} ${desc ?? ""}`) ?? null;54 // metro is a city name for nearly all DLR sites; skip the few market names that are not cities55 const metroIsCity = metro && !/northern virginia|silicon valley|dallas[- ]fort worth|bay area|randstad|nordic|benelux/i.test(metro);56 const city = pa?.city ?? (metroIsCity ? metro : null);57 const name = h1 && h1.toUpperCase().includes(code) ? h1 : `${metro ?? "Digital Realty"} ${code}`;5859 const data: Record<string, unknown> = {60 name, code, aliases: [`Digital Realty ${code}`],61 address: pa?.address ?? addrText, city, regionName: pa?.region ?? null, postalCode: pa?.postal ?? null, countryIso2,62 lat, lng, geoPrecision: "exact",63 buildingSqm, pue, certifications, cloudProviders, renewableClaim,64 status: statusFromText(`${h1 ?? ""} ${intro ?? ""}`),65 campusName: metro && /campus/i.test(intro ?? "") ? `${metro} campus` : null,66 description: [desc, extra].filter(Boolean).join(" ") || null,67 externalIds: { digitalrealty_node: String(node.node_id ?? ""), digitalrealty_site_code: code },68 };69 const methods: Record<string, string> = {70 name: "selector:h1.hero-title", code: "next-data:field_site_code_location", address: "next-data:field_address_location", city: pa?.city ? "next-data:field_address_location+parse" : "next-data:metro", regionName: "next-data:field_address_location+parse", postalCode: "next-data:field_address_location+parse",71 countryIso2: "next-data:country>country", lat: "next-data:field_latitude", lng: "next-data:field_longitude", buildingSqm: "next-data:field_total_building_size_in_m2", pue: "next-data:field_design_pue",72 certifications: "next-data:field_certifications_*", cloudProviders: "next-data:field_available_clouds", renewableClaim: "next-data:field_carbon_free_energy", status: "regex:status_v1", campusName: "regex:campus", description: "next-data:field_meta_tags", externalIds: "next-data:node_id",73 };74 return [record("facility", `digitalrealty:${code}`, doc.finalUrl, data, methods, 0.95)];75 },76};77