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 { certificationsFrom, cleanText, coolingFrom, description, firstText, lastSlug, mwWithContext, parseCityLine, parseMw, record, renewableFrom, statusFromText } from "./shared.js";45/**6 * CyrusOne facility pages (www.cyrusone.com/data-centers/<region>/<slug>). HubSpot CMS:7 * <h1>Frankfurt, Germany</h1><h3>FRA5</h3>, breadcrumb #hs_cos_wrapper_name "Frankfurt, Germany: FRA5",8 * "Key Features" tiles: <h3>54 MW</h3><p>Total IT Capacity</p>, <h3>18,000 sq. m.</h3><p>Technical IT Space</p>,9 * accordions "Technical Specifications" / "Sustainability" (li/span items).10 * No street address and no coordinates are published → geo left null (city-level from other sources).11 */12export const cyrusOneFacility: Parser = {13 name: "cyrusone_facility_v1",14 version: "1.0.0",15 pageTypes: ["facility_page"],16 parse(doc: RawDocument): ExtractedRecord[] {17 const url = doc.finalUrl;18 if (!/\/data-centers\/(north-america|emea|apac)\/[a-z0-9-]+\/?$/.test(url)) return [];19 const $ = load(doc.text);20 const h1 = firstText($, "h1");21 if (!h1) return [];22 const codeLine = cleanText($("h1").first().nextAll("h3").first().text());23 const codes = codeLine ? (codeLine.toUpperCase().match(/\b[A-Z]{2,4}\d{1,2}[A-Z]?\b/g) ?? []) : [];24 // h1 is normally "City, ST" / "City, Country"; campus pages ("Yorkville Technology Campus") name the place in the meta description ("… campus in Yorkville, Illinois")25 let cl = parseCityLine(h1);26 const metaDesc = description(doc.text);27 if (!h1.includes(",") || !cl.countryIso2) {28 const inLoc = metaDesc?.match(/\b(?:in|near|located in)\s+([A-Z][A-Za-z .'-]+?),\s*([A-Z][A-Za-z .]+?)(?:[,.;]|\s-|$)/);29 if (inLoc) { const alt = parseCityLine(`${inLoc[1]}, ${inLoc[2]}`); if (alt.countryIso2) cl = alt; }30 if (!h1.includes(",") && cl.city === h1) cl = { ...cl, city: null };31 }32 const slug = lastSlug(url);33 const key = codes.length === 1 ? `cyrusone:${codes[0]}` : `cyrusone:${slug}`;34 const name = codes.length ? `CyrusOne ${codes.join("/")} (${cl.city ?? h1})` : `CyrusOne ${h1}`;3536 // Key Features tiles37 let itCapacityMw: number | null = null;38 let techSpace: string | null = null;39 $("h3").each((_, e) => {40 const v = cleanText($(e).text());41 const label = cleanText($(e).nextAll("p").first().text()) ?? cleanText($(e).parent().find("p").first().text());42 if (!v || !label || !/\d/.test(v)) return; // tile values are figures ("54 MW", "18,000 sq. m."); headings like "Key Features:" are not43 if (/IT capacity|critical (?:IT )?(?:load|power)/i.test(label) && itCapacityMw == null) itCapacityMw = parseMw(v);44 if (/technical (?:IT )?space|white ?space|data hall space|raised floor/i.test(label) && !techSpace) techSpace = v;45 });46 const text = cleanText($("main, body").first().text()) ?? "";47 const techSpaceStr = techSpace as string | null; // assigned inside the .each callback (TS narrows the let to null otherwise)48 itCapacityMw ??= mwWithContext(text, /\s*(?:of\s+)?(?:total\s+)?IT\s+capacity/i) ?? mwWithContext(text, /\s*(?:of\s+)?critical\s+(?:IT\s+)?load/i);4950 const accordionItems = $("li, span").filter((_, e) => $(e).children().length === 0).map((_, e) => cleanText($(e).text()) ?? "").get().filter((t) => t.length > 2 && t.length < 160);51 const certifications = certificationsFrom(accordionItems.filter((t) => /ISO|SOC|PCI|HIPAA|LEED|BREEAM|Uptime|Tier|NIST|FedRAMP|HITRUST|EN\s?50600/i.test(t)));52 const sustText = accordionItems.filter((t) => /renewable|carbon|green|wind|solar|water|PUE|WUE/i.test(t)).join(". ");53 const h2 = firstText($, "h2");54 const firstP = cleanText($("h2").first().nextAll("div, p").find("p").first().text()) ?? cleanText($("h2").first().next("p").text());55 const desc = description(doc.text);56 const status = statusFromText(`${h1} ${codeLine ?? ""} ${h2 ?? ""} ${firstP ?? ""}`);5758 const data: Record<string, unknown> = {59 name, code: codes.length === 1 ? codes[0] : null, aliases: codes.map((c) => `CyrusOne ${c}`),60 city: cl.city, regionName: cl.region, countryIso2: cl.countryIso2,61 itCapacityMw, certifications, renewableClaim: renewableFrom(sustText), coolingType: coolingFrom(`${sustText} ${accordionItems.join(". ")}`),62 status, description: [desc, techSpaceStr ? `Technical IT space: ${techSpaceStr.replace(/\.+$/, "")}.` : null].filter(Boolean).join(" ") || null,63 externalIds: { cyrusone_slug: slug },64 };65 const methods: Record<string, string> = {66 name: "selector:h1+h3", code: "selector:h1~h3", city: "selector:h1+parse", regionName: "selector:h1+parse", countryIso2: "selector:h1+parse>country",67 itCapacityMw: "selector:key-features(h3+p 'IT Capacity')>mw", certifications: "selector:accordion li+regex", renewableClaim: "selector:accordion li+regex", coolingType: "regex:cooling_v1",68 status: "regex:status_v1", description: "meta:description+key-features", externalIds: "url:slug",69 };70 return [record("facility", key, url, data, methods, codes.length ? 0.9 : 0.75)];71 },72};73