spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import { load, type CheerioAPI } from "cheerio";2import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors";3import { areaHa, areaSqm, bodyText, cleanText, countryFromText, description, firstText, lastSlug, mwAfter, mwWithContext, parseCityLine, parseEuAddress, parseNaAddress, record, statusFromText, usStateCode, type ParsedAddress } from "./shared.js";45/**6 * QTS (q.com/data-centers/<slug>/). WordPress + Elementor; two templates:7 * US campus: <h1>Ashburn 3</h1> … "Campus footprint 25 acre campus" … "Critical campus capacity 80 MW+" (or, figure first,8 * "210 MW+ of critical campus capacity") … <h2>Ashburn 3 (ASH3)</h2>9 * <p>ASH3 DC1: 22291 Shellhorn Rd<br>Ashburn, VA 20147</p>; addresses also in Elementor hotspot tooltips.10 * EU site: <h1>Eemshaven, Netherlands</h1> … "36 MW total power" … "Huibertgatweg 2 9979 XZ Eemshaven, Netherlands".11 * Language duplicates (-nl, calatorao/vimercate ES/IT originals) are excluded in YAML; "-en" is stripped from the key.12 */13const NA_ADDR = /(\d{2,6}\s[A-Za-z0-9 .'-]+?\b(?:Rd|Road|Dr|Drive|Blvd|Boulevard|St|Street|Ave|Avenue|Pkwy|Parkway|Way|Ln|Lane|Ct|Court|Hwy|Highway|Trail|Trl|Pl|Place|Cir|Circle|Loop|Pike|Route|Rte)\.?)\s*,?\s*\n?\s*([A-Za-z .'-]+?),\s*([A-Z]{2})\s+(\d{5})\b/;14const EU_ADDR = /([A-Z][A-Za-z .'-]+?\s\d+[a-z]?)\s*,?\s*\n?\s*((?:[A-Z]{1,2}-)?\d{4,6}(?:\s?[A-Z]{2})?)\s+([A-Za-z .'-]+?)\s*,\s*([A-Za-z ]+)\b/;1516const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");1718/**19 * Elementor Google-Maps widget: data-pins='[{"address":"<lat>, <lng>","position":{"lat":…,"lng":…},"desc":"<p>2800 S Ashland Ave, Chicago, IL 60608</p>"}]'.20 * The widget is copied between page templates (the Phoenix 4 page carried the Chicago pin), so a pin is used only when21 * its own caption names the facility's postal code, or its city and state. No matching caption → no coordinates: a22 * facility never inherits another site's point.23 */24export function mapPin($: CheerioAPI, pa: ParsedAddress | null): { lat: number; lng: number } | null {25 if (!pa) return null;26 const pins: Array<Record<string, unknown>> = [];27 $("[data-pins]").each((_, el) => {28 try { const v = JSON.parse($(el).attr("data-pins") ?? "[]") as unknown; if (Array.isArray(v)) pins.push(...(v as Array<Record<string, unknown>>)); } catch { /* not JSON */ }29 });30 for (const pin of pins) {31 const pos = pin.position as { lat?: unknown; lng?: unknown } | undefined;32 const fromAddr = typeof pin.address === "string" ? pin.address.match(/^\s*(-?\d{1,2}\.\d{3,}),\s*(-?\d{1,3}\.\d{3,})\s*$/) : null;33 const lat = Number(pos?.lat ?? fromAddr?.[1]), lng = Number(pos?.lng ?? fromAddr?.[2]);34 if (!Number.isFinite(lat) || !Number.isFinite(lng) || (lat === 0 && lng === 0)) continue;35 const caption = cleanText(String(pin.desc ?? "").replace(/<[^>]+>/g, " ")) ?? "";36 if (!caption) continue;37 const samePostal = !!pa.postal && new RegExp(`\\b${escapeRe(pa.postal)}\\b`).test(caption);38 const sameCity = !!pa.city && !!pa.region && new RegExp(`\\b${escapeRe(pa.city)}\\b[,\\s]+${escapeRe(pa.region)}\\b`, "i").test(caption);39 if (samePostal || sameCity) return { lat, lng };40 }41 return null;42}4344export const qtsFacility: Parser = {45 name: "qts_facility_v1",46 version: "1.1.0",47 pageTypes: ["facility_page"],48 parse(doc: RawDocument): ExtractedRecord[] {49 const url = doc.finalUrl;50 if (!/\/data-centers\/[a-z0-9-]+\/?$/.test(url)) return [];51 const $ = load(doc.text);52 const h1 = firstText($, "h1");53 if (!h1) return [];54 const text = bodyText(doc.text);55 const h2s = $("h2").map((_, e) => cleanText($(e).text()) ?? "").get();56 const codeMatch = h2s.map((t) => t.match(/^(.+?)\s*\(([A-Z]{2,5}\d{0,2})\)$/)).find(Boolean);57 const code = codeMatch?.[2] ?? null;58 const slug = lastSlug(url).replace(/-(en|nl|es|it|fi)$/, "");5960 // label first ("Critical campus capacity 80 MW+") or figure first ("210 MW+ of critical campus capacity", "36 MW total power")61 const totalPowerMw = mwAfter(text, /Critical campus capacity/i, 80) ?? mwWithContext(text, /\s*(?:of\s+)?(?:total|totaal)\b/i) ?? mwWithContext(text, /\s*(?:aan|of)\s+(?:totaal|total)/i) ?? mwWithContext(text, /\s*(?:of\s+)?(?:critical|total)\s+(?:campus\s+)?(?:power|capacity|IT load)/i);62 const acre = text.match(/(\d+(?:\.\d+)?)\s*[- ]?acre\b/i);63 const siteAreaHa = acre ? areaHa(`${acre[1]} acres`) : areaHa(text.match(/(\d+(?:\.\d+)?)\s*hectares?\b/i)?.[0]);6465 // addresses: hotspot tooltips first, then text patterns66 const tooltips = [...doc.text.matchAll(/hotspot_tooltip_content":"(.*?)"/g)].map((m) => cleanText(m[1]!.replace(/\\u00a0/g, " ").replace(/<[^&]*>|\\\/|\\/g, " ").replace(/<[^>]+>/g, " ")) ?? "").filter(Boolean);67 let addrRaw: string | null = tooltips[0] ?? null;68 let pa = addrRaw ? parseNaAddress(addrRaw) ?? parseEuAddress(addrRaw) : null;69 if (!pa) {70 const na = text.match(NA_ADDR);71 if (na) { addrRaw = `${na[1]}, ${na[2]}, ${na[3]} ${na[4]}`; pa = parseNaAddress(addrRaw); }72 }73 if (!pa) {74 const eu = text.match(EU_ADDR);75 if (eu) { addrRaw = `${eu[1]}, ${eu[2]} ${eu[3]}, ${eu[4]}`; pa = parseEuAddress(addrRaw); }76 }77 // h1 is "Ashburn 3" / "Miami 1" / "Eemshaven, Netherlands" → the trailing site number is not part of the city78 const cl = parseCityLine(h1.replace(/\s+\d+$/, ""));79 const title = cleanText($("title").first().text()) ?? "";80 // US campus pages carry a "Proud to call Virginia home" banner → state (and therefore country) when no address parsed81 const proud = text.match(/Proud to call ([A-Z][a-z]+(?: [A-Z][a-z]+)?) home/);82 const proudState = proud ? usStateCode(proud[1]) : null;83 const countryIso2 = pa?.countryIso2 ?? cl.countryIso2 ?? (proudState ? "US" : null) ?? countryFromText(title.replace(/-\s*QTS.*$/, "").replace(/-/g, " ")) ?? null;84 const buildings = [...text.matchAll(/\b([A-Z]{2,5}\d{0,2}\s+DC\d+):\s*([^\n]+)\n?([^\n]*\d{5})?/g)].map((m) => cleanText(`${m[1]}: ${m[2]} ${m[3] ?? ""}`) ?? "").filter(Boolean);85 // Elementor Google-Maps pin, only when its caption is this facility's address (see mapPin)86 const pin = mapPin($, pa);87 const lat = pin?.lat ?? null, lng = pin?.lng ?? null;88 // "200,000 ft2 facility" / "22,000 m2 total"89 const bld = text.match(/(\d{1,3}(?:,\d{3})+|\d+)\s*(?:ft2|ft²|sq\.?\s?ft|square feet)\s+(?:facility|building|data center)/i);90 const bldM = text.match(/(\d{1,3}(?:[,.]\d{3})+|\d+)\s*(?:m2|m²|sqm|sq\.?\s?m\.?)\s+(?:total|facility|building|of)/i);91 const buildingSqm = bld ? areaSqm(`${bld[1]} sq ft`) : bldM ? areaSqm(`${bldM[1]!.replace(/[.,]/g, "")} sqm`) : null;92 const introP = $("p").filter((_, e) => (cleanText($(e).text())?.length ?? 0) > 80 && !/cookie|subscribe|validation/i.test($(e).text())).first();93 const intro = cleanText(introP.text());94 const metaDesc = description(doc.text);95 // Yoast excerpt on q.com concatenates nav/CTA text ("Schedule a tour …") → prefer the intro paragraph then96 const desc = metaDesc && !/schedule a tour|\[…\]|\[…\]/i.test(metaDesc) ? metaDesc : intro ?? metaDesc;97 const status = statusFromText(`${h1} ${intro ?? ""}`);9899 const data: Record<string, unknown> = {100 name: `QTS ${h1.split(",")[0]!.trim()}`, code, aliases: [h1, ...(code ? [`QTS ${code}`] : [])], // keep the site number ("QTS Ashburn 3") — several campuses share a city101 address: pa?.address ?? null, city: pa?.city ?? cl.city, regionName: pa?.region ?? cl.region ?? proudState, postalCode: pa?.postal ?? null, countryIso2,102 lat, lng, geoPrecision: pin ? "exact" : null,103 totalPowerMw, siteAreaHa, buildingSqm,104 status, description: [desc, buildings.length ? `Buildings: ${buildings.join("; ")}.` : null].filter(Boolean).join(" ") || null,105 externalIds: { qts_slug: slug },106 };107 const methods: Record<string, string> = {108 name: "selector:h1", code: "selector:h2+regex:(CODE)", address: addrRaw && tooltips.length ? "elementor:hotspot_tooltip+parse" : "regex:address_v1", city: pa?.city ? "regex:address_v1" : "selector:h1+parse", regionName: "regex:address_v1", postalCode: "regex:address_v1", countryIso2: "regex:address_v1>country",109 lat: "elementor:google_maps.pin(caption=address)", lng: "elementor:google_maps.pin(caption=address)", geoPrecision: "elementor:google_maps.pin(caption=address)",110 totalPowerMw: "regex:critical_campus_capacity_v1>mw", siteAreaHa: "regex:acre_campus>ha", buildingSqm: "regex:facility_sqft>sqm", status: "regex:status_v1", description: desc === metaDesc ? "meta:description" : "selector:p(intro)", externalIds: "url:slug",111 };112 return [record("facility", `qts:${slug}`, url, data, methods, 0.85)];113 },114};115