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, countryFromText, description, lastSlug, record, renewableFrom, statusFromText, usStateCode } from "./shared.js";45/**6 * Equinix IBX pages (www.equinix.com/data-centers/<region>-colocation/<country>-colocation/<metro>-data-centers/<code>).7 * The page embeds plain JS constants for the map (title, metro, address1/2, city, postalCode, country, latitude, longitude)8 * plus a spec table (<td>label</td><td>value</td>) and a certifications list. No MW figure is published per IBX.9 */10function jsVar(html: string, name: string): string | null {11 const m = html.match(new RegExp(`const ${name} = "((?:[^"\\\\]|\\\\.)*)"`));12 if (!m) return null;13 try { return cleanText(JSON.parse(`"${m[1]}"`) as string); } catch { return cleanText(m[1]!); }14}1516export const equinixFacility: Parser = {17 name: "equinix_facility_v1",18 version: "1.0.0",19 pageTypes: ["facility_page"],20 parse(doc: RawDocument): ExtractedRecord[] {21 const html = doc.text;22 if (!html || !/const latitude = /.test(html)) return [];23 const $ = load(html);24 const code = (jsVar(html, "title") ?? lastSlug(doc.finalUrl)).toUpperCase();25 if (!/^[A-Z]{2,3}\d{1,2}[A-Z]?$/.test(code)) return [];26 const h1 = cleanText($("h1#hero-banner-heading").first().text()) ?? cleanText($("h1").first().text());27 const metro = jsVar(html, "metro");28 const city = jsVar(html, "city");29 const address1 = jsVar(html, "address1");30 const address2 = jsVar(html, "address2");31 const postalCode = jsVar(html, "postalCode");32 const country = jsVar(html, "country");33 const ibxType = jsVar(html, "ibxType");34 const lat = Number(jsVar(html, "latitude"));35 const lng = Number(jsVar(html, "longitude"));36 const desc = description(html);3738 // US state from the meta description ("… located at 21715 Filigree Court, Ashburn, VA. See …")39 let regionName: string | null = null;40 const rm = desc?.match(/,\s*([A-Z]{2})\.\s/);41 if (rm && usStateCode(rm[1])) regionName = rm[1]!;4243 // headline metrics: <span data-role="metric">N+1</span><span data-role="qualifier">Power redundancy</span>44 const metrics: Record<string, string> = {};45 $('span[data-role="metric"]').each((_, el) => {46 const q = cleanText($(el).siblings('span[data-role="qualifier"]').first().text());47 const v = cleanText($(el).text());48 if (q && v) metrics[q.toLowerCase()] = v;49 });50 // spec table rows51 const specs: Record<string, string> = {};52 $("table tr").each((_, tr) => {53 const tds = $(tr).find("td");54 if (tds.length >= 2) { const k = cleanText(tds.eq(0).text()); const v = cleanText(tds.eq(1).text()); if (k && v) specs[k.toLowerCase()] = v; }55 });56 const coloSpaceRaw = specs["colocation space"] ?? null;57 const coloM = coloSpaceRaw?.match(/([\d,.]+)\s*m(?:²|2)?\b/);58 const coloSqm = coloM ? Number(coloM[1]!.replace(/,/g, "")) : null;5960 // certifications block61 const certHeader = $("h3").filter((_, e) => /Certified to global standards/i.test($(e).text())).first();62 let certScope = certHeader.closest("article");63 if (!certScope.length) certScope = certHeader.parents().filter((_, e) => $(e).find("ul li").length > 0).first();64 const certItems = certScope.find("li span").filter((_, e) => !$(e).attr("data-role") && !$(e).find("svg").length).map((_, e) => cleanText($(e).text()) ?? "").get().filter(Boolean);65 const certifications = certificationsFrom(certItems);6667 // Sustainability row: keep only the bullet that states the renewable / REC claim68 const sustRow = $("table tr").filter((_, tr) => /^sustainability$/i.test(cleanText($(tr).find("td").first().text()) ?? "")).first();69 const sustItems = sustRow.find("li").map((_, li) => cleanText($(li).text()) ?? "").get().filter(Boolean);70 const sustainability = sustItems.find((t) => /\b(RECs?|renewable|wind|solar|hydro|carbon[- ]free|green[- ]e|PPA)\b/i.test(t)) ?? null;71 const heroP = cleanText($("h1").first().nextAll("p").first().text());72 const status = statusFromText(`${h1 ?? ""} ${heroP ?? ""}`);73 const name = h1 && h1.toUpperCase().includes(code) ? h1 : `${metro ?? city ?? "Equinix"} ${code}`;74 const extraDesc = [coloSpaceRaw ? `Colocation space: ${coloSpaceRaw}.` : null, metrics["power redundancy"] ? `Power redundancy: ${metrics["power redundancy"]}.` : null, specs["ups redundancy"] ? `UPS redundancy: ${specs["ups redundancy"]}.` : null].filter(Boolean).join(" ");7576 const data: Record<string, unknown> = {77 name, code, aliases: [`Equinix ${code}`, `${code} IBX`],78 address: [address1, address2].filter(Boolean).join(", ") || null, city, regionName, postalCode,79 countryIso2: country ? countryFromText(country) : null,80 lat: Number.isFinite(lat) && lat !== 0 ? lat : null, lng: Number.isFinite(lng) && lng !== 0 ? lng : null, geoPrecision: "exact",81 certifications, renewableClaim: renewableFrom(sustainability),82 status, facilityType: ibxType && /xscale/i.test(ibxType) ? "hyperscale" : "colocation",83 description: [desc, extraDesc].filter(Boolean).join(" ") || null,84 campusName: metro && /campus/i.test(heroP ?? "") ? `${metro} campus` : null,85 externalIds: { equinix_ibx: code },86 _coloSqm: coloSqm,87 };88 delete data._coloSqm; // colocation (white) space is not building size — kept in description only89 const methods: Record<string, string> = {90 name: "selector:h1#hero-banner-heading", code: "js-var:title", address: "js-var:address1", city: "js-var:city", regionName: "meta:description+regex:state", postalCode: "js-var:postalCode",91 countryIso2: "js-var:country>country", lat: "js-var:latitude", lng: "js-var:longitude", certifications: "selector:certifications-list", renewableClaim: "table:sustainability+regex:renewable_v1",92 status: "regex:status_v1", facilityType: "js-var:ibxType", description: "meta:description+table:specs", campusName: "regex:campus", externalIds: "js-var:title",93 };94 return [record("facility", `equinix:${code}`, doc.finalUrl, data, methods, 0.95)];95 },96};97