/* URL transformations and semantic ID generation for structured data. Framework-agnostic — used across all Warpgogol apps. Do not perform raw URL parsing beyond basic structure validation. Do not manage application configuration or external service interactions. RFC-0910: add canonicalRootUrl — unprefixed root URL for entity identity (Organization.url, WebSite.url). RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ export function getBaseUrl(url: URL | string): string { const resolvedUrl = new URL(typeof url === "string" ? url : url.toString()); return resolvedUrl.origin; } export function toCanonicalUrl(url: URL | string): string { const resolvedUrl = new URL(typeof url === "string" ? url : url.toString()); resolvedUrl.search = ""; resolvedUrl.hash = ""; return resolvedUrl.toString(); } export function toAbsoluteUrl(baseUrl: string, path: string): string { return new URL(path, `${baseUrl}/`).toString(); } /** * RFC-0910: produce the canonical root URL for entity identity. * * The entity root URL is language-independent — it is always `https://site/` * regardless of the default language. This contrasts with page URLs, which * are language-prefixed for non-default languages and unprefixed for the * default language per RFC-0160. */ export function canonicalRootUrl(baseUrl: string): string { return new URL("/", `${baseUrl}/`).toString(); } export function toPathname(url: string): string { return new URL(url).pathname; } export function markdownTwinRelPath( pathname: string, opts: { supportedLangs: readonly string[] }, ): string { const p = pathname.replace(/^\/+|\/+$/g, ""); if (p === "") return "index.md"; const isLangRoot = !p.includes("/") && opts.supportedLangs.includes(p); if (isLangRoot) return `${p}/index.md`; return `${p}.md`; } export function markdownTwinUrlPath( pathname: string, opts: { supportedLangs: readonly string[] }, ): string { return `/${markdownTwinRelPath(pathname, opts)}`; } function toIdSegment(value: string): string { return ( value .toLowerCase() .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") || "entity" ); } export function createSemanticIds(baseUrl: string) { const normalizedBaseUrl = baseUrl.replace(/\/$/, ""); return { organization: `${normalizedBaseUrl}/#/schema/organization`, website: `${normalizedBaseUrl}/#/schema/website`, webpage: (pageUrl: string) => `${toCanonicalUrl(pageUrl)}#/schema/webpage`, breadcrumb: (pageUrl: string) => `${toCanonicalUrl(pageUrl)}#/schema/breadcrumb`, person: (name: string) => `${normalizedBaseUrl}/#/schema/person/${toIdSegment(name)}`, initiative: (name: string) => `${normalizedBaseUrl}/#/schema/initiative/${toIdSegment(name)}`, service: (name: string) => `${normalizedBaseUrl}/#/schema/service/${toIdSegment(name)}`, faq: (pageUrl: string) => `${toCanonicalUrl(pageUrl)}#/schema/faq`, }; }