import { load } from "cheerio"; import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors"; import { embeddedJson, jsonPath } from "@dci/connectors"; import { certificationsFrom, cleanText, countryFromText, description, parseAddress, record, statusFromText } from "./shared.js"; /** * Digital Realty facility pages (www.digitalrealty.com/data-centers///). The page is Next.js: * `__NEXT_DATA__.props.pageProps.data` is the Drupal node (content_type "location") with typed fields: * field_site_code_location, field_address_location (html), field_latitude/longitude, field_total_building_size_in_m2, * field_certifications_compliance[].name, field_certifications_sustainabil[].name, field_available_clouds[].name, * field_design_pue, field_ups_redundancy, field_carbon_free_energy (%), field_renewable_energy (%), metro, country. * `field_utility_power_capacity` has inconsistent units across pages (16000 vs 120) → deliberately NOT used. */ type Node = Record; const 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; }; const 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) : []; }; const num = (v: unknown): number | null => { const x = Number(String(v ?? "").replace(/[^\d.-]/g, "")); return Number.isFinite(x) && x !== 0 ? x : null; }; export const digitalRealtyFacility: Parser = { name: "digitalrealty_facility_v1", version: "1.0.0", pageTypes: ["facility_page"], parse(doc: RawDocument): ExtractedRecord[] { const html = doc.text; const blob = embeddedJson(html).find((b) => b.id === "__NEXT_DATA__"); const node = blob ? (jsonPath(blob.data, "props.pageProps.data") as Node | undefined) : undefined; if (!node || node.content_type !== "location") return []; const code = cleanText(String(first(node, "field_site_code_location") ?? node.title ?? ""))?.toUpperCase(); if (!code) return []; const $ = load(html); const h1 = cleanText($("h1.hero-title").first().text()) ?? cleanText($("h1").first().text()); const metro = cleanText(String(node.metro ?? "")) ; const country = cleanText(String(node.country ?? "")); const addrText = cleanText(String(first(node, "field_address_location") ?? "").replace(/<[^>]+>/g, " ")); const pa = parseAddress(addrText); const lat = num(first(node, "field_latitude")), lng = num(first(node, "field_longitude")); const m2 = num(first(node, "field_total_building_size_in_m2")); const ft2 = num(first(node, "field_total_building_size_in_ft2")); const buildingSqm = m2 ?? (ft2 ? Math.round(ft2 * 0.09290304) : null); const pue = num(first(node, "field_design_pue")); const certifications = certificationsFrom([...names(node, "field_certifications_compliance"), ...names(node, "field_certifications_sustainabil")]); const cloudProviders = [...new Set(names(node, "field_available_clouds").map((n) => n.replace(/\s*\(.*?\)\s*/g, "").trim()).filter(Boolean))]; const cfe = num(first(node, "field_carbon_free_energy")); const ren = num(first(node, "field_renewable_energy")); const renewableClaim = cfe ? `${cfe}% carbon-free energy${ren ? `; ${ren}% renewable energy` : ""} (operator-reported)` : ren ? `${ren}% renewable energy (operator-reported)` : null; const metaTags = Array.isArray(node.field_meta_tags) ? (node.field_meta_tags[0] as { description?: string } | undefined) : undefined; const desc = cleanText(metaTags?.description) ?? description(html); const intro = cleanText(String(first(node, "field_intro_location") ?? "").replace(/<[^>]+>/g, " ")); const ups = cleanText(String(first(node, "field_ups_redundancy") ?? "")); const coolingRed = cleanText(String(first(node, "field_cooling_plant_redundancy") ?? "")); const extra = [ups ? `UPS redundancy: ${ups}.` : null, coolingRed ? `Cooling redundancy: ${coolingRed}.` : null].filter(Boolean).join(" "); // node.country is usually a country name ("USA", "Germany") but is sometimes junk ("Bogota") → fall back to the page text const countryIso2 = (country ? countryFromText(country) : null) ?? pa?.countryIso2 ?? countryFromText(`${intro ?? ""} ${desc ?? ""}`) ?? null; // metro is a city name for nearly all DLR sites; skip the few market names that are not cities const metroIsCity = metro && !/northern virginia|silicon valley|dallas[- ]fort worth|bay area|randstad|nordic|benelux/i.test(metro); const city = pa?.city ?? (metroIsCity ? metro : null); const name = h1 && h1.toUpperCase().includes(code) ? h1 : `${metro ?? "Digital Realty"} ${code}`; const data: Record = { name, code, aliases: [`Digital Realty ${code}`], address: pa?.address ?? addrText, city, regionName: pa?.region ?? null, postalCode: pa?.postal ?? null, countryIso2, lat, lng, geoPrecision: "exact", buildingSqm, pue, certifications, cloudProviders, renewableClaim, status: statusFromText(`${h1 ?? ""} ${intro ?? ""}`), campusName: metro && /campus/i.test(intro ?? "") ? `${metro} campus` : null, description: [desc, extra].filter(Boolean).join(" ") || null, externalIds: { digitalrealty_node: String(node.node_id ?? ""), digitalrealty_site_code: code }, }; const methods: Record = { 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", 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", 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", }; return [record("facility", `digitalrealty:${code}`, doc.finalUrl, data, methods, 0.95)]; }, };