// author: simon-pierre boucher import type { PageMetadata, StructuredData } from "./types.js"; function flattenJsonLd(node: unknown, out: unknown[]): void { if (Array.isArray(node)) { for (const item of node) flattenJsonLd(item, out); return; } if (node !== null && typeof node === "object") { const obj = node as Record; if ("@graph" in obj) { flattenJsonLd(obj["@graph"], out); const rest = { ...obj }; delete rest["@graph"]; delete rest["@context"]; if (Object.keys(rest).length > 0) out.push(rest); return; } out.push(obj); } } function harvestJsonLd(document: Document): unknown[] { const out: unknown[] = []; for (const el of Array.from(document.querySelectorAll('script[type="application/ld+json"]'))) { const raw = el.textContent ?? ""; if (raw.trim() === "") continue; try { flattenJsonLd(JSON.parse(raw), out); } catch { continue; } } return out; } function harvestMetaPrefixed(document: Document, attr: "property" | "name", prefix: string): Record { const map: Record = {}; for (const el of Array.from(document.querySelectorAll(`meta[${attr}^="${prefix}"]`))) { const key = (el.getAttribute(attr) ?? "").slice(prefix.length); const content = el.getAttribute("content"); if (key !== "" && content !== null && !(key in map)) map[key] = content; } return map; } function metaContent(document: Document, name: string): string | undefined { const el = document.querySelector(`meta[name="${name}" i]`); const content = el?.getAttribute("content"); return content !== null && content !== undefined && content !== "" ? content : undefined; } function firstDefined(...values: Array): string | undefined { for (const v of values) if (v !== undefined && v !== "") return v; return undefined; } function jsonLdString(nodes: unknown[], types: string[], keys: string[]): string | undefined { for (const node of nodes) { if (node === null || typeof node !== "object") continue; const obj = node as Record; const t = obj["@type"]; const typeStr = Array.isArray(t) ? t.map(String) : typeof t === "string" ? [t] : []; if (types.length > 0 && !typeStr.some((x) => types.includes(x))) continue; for (const key of keys) { const val = obj[key]; if (typeof val === "string" && val !== "") return val; if (Array.isArray(val) && typeof val[0] === "string" && val[0] !== "") return val[0]; if (val !== null && typeof val === "object") { const obj2 = val as Record; for (const nested of ["name", "url"]) { const nv = obj2[nested]; if (typeof nv === "string" && nv !== "") return nv; } } } } return undefined; } export function harvestStructured(document: Document): { metadata: PageMetadata; structured: StructuredData } { const jsonld = harvestJsonLd(document); const openGraph = harvestMetaPrefixed(document, "property", "og:"); const twitter = harvestMetaPrefixed(document, "name", "twitter:"); const structured: StructuredData = { jsonld, openGraph, twitter }; const htmlLang = document.querySelector("html")?.getAttribute("lang") ?? undefined; const canonical = document.querySelector('link[rel="canonical"]')?.getAttribute("href") ?? undefined; const docTitle = firstDefined(document.querySelector("title")?.textContent?.trim()); const h1 = firstDefined(document.querySelector("h1")?.textContent?.trim()); let firstPara: string | undefined; for (const p of Array.from(document.querySelectorAll("p"))) { const text = (p.textContent ?? "").trim(); if (text.length > 100) { firstPara = text; break; } } const metadata: PageMetadata = {}; const set = (key: K, value: PageMetadata[K] | undefined): void => { if (value !== undefined) metadata[key] = value; }; set("title", firstDefined(jsonLdString(jsonld, [], ["headline", "name"]), openGraph["title"], docTitle, h1)); set( "description", firstDefined(jsonLdString(jsonld, [], ["description"]), openGraph["description"], metaContent(document, "description"), firstPara), ); set("author", firstDefined(jsonLdString(jsonld, [], ["author"]), metaContent(document, "author"))); set("image", firstDefined(jsonLdString(jsonld, [], ["image"]), openGraph["image"], twitter["image"])); set("siteName", firstDefined(openGraph["site_name"])); set("type", firstDefined(openGraph["type"])); set("publishedTime", firstDefined(jsonLdString(jsonld, [], ["datePublished"]), openGraph["article:published_time"])); set("modifiedTime", firstDefined(jsonLdString(jsonld, [], ["dateModified"]), openGraph["article:modified_time"])); set("canonical", canonical); set("lang", htmlLang); const keywords = metaContent(document, "keywords"); if (keywords !== undefined) { const list = keywords.split(",").map((k) => k.trim()).filter((k) => k !== ""); if (list.length > 0) metadata.keywords = list; } return { metadata, structured }; }