SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
6.3 KB · 77 lines typescript
Raw Blame History
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