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 { CA_PROVINCES, areaHa, areaSqm, bodyText, cleanText, description, extractCode, firstText, parseMw, record, statusFromText, usStateCode } from "./shared.js";45/**6 * STACK Infrastructure campus pages (www.stackinfra.com/locations/<region>/<metro>/<code>/).7 * <h1>ATL01 – Alpharetta</h1>; campus tiles p.mb-0.uppercase ("20MW", "2 Buildings", "18 Acres"); building cards8 * h3.title-campus (ATL01A) with div.flex.items-end values ("105,000 SQ FT", "8MW"); gallery caption "ATL01 Campus – Alpharetta, GA".9 * No street address or coordinates are published. Country comes from the US state / CA province in the caption, else from the10 * metro segment of the URL (fixed table of STACK metros; deterministic, not geocoding). One record for the campus + one per building.11 */12const METRO_COUNTRY: Record<string, string> = { atlanta: "US", chicago: "US", portland: "US", phoenix: "US", "northern-virginia": "US", "silicon-valley": "US", "dallas-fort-worth": "US", "new-albany": "US", toronto: "CA", calgary: "CA", milan: "IT", zurich: "CH", geneva: "CH", oslo: "NO", frankfurt: "DE", copenhagen: "DK", stockholm: "SE", tokyo: "JP", osaka: "JP", sydney: "AU", melbourne: "AU", "johor-bahru": "MY" };1314export const stackCampus: Parser = {15 name: "stack_campus_v1",16 version: "1.0.0",17 pageTypes: ["facility_page"],18 parse(doc: RawDocument): ExtractedRecord[] {19 const url = doc.finalUrl;20 const m = url.match(/\/locations\/(americas|emea|asia-pacific)\/([a-z-]+)\/([a-z0-9-]+)\/?$/);21 if (!m) return [];22 const metro = m[2]!, slug = m[3]!;23 const $ = load(doc.text);24 const h1 = firstText($, "h1");25 if (!h1) return [];26 const [codePart, cityPart] = h1.split(/\s[–—-]\s/);27 const code = extractCode(codePart) ?? slug.toUpperCase();28 let city = cleanText(cityPart) ?? null;29 const text = bodyText(doc.text);30 const cap = text.match(new RegExp(`${code}[^\\n]{0,24}[–—-]\\s*([A-Za-z .'-]+?),\\s*([A-Z]{2})\\b`));31 const regionName = cap?.[2] && (usStateCode(cap[2]) || CA_PROVINCES.has(cap[2])) ? cap[2] : null;32 city ??= cap?.[1] ? cleanText(cap[1]) : null;33 const countryIso2 = regionName ? (usStateCode(regionName) ? "US" : "CA") : (METRO_COUNTRY[metro] ?? null);3435 let totalPowerMw: number | null = null, siteAreaHa: number | null = null;36 $("p.mb-0.uppercase").each((_, e) => {37 const v = cleanText($(e).text());38 if (!v) return;39 if (/\d\s*MW\b/i.test(v) && totalPowerMw == null) totalPowerMw = parseMw(v);40 else if (/acre|hectare|\bha\b/i.test(v) && siteAreaHa == null) siteAreaHa = areaHa(v);41 });42 const designed = text.match(/designed (?:for|to deliver|to support)\s+(\d+(?:\.\d+)?)\s*MW/i);43 const plannedPowerMw = designed ? parseMw(`${designed[1]} MW`) : null;44 const introH2 = $("h2").filter((_, e) => new RegExp(`${code}\\s+Campus$`, "i").test($(e).text())).first();45 const intro = cleanText(introH2.nextAll("p").first().text()) ?? cleanText(introH2.parent().find("p").first().text()) ?? cleanText($("p").filter((_, p) => { const t = $(p).text(); return t.length > 80 && /campus/i.test(t); }).first().text());46 // Yoast builds the meta description by concatenating the page ("…Schedule a tour ATL01 Campus…") → only use it when it reads as prose47 const metaDesc = description(doc.text);48 const desc = metaDesc && !/schedule a tour|connect with us/i.test(metaDesc) ? metaDesc : null;49 const status = statusFromText(`${h1} ${intro ?? ""}`);50 const campusName = `STACK ${code}`;51 const common = { city, regionName, countryIso2, campusName };52 const records: ExtractedRecord[] = [];53 records.push(record("facility", `stack:${code.toLowerCase()}`, url, {54 name: `STACK ${code}${city ? ` – ${city}` : ""}`, code, aliases: [`${code} Campus`], ...common,55 totalPowerMw, plannedPowerMw: plannedPowerMw && plannedPowerMw !== totalPowerMw ? plannedPowerMw : null, siteAreaHa, status, description: intro ?? desc,56 externalIds: { stack_slug: slug },57 }, {58 name: "selector:h1", code: "selector:h1+regex", city: cityPart ? "selector:h1" : "regex:caption", regionName: "regex:caption(City, ST)", countryIso2: regionName ? "regex:caption>country" : "url:metro-table", campusName: "selector:h1",59 totalPowerMw: "selector:p.mb-0.uppercase>mw", plannedPowerMw: "regex:designed_for_mw", siteAreaHa: "selector:p.mb-0.uppercase>ha", status: "regex:status_v1", description: "selector:h2(Campus)+p", externalIds: "url:slug",60 }, 0.85));6162 $("h3.title-campus").each((_, e) => {63 const bcode = cleanText($(e).text())?.toUpperCase();64 if (!bcode || !/^[A-Z]{2,4}\d{1,3}[A-Z]?$/.test(bcode)) return;65 const vals = $(e).parent().find("div.flex.items-end").map((_, v) => cleanText($(v).text()) ?? "").get();66 let bSqm: number | null = null, bMw: number | null = null;67 for (const v of vals) { if (/SQ\.?\s?FT|SQFT|square feet/i.test(v)) bSqm ??= areaSqm(v); else if (/sqm|m²|square met/i.test(v)) bSqm ??= areaSqm(v); else if (/\d\s*MW\b/i.test(v)) bMw ??= parseMw(v); }68 records.push(record("facility", `stack:${bcode.toLowerCase()}`, url, {69 name: `STACK ${bcode}${city ? ` – ${city}` : ""}`, code: bcode, ...common, buildingSqm: bSqm, totalPowerMw: bMw, status,70 description: `Building ${bcode} on the STACK ${code} campus${city ? ` in ${city}` : ""}.`, externalIds: { stack_campus: code },71 }, { name: "selector:h3.title-campus", code: "selector:h3.title-campus", city: "selector:h1", regionName: "regex:caption(City, ST)", countryIso2: regionName ? "regex:caption>country" : "url:metro-table", campusName: "selector:h1", buildingSqm: "selector:div.flex.items-end>sqm", totalPowerMw: "selector:div.flex.items-end>mw", status: "regex:status_v1", description: "derived:campus", externalIds: "selector:h1" }, 0.85));72 });73 return records;74 },75};76