import { load } from "cheerio"; import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors"; import { CA_PROVINCES, areaHa, areaSqm, bodyText, cleanText, description, extractCode, firstText, parseMw, record, statusFromText, usStateCode } from "./shared.js"; /** * STACK Infrastructure campus pages (www.stackinfra.com/locations////). *

ATL01 – Alpharetta

; campus tiles p.mb-0.uppercase ("20MW", "2 Buildings", "18 Acres"); building cards * h3.title-campus (ATL01A) with div.flex.items-end values ("105,000 SQ FT", "8MW"); gallery caption "ATL01 Campus – Alpharetta, GA". * No street address or coordinates are published. Country comes from the US state / CA province in the caption, else from the * metro segment of the URL (fixed table of STACK metros; deterministic, not geocoding). One record for the campus + one per building. */ const METRO_COUNTRY: Record = { 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" }; export const stackCampus: Parser = { name: "stack_campus_v1", version: "1.0.0", pageTypes: ["facility_page"], parse(doc: RawDocument): ExtractedRecord[] { const url = doc.finalUrl; const m = url.match(/\/locations\/(americas|emea|asia-pacific)\/([a-z-]+)\/([a-z0-9-]+)\/?$/); if (!m) return []; const metro = m[2]!, slug = m[3]!; const $ = load(doc.text); const h1 = firstText($, "h1"); if (!h1) return []; const [codePart, cityPart] = h1.split(/\s[–—-]\s/); const code = extractCode(codePart) ?? slug.toUpperCase(); let city = cleanText(cityPart) ?? null; const text = bodyText(doc.text); const cap = text.match(new RegExp(`${code}[^\\n]{0,24}[–—-]\\s*([A-Za-z .'-]+?),\\s*([A-Z]{2})\\b`)); const regionName = cap?.[2] && (usStateCode(cap[2]) || CA_PROVINCES.has(cap[2])) ? cap[2] : null; city ??= cap?.[1] ? cleanText(cap[1]) : null; const countryIso2 = regionName ? (usStateCode(regionName) ? "US" : "CA") : (METRO_COUNTRY[metro] ?? null); let totalPowerMw: number | null = null, siteAreaHa: number | null = null; $("p.mb-0.uppercase").each((_, e) => { const v = cleanText($(e).text()); if (!v) return; if (/\d\s*MW\b/i.test(v) && totalPowerMw == null) totalPowerMw = parseMw(v); else if (/acre|hectare|\bha\b/i.test(v) && siteAreaHa == null) siteAreaHa = areaHa(v); }); const designed = text.match(/designed (?:for|to deliver|to support)\s+(\d+(?:\.\d+)?)\s*MW/i); const plannedPowerMw = designed ? parseMw(`${designed[1]} MW`) : null; const introH2 = $("h2").filter((_, e) => new RegExp(`${code}\\s+Campus$`, "i").test($(e).text())).first(); 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()); // Yoast builds the meta description by concatenating the page ("…Schedule a tour ATL01 Campus…") → only use it when it reads as prose const metaDesc = description(doc.text); const desc = metaDesc && !/schedule a tour|connect with us/i.test(metaDesc) ? metaDesc : null; const status = statusFromText(`${h1} ${intro ?? ""}`); const campusName = `STACK ${code}`; const common = { city, regionName, countryIso2, campusName }; const records: ExtractedRecord[] = []; records.push(record("facility", `stack:${code.toLowerCase()}`, url, { name: `STACK ${code}${city ? ` – ${city}` : ""}`, code, aliases: [`${code} Campus`], ...common, totalPowerMw, plannedPowerMw: plannedPowerMw && plannedPowerMw !== totalPowerMw ? plannedPowerMw : null, siteAreaHa, status, description: intro ?? desc, externalIds: { stack_slug: slug }, }, { 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", 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", }, 0.85)); $("h3.title-campus").each((_, e) => { const bcode = cleanText($(e).text())?.toUpperCase(); if (!bcode || !/^[A-Z]{2,4}\d{1,3}[A-Z]?$/.test(bcode)) return; const vals = $(e).parent().find("div.flex.items-end").map((_, v) => cleanText($(v).text()) ?? "").get(); let bSqm: number | null = null, bMw: number | null = null; 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); } records.push(record("facility", `stack:${bcode.toLowerCase()}`, url, { name: `STACK ${bcode}${city ? ` – ${city}` : ""}`, code: bcode, ...common, buildingSqm: bSqm, totalPowerMw: bMw, status, description: `Building ${bcode} on the STACK ${code} campus${city ? ` in ${city}` : ""}.`, externalIds: { stack_campus: code }, }, { 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)); }); return records; }, };