@aranova/tracking-next 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +48 -1
  2. package/dist/blog-server.js.map +1 -1
  3. package/dist/blog-server.mjs.map +1 -1
  4. package/dist/blog.js.map +1 -1
  5. package/dist/blog.mjs.map +1 -1
  6. package/dist/calendar-server.d.mts +1 -1
  7. package/dist/calendar-server.d.ts +1 -1
  8. package/dist/calendar-server.js.map +1 -1
  9. package/dist/calendar-server.mjs.map +1 -1
  10. package/dist/calendar.d.mts +14 -3
  11. package/dist/calendar.d.ts +14 -3
  12. package/dist/calendar.js +212 -18
  13. package/dist/calendar.js.map +1 -1
  14. package/dist/calendar.mjs +211 -18
  15. package/dist/calendar.mjs.map +1 -1
  16. package/dist/client.d.mts +9 -3
  17. package/dist/client.d.ts +9 -3
  18. package/dist/client.js +550 -194
  19. package/dist/client.js.map +1 -1
  20. package/dist/client.mjs +550 -194
  21. package/dist/client.mjs.map +1 -1
  22. package/dist/index.d.mts +10 -9
  23. package/dist/index.d.ts +10 -9
  24. package/dist/index.js +491 -284
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +490 -284
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/{ingest-typed-BdKmlouW.d.ts → ingest-typed-MBuMe9Pn.d.ts} +94 -2
  29. package/dist/{ingest-typed-Bo7WPmpU.d.mts → ingest-typed-UA13xTfI.d.mts} +94 -2
  30. package/dist/{phone-utils-CubQif9Z.d.mts → phone-utils-C0ds6cqZ.d.mts} +6 -0
  31. package/dist/{phone-utils-CubQif9Z.d.ts → phone-utils-C0ds6cqZ.d.ts} +6 -0
  32. package/dist/phone.d.mts +1 -1
  33. package/dist/phone.d.ts +1 -1
  34. package/dist/{sales-DG150g3o.d.ts → sales-DQC815f3.d.ts} +1 -1
  35. package/dist/{sales-D7KstKA-.d.mts → sales-w1reW_Y3.d.mts} +1 -1
  36. package/dist/sales.d.mts +2 -2
  37. package/dist/sales.d.ts +2 -2
  38. package/dist/sales.js +17 -1
  39. package/dist/sales.js.map +1 -1
  40. package/dist/sales.mjs +17 -1
  41. package/dist/sales.mjs.map +1 -1
  42. package/dist/server.d.mts +1 -1
  43. package/dist/server.d.ts +1 -1
  44. package/dist/{tracking-config-runtime-D7prdTE1.d.mts → tracking-config-runtime-C6H7ofmo.d.mts} +6 -0
  45. package/dist/{tracking-config-runtime-D7prdTE1.d.ts → tracking-config-runtime-C6H7ofmo.d.ts} +6 -0
  46. package/dist/{transport-I_cMSFQ6.d.mts → transport-BlXQZs9Z.d.mts} +120 -50
  47. package/dist/{transport-CDmGOiCq.d.ts → transport-CAZYrZbB.d.ts} +120 -50
  48. package/package.json +1 -1
package/dist/blog.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/blog.ts","../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/tracking-config-runtime.ts","../../tracking-core/src/resources/blog/schema.ts","../../tracking-core/src/resources/blog/theme.ts","../../tracking-core/src/resources/blog/client.ts","../../tracking-core/src/resources/blog/presets.ts","../../tracking-core/src/resources/blog/view/class-names.ts","../../tracking-core/src/resources/blog/seo.ts","../../tracking-core/src/resources/blog/toc.ts","../../tracking-core/src/resources/blog/validate-mdx.ts"],"sourcesContent":["// React-free blog SDK entry — `@aranova/tracking-next/blog`. Safe to import\n// from metadata/sitemap/route-handler code (and client bundles) without pulling\n// React or the MDX renderer: schemas + parsers, theme resolution, the CDN blog\n// client, and the SEO builders. The RSC renderer lives on `./blog-server`.\n// The surface is authored once in tracking-core (see blog-public.ts); this file\n// exists only as the per-package tsup build entry.\nexport * from \"../../tracking-core/src/blog-public\";\n","// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","import { applyDefaultConsentState, isValidGtagId, loadGtagScript } from \"../gtag\";\nimport {\n parseConversionConfig,\n type ConversionConfig,\n type ConversionGoal,\n} from \"./conversion-config\";\nimport { fireConversionWithConsent } from \"./conversion-firing\";\nimport { getAutomaticTransactionId } from \"./automatic-transaction\";\nimport { automaticThresholdMet } from \"./automatic-trigger\";\nimport { fromMinor } from \"./sales/money\";\nimport type { SupportedCurrency } from \"./sales/schema\";\n\n/**\n * Where a business's published tracking config lives.\n *\n * `businessId` + `environment` are the stable identity; the ORIGIN it is fetched\n * from is deployment-specific (production CDN vs. a local object store), which is\n * why `cdnBaseUrl` exists. Codegen bakes only the identity, so one committed\n * generated file works in every environment and only an env var changes.\n *\n * `cdnUrl` remains supported and WINS when present — every client repo generated\n * before `cdnBaseUrl` passes one, and those must keep working untouched.\n */\nexport interface TrackingConfigReference {\n /**\n * Fully-resolved object URL. Optional: omit it and pass `cdnBaseUrl` (or rely\n * on the production default) to have it composed from the identity below.\n */\n cdnUrl?: string;\n /**\n * Origin (optionally with a path prefix) the config object is served from, e.g.\n * `https://demos.aranova.io` in production or\n * `http://localhost:9100/aranova-demos` against a local MinIO. Ignored when\n * `cdnUrl` is set; blank or omitted falls back to {@link DEFAULT_CDN_BASE_URL}.\n * `tracking-cli gen` wires this to an env var so retargeting is config, not code.\n */\n cdnBaseUrl?: string;\n businessId: string;\n /**\n * The site whose config object to load (ENG-284). Selects the per-site\n * `tracking-config/v1/sites/` key; omitted (older generated files) resolves\n * the legacy per-business key, which is the primary site's config.\n */\n siteId?: string;\n environment: \"production\" | \"test\";\n}\n\n/** Production object-storage origin — the default when nothing overrides it. */\nexport const DEFAULT_CDN_BASE_URL = \"https://demos.aranova.io\";\n\n/**\n * Object key for a config, mirroring the backend's\n * `core/storage.py::tracking_config_key` (per-site) and its legacy per-business\n * key. The backend stays the authority: it\n * publishes at this key and returns the absolute URL, which is what the CLI bakes\n * as the legacy `cdnUrl`. This is the composition path for references that carry\n * identity instead of a URL — the `v1` prefix is the schema MAJOR and must move in\n * lockstep with the backend if it is ever cut to `v2`.\n */\nfunction trackingConfigKey(ref: TrackingConfigReference): string {\n if (ref.siteId) return `tracking-config/v1/sites/${ref.siteId}-${ref.environment}.json`;\n return `tracking-config/v1/${ref.businessId}-${ref.environment}.json`;\n}\n\n/**\n * The URL a reference resolves to. An explicit `cdnUrl` wins (back-compat);\n * otherwise compose from `cdnBaseUrl` (or the production default).\n *\n * A blank/whitespace `cdnBaseUrl` is treated as UNSET rather than composed: the\n * generated module reads it from an env var, and a var that is present-but-empty\n * (a stray `NEXT_PUBLIC_ARANOVA_CDN_BASE_URL=` line) would otherwise produce\n * `/tracking-config/...` — a same-origin request to the client's own site.\n */\nexport function resolveTrackingConfigUrl(ref: TrackingConfigReference): string {\n if (ref.cdnUrl) return ref.cdnUrl;\n const configured = ref.cdnBaseUrl?.trim();\n const base = (configured || DEFAULT_CDN_BASE_URL).replace(/\\/+$/, \"\");\n return `${base}/${trackingConfigKey(ref)}`;\n}\n\ntype RuntimeState = \"unconfirmed\" | \"active\" | \"tombstone\";\n\ninterface CachedEntry {\n etag: string | null;\n config: ConversionConfig;\n}\n\ninterface QueuedConversion {\n key: string;\n value?: number | null;\n currency?: string | null;\n transactionId?: string | null;\n}\n\nexport type TrackingConfigConversionOptions = Omit<QueuedConversion, \"key\">;\n\ninterface QueuedPageView {\n href: string;\n title: string | null;\n referrer: string | null;\n}\n\ninterface QueuedAutomaticEvent {\n eventType: string;\n metadata: Record<string, unknown>;\n transactionPath: string;\n transactionScope: string;\n}\n\nconst CACHE_PREFIX = \"_aranova_cfg_runtime_\";\nconst AUTHORITY_TTL_MS = 60_000;\n// Queues are bounded rather than cleared while authority is unconfirmed: the\n// config can appear at any time (a first publish, or R2 recovering), and the\n// most recent items are still worth firing when it does. Unbounded, a site whose\n// config 404s accumulates an entry per page view for the life of the tab.\nconst MAX_QUEUE_LENGTH = 50;\n// While unconfirmed, revalidation is spaced by a growing delay instead of riding\n// every queued item. Without it a missing config produces one no-cache request\n// per page view, forever. `focus` / `visibilitychange` still revalidate, so\n// recovery stays prompt once the object exists.\nconst RETRY_BASE_MS = 5_000;\nconst RETRY_MAX_MS = 5 * 60_000;\n\nconst runtimes = new Map<string, TrackingConfigRuntime>();\nconst configuredIds = new Set<string>();\nlet scriptLoad: Promise<void> | null = null;\nlet jsInitialized = false;\n\nfunction cacheKey(url: string): string {\n return `${CACHE_PREFIX}${url}`;\n}\n\n/** Append, dropping the oldest entry once the cap is reached. */\nfunction pushBounded<T>(queue: T[], item: T): void {\n queue.push(item);\n if (queue.length > MAX_QUEUE_LENGTH) queue.splice(0, queue.length - MAX_QUEUE_LENGTH);\n}\n\nfunction readCache(url: string): CachedEntry | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.sessionStorage.getItem(cacheKey(url));\n if (!raw) return null;\n const parsed = JSON.parse(raw) as { etag?: unknown; config?: unknown };\n const config = parseConversionConfig(parsed.config);\n if (!config) return null;\n return {\n etag: typeof parsed.etag === \"string\" ? parsed.etag : null,\n config,\n };\n } catch {\n return null;\n }\n}\n\nfunction writeCache(url: string, entry: CachedEntry): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(cacheKey(url), JSON.stringify(entry));\n } catch {\n // sessionStorage unavailable — runtime still works, just without a conditional candidate\n }\n}\n\nfunction isTombstone(config: ConversionConfig): boolean {\n return config.google_tracking_state === \"disabled\" || Object.keys(config.gtag_ids).length === 0;\n}\n\nfunction validateConfig(config: ConversionConfig, ref: TrackingConfigReference): boolean {\n return (\n config.business_id === ref.businessId &&\n config.environment === ref.environment &&\n (config.google_tracking_state === \"active\" || config.google_tracking_state === \"disabled\")\n );\n}\n\nfunction pageViewSnapshot(): QueuedPageView | null {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return null;\n return {\n href: window.location.href,\n title: document.title || null,\n referrer: document.referrer || null,\n };\n}\n\nfunction ensureScript(gtagId: string): Promise<void> {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return Promise.resolve();\n applyDefaultConsentState();\n loadGtagScript(gtagId);\n if (scriptLoad) return scriptLoad;\n scriptLoad = new Promise((resolve) => {\n const script = document.querySelector<HTMLScriptElement>(\n 'script[data-aranova-tracking=\"aranova-gtag-loader\"]',\n );\n if (!script) {\n resolve();\n return;\n }\n if ((script as HTMLScriptElement & { dataset: DOMStringMap }).dataset.loaded === \"true\") {\n resolve();\n return;\n }\n window.setTimeout(resolve, 0);\n script.addEventListener(\n \"load\",\n () => {\n script.dataset.loaded = \"true\";\n resolve();\n },\n { once: true },\n );\n script.addEventListener(\"error\", () => resolve(), { once: true });\n });\n return scriptLoad;\n}\n\nexport class TrackingConfigRuntime {\n private readonly fetchImpl: typeof fetch;\n /** Resolved config URL — see the constructor for why it is computed once. */\n private readonly url: string;\n private current: ConversionConfig | null = null;\n private etag: string | null = null;\n private stateValue: RuntimeState = \"unconfirmed\";\n private confirmedAt = 0;\n private authorityGeneration = 0;\n private inFlight: Promise<void> | null = null;\n private flushInFlight: Promise<void> | null = null;\n private flushRequested = false;\n private retryTimer: number | null = null;\n private started = false;\n /** Consecutive failed authority attempts — drives the revalidate backoff. */\n private authorityFailures = 0;\n /** Epoch ms before which `ensureAuthority` must not issue another request. */\n private nextAuthorityAttemptAt = 0;\n private readonly conversionQueue: QueuedConversion[] = [];\n private readonly automaticQueue: QueuedAutomaticEvent[] = [];\n private readonly pageQueue: QueuedPageView[] = [];\n private readonly listeners = new Set<() => void>();\n\n constructor(\n readonly ref: TrackingConfigReference,\n fetchImpl: typeof fetch = globalThis.fetch,\n ) {\n // Native browser fetch requires Window/WorkerGlobalScope as its receiver.\n // Calling an unbound native fetch stored on `this` throws \"Illegal invocation\".\n this.fetchImpl = fetchImpl.bind(globalThis);\n // Resolved once: the cache key, the fetch target, and the write-back key must\n // all be the same string, or a reference that composes its URL would read a\n // cache entry it never writes.\n this.url = resolveTrackingConfigUrl(ref);\n const cached = readCache(this.url);\n this.current = cached?.config ?? null;\n this.etag = cached?.etag ?? null;\n this.start();\n }\n\n /** The URL this runtime actually fetches (composed or explicit). */\n configUrl(): string {\n return this.url;\n }\n\n /**\n * Explicitly start authority resolution and Google-tag bootstrap.\n *\n * Idempotent so framework effects can call it after hydration without\n * depending on constructor timing.\n */\n start(): void {\n if (this.started) {\n void this.flush();\n return;\n }\n this.started = true;\n void this.revalidate();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"visible\") void this.revalidate();\n });\n window.addEventListener(\"focus\", () => void this.revalidate());\n }\n }\n\n state(): RuntimeState {\n return this.stateValue;\n }\n\n config(): ConversionConfig | null {\n return this.stateValue === \"active\" || this.stateValue === \"tombstone\" ? this.current : null;\n }\n\n __unsafeExpireAuthorityForTests(): void {\n this.confirmedAt = Number.NEGATIVE_INFINITY;\n this.nextAuthorityAttemptAt = 0;\n }\n\n /** Queue depths — asserted by tests to pin the bound. */\n __queueDepthsForTests(): { conversions: number; automatic: number; pages: number } {\n return {\n conversions: this.conversionQueue.length,\n automatic: this.automaticQueue.length,\n pages: this.pageQueue.length,\n };\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n async ensureAuthority(): Promise<boolean> {\n if (this.stateValue !== \"unconfirmed\" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {\n return true;\n }\n // Backoff applies only to this implicit path (driven by queued work). An\n // explicit `revalidate()` — the visibility/focus listeners, or a caller —\n // always goes to the network so recovery is never delayed by the timer.\n if (Date.now() < this.nextAuthorityAttemptAt) return false;\n await this.revalidateAuthority();\n return this.stateValue !== \"unconfirmed\" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;\n }\n\n async revalidate(): Promise<void> {\n this.nextAuthorityAttemptAt = 0;\n await this.revalidateAuthority();\n await this.flush();\n }\n\n private async revalidateAuthority(): Promise<void> {\n if (typeof window === \"undefined\" || !this.fetchImpl) return;\n if (this.inFlight) return this.inFlight;\n this.inFlight = this.revalidateNow().finally(() => {\n this.inFlight = null;\n });\n return this.inFlight;\n }\n\n queuePageView(snapshot: QueuedPageView | null = pageViewSnapshot()): void {\n if (!snapshot) return;\n pushBounded(this.pageQueue, snapshot);\n void this.flush();\n }\n\n fireConversion(key: string, options?: TrackingConfigConversionOptions): void {\n pushBounded(this.conversionQueue, { key, ...options });\n void this.flush();\n }\n\n queueAutomaticEvent(\n eventType: string,\n metadata: Record<string, unknown>,\n transactionPath: string,\n transactionScope: string,\n ): void {\n pushBounded(this.automaticQueue, {\n eventType,\n metadata,\n transactionPath,\n transactionScope,\n });\n void this.flush();\n }\n\n listGoals(): ConversionGoal[] {\n return this.current?.goals ?? [];\n }\n\n private async revalidateNow(): Promise<void> {\n try {\n const headers: Record<string, string> = {};\n if (this.etag) headers[\"If-None-Match\"] = this.etag;\n const response = await this.fetchImpl(this.url, {\n method: \"GET\",\n headers,\n cache: \"no-cache\",\n });\n if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {\n this.confirm(this.current, this.etag);\n return;\n }\n if (!response.ok) {\n this.expireAuthority();\n return;\n }\n const next = parseConversionConfig(await response.json());\n if (!next || !validateConfig(next, this.ref)) {\n this.expireAuthority();\n return;\n }\n if (this.current && next.config_version < this.current.config_version) {\n this.expireAuthority();\n return;\n }\n this.confirm(next, response.headers.get(\"ETag\"));\n } catch {\n this.expireAuthority();\n }\n }\n\n private expireAuthority(): void {\n this.authorityGeneration += 1;\n if (this.stateValue !== \"unconfirmed\") this.stateValue = \"unconfirmed\";\n // Exponential, capped. Queued work keeps accumulating (bounded), so the\n // config appearing later still fires the most recent items.\n this.authorityFailures += 1;\n const delay = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** (this.authorityFailures - 1));\n this.nextAuthorityAttemptAt = Date.now() + delay;\n }\n\n private confirm(config: ConversionConfig, etag: string | null): void {\n this.authorityGeneration += 1;\n this.current = config;\n this.etag = etag;\n this.confirmedAt = Date.now();\n this.authorityFailures = 0;\n this.nextAuthorityAttemptAt = 0;\n this.stateValue = isTombstone(config) ? \"tombstone\" : \"active\";\n writeCache(this.url, { etag, config });\n if (this.stateValue === \"tombstone\") {\n if (this.retryTimer !== null) {\n window.clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n this.conversionQueue.length = 0;\n this.automaticQueue.length = 0;\n this.pageQueue.length = 0;\n }\n for (const listener of this.listeners) listener();\n }\n\n private async flush(): Promise<void> {\n if (this.flushInFlight) {\n this.flushRequested = true;\n return this.flushInFlight;\n }\n this.flushRequested = false;\n this.flushInFlight = this.flushNow().finally(() => {\n this.flushInFlight = null;\n if (this.flushRequested && this.stateValue === \"active\") void this.flush();\n });\n return this.flushInFlight;\n }\n\n private scheduleRetry(): void {\n if (this.retryTimer !== null || typeof window === \"undefined\") return;\n this.retryTimer = window.setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, 1000);\n }\n\n private async flushNow(): Promise<void> {\n if (!(await this.ensureAuthority())) return;\n if (this.stateValue !== \"active\" || !this.current) return;\n const config = this.current;\n const generation = this.authorityGeneration;\n const ids = Object.values(config.gtag_ids).filter(\n (id): id is string => typeof id === \"string\" && isValidGtagId(id),\n );\n if (ids.length === 0) return;\n await ensureScript(ids[0]!);\n if (\n this.authorityGeneration !== generation ||\n this.stateValue !== \"active\" ||\n this.current !== config\n ) {\n return;\n }\n const gtag = window.gtag;\n if (typeof gtag !== \"function\") return;\n try {\n if (!jsInitialized) {\n gtag(\"js\", new Date());\n jsInitialized = true;\n }\n for (const id of ids) {\n if (configuredIds.has(id)) continue;\n gtag(\"config\", id, { send_page_view: false });\n configuredIds.add(id);\n }\n } catch {\n this.scheduleRetry();\n return;\n }\n while (this.pageQueue.length > 0) {\n const page = this.pageQueue[0]!;\n try {\n gtag(\"event\", \"page_view\", {\n page_location: page.href,\n page_title: page.title ?? undefined,\n page_referrer: page.referrer ?? undefined,\n });\n this.pageQueue.shift();\n } catch {\n this.scheduleRetry();\n return;\n }\n }\n while (this.automaticQueue.length > 0) {\n const event = this.automaticQueue[0]!;\n for (const goal of config.goals) {\n if (goal.kind !== \"event\" || !goal.firing) continue;\n if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;\n pushBounded(this.conversionQueue, {\n key: goal.key,\n transactionId: getAutomaticTransactionId(\n event.transactionScope,\n goal.key,\n event.transactionPath,\n ),\n });\n }\n this.automaticQueue.shift();\n }\n while (this.conversionQueue.length > 0) {\n const item = this.conversionQueue[0]!;\n const goal = config.goals.find((g) => g.key === item.key);\n const firing = goal?.firing;\n if (!firing) {\n this.conversionQueue.shift();\n continue;\n }\n const currency = item.currency ?? firing.currency ?? null;\n const value =\n item.value ??\n (firing.value_cents != null && currency\n ? fromMinor(firing.value_cents, currency as SupportedCurrency)\n : null);\n const outcome = fireConversionWithConsent({\n sendTo: firing.send_to,\n value,\n currency,\n transactionId: item.transactionId ?? null,\n });\n if (outcome === \"retryable\") {\n this.scheduleRetry();\n return;\n }\n this.conversionQueue.shift();\n }\n }\n}\n\nexport function getTrackingConfigRuntime(\n ref: TrackingConfigReference,\n fetchImpl?: typeof fetch,\n): TrackingConfigRuntime {\n // Keyed on the RESOLVED url: `{cdnUrl}` and an equivalent\n // `{cdnBaseUrl,businessId,environment}` name the same object and must share one\n // runtime, or each would keep its own authority state and gtag bootstrap.\n const key = `${resolveTrackingConfigUrl(ref)}|${ref.businessId}|${ref.environment}`;\n const existing = runtimes.get(key);\n if (existing) return existing;\n const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);\n runtimes.set(key, runtime);\n return runtime;\n}\n\nexport function resetTrackingConfigRuntimesForTests(): void {\n runtimes.clear();\n configuredIds.clear();\n scriptLoad = null;\n jsInitialized = false;\n}\n","import * as z from \"zod/mini\";\n\nimport { blogThemeOverrideSchema, nullishField } from \"./theme\";\n\n/**\n * Blog wire schemas — the client-side source of truth for the CDN-published\n * blog objects (ENG-31 Area C).\n *\n * {cdnBaseUrl}/blogs/v1/{namespace}/manifest.json (mutable)\n * {cdnBaseUrl}/blogs/v1/{namespace}/posts/{slug}/{content_hash}.json (immutable)\n *\n * Mirrored by the backend publisher and checked by the schema-drift contract\n * (the `resources` section of `events.schema.json`). Field names are FROZEN.\n *\n * These are TOLERANT readers (same philosophy as `conversion-config.ts`): the\n * backend may add fields freely — unknown fields are stripped, malformed\n * optional fields fall back to a safe default via `z.catch()`, and only a\n * structurally unusable object makes the parse helpers return `null`. They\n * never throw.\n */\n\n// The wire major, pinned to the version this reader understands. Within v1\n// the contract is additive-only (unknown fields are stripped), but a future\n// v2 object is by definition not readable under v1 assumptions — the literal\n// makes the whole-object parse fail, so `parseBlog*` returns `null` (the\n// tolerant \"unusable object\" path) instead of misreading it.\nconst schemaVersionSchema = z.literal(1);\n\nexport const blogHeroImageSchema = z.object({\n url: z.string(),\n alt: z.catch(z.string(), \"\"),\n width: z.int(),\n height: z.int(),\n});\n\nexport const blogManifestEntrySchema = z.object({\n slug: z.string(),\n title: z.string(),\n description: z.catch(z.string(), \"\"),\n content_hash: z.string(),\n content_key: z.string(),\n published_at: z.string(),\n updated_at: z.string(),\n // Estimated read time in whole minutes (computed at publish from the post\n // body). 0 when unknown; index cards render \"N min read\".\n reading_minutes: z.catch(z.int(), 0),\n // A malformed tags array / hero_image degrades gracefully instead of\n // dropping the whole entry.\n tags: z.catch(z.array(z.string()), []),\n hero_image: z.catch(z.nullable(blogHeroImageSchema), null),\n});\n\n// WIRE schema — what the backend publisher actually writes (emitted into\n// `events.schema.json` and drift-checked against the Pydantic `BlogManifest`).\n// The runtime reader below layers per-entry drop-tolerance on top of it: a\n// writer never emits `null` entries, so that tolerance is a READER refinement,\n// deliberately kept out of the wire contract.\nexport const blogManifestWireSchema = z.object({\n schema_version: schemaVersionSchema,\n namespace: z.string(),\n generated_at: z.string(),\n posts: z.catch(z.array(blogManifestEntrySchema), []),\n});\n\nexport const blogManifestSchema = z.extend(blogManifestWireSchema, {\n // A single malformed entry is dropped; the rest of the manifest survives.\n posts: z.catch(\n z.pipe(\n z.array(z.catch(z.nullable(blogManifestEntrySchema), null)),\n z.transform((entries) => entries.filter((e): e is BlogManifestEntry => e !== null)),\n ),\n [],\n ),\n});\n\nexport const blogImageSchema = z.object({\n id: z.string(),\n url: z.string(),\n alt: z.catch(z.string(), \"\"),\n caption: z.catch(z.nullable(z.string()), null),\n role: z.catch(z.enum([\"hero\", \"inline\"]), \"inline\"),\n width: z.int(),\n height: z.int(),\n mime: z.catch(z.string(), \"\"),\n});\n\nexport const blogSeoSchema = z.object({\n meta_title: z.catch(z.string(), \"\"),\n meta_description: z.catch(z.string(), \"\"),\n canonical_path: z.catch(z.string(), \"\"),\n og_image: z.catch(z.nullable(z.string()), null),\n});\n\n// WIRE schema — see `blogManifestWireSchema` for the wire/reader split.\nexport const blogPostContentWireSchema = z.object({\n schema_version: schemaVersionSchema,\n slug: z.string(),\n title: z.string(),\n description: z.catch(z.string(), \"\"),\n mdx: z.string(),\n images: z.catch(z.array(blogImageSchema), []),\n // Already schema-validated at publish time, but re-sanitized on read (and\n // again before rendering) — the override schema itself drops invalid values.\n // Nullish: the backend serializes an unset override as an explicit `null`\n // (Pydantic `model_dump()`); it folds into `undefined` for TS consumers.\n theme_overrides: z.catch(nullishField(blogThemeOverrideSchema), undefined),\n seo: z.catch(blogSeoSchema, {\n meta_title: \"\",\n meta_description: \"\",\n canonical_path: \"\",\n og_image: null,\n }),\n jsonld: z.catch(z.record(z.string(), z.unknown()), {}),\n published_at: z.string(),\n // The applied_at of the version this content was published from; `null`\n // when the backend has no separate modified date. Nullish on the wire\n // (Pydantic `model_dump()` emits explicit `null`s), folded to `null` for a\n // single TS sentinel; a malformed value degrades to `null` instead of\n // dropping the post.\n updated_at: z.catch(\n z.pipe(\n z.nullish(z.string()),\n z.transform((v) => v ?? null),\n ),\n null,\n ),\n version_number: z.catch(z.int(), 0),\n content_hash: z.string(),\n});\n\nexport const blogPostContentSchema = z.extend(blogPostContentWireSchema, {\n // A single malformed image record is dropped; the post survives.\n images: z.catch(\n z.pipe(\n z.array(z.catch(z.nullable(blogImageSchema), null)),\n z.transform((images) => images.filter((i): i is BlogImage => i !== null)),\n ),\n [],\n ),\n});\n\nexport type BlogHeroImage = z.infer<typeof blogHeroImageSchema>;\nexport type BlogManifestEntry = z.infer<typeof blogManifestEntrySchema>;\nexport type BlogManifest = z.infer<typeof blogManifestSchema>;\nexport type BlogImage = z.infer<typeof blogImageSchema>;\nexport type BlogSeo = z.infer<typeof blogSeoSchema>;\nexport type BlogPostContent = z.infer<typeof blogPostContentSchema>;\n\n/** Tolerant parse of a raw manifest object — `null` only when unusable. Never throws. */\nexport function parseBlogManifest(raw: unknown): BlogManifest | null {\n const parsed = z.safeParse(blogManifestSchema, raw);\n return parsed.success ? parsed.data : null;\n}\n\n/** Tolerant parse of a raw post-content object — `null` only when unusable. Never throws. */\nexport function parseBlogPostContent(raw: unknown): BlogPostContent | null {\n const parsed = z.safeParse(blogPostContentSchema, raw);\n return parsed.success ? parsed.data : null;\n}\n","import type { $ZodType } from \"zod/v4/core\";\nimport * as z from \"zod/mini\";\n\n/**\n * Blog theme — the typed styling contract for CDN-published blog posts.\n *\n * Three layers resolve into one fully-populated {@link BlogTheme}\n * (later layers win):\n *\n * 1. {@link BASE_BLOG_THEME} — neutral editorial defaults (always complete)\n * 2. the client-registered default theme (client code, per site)\n * 3. per-post `theme_overrides` from the content JSON (backend-authored)\n *\n * Because layers 2 and 3 cross a trust boundary (they end up in inline\n * `style` attributes and CSS custom properties), **sanitization lives in the\n * zod schemas themselves**: every value is validated against a strict grammar\n * (hex/functional colors, unit lengths, clamped weights, token-list font\n * families) and any invalid individual value is DROPPED via `.catch(undefined)`\n * — never passed through to CSS.\n */\n\n// ---------------------------------------------------------------------------\n// Responsive values (Chakra-style breakpoint maps)\n// ---------------------------------------------------------------------------\n\n/**\n * Fixed, SDK-owned breakpoints (mobile-first `min-width` px). Ordered smallest\n * to largest — CSS emission and any future style build must iterate in this\n * order so later (wider) breakpoints cascade over earlier ones.\n */\nexport const BLOG_BREAKPOINTS = [\"sm\", \"md\", \"lg\", \"xl\", \"2xl\", \"3xl\"] as const;\n\nexport type BlogBreakpoint = (typeof BLOG_BREAKPOINTS)[number];\n\nexport const BLOG_BREAKPOINT_MIN_WIDTHS_PX = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n \"2xl\": 1536,\n \"3xl\": 1920,\n} as const satisfies Record<BlogBreakpoint, number>;\n\n/** A breakpoint map: `base` is required, wider breakpoints are optional. */\nexport type BlogBreakpointMap<T = string> = { base: T } & {\n [K in BlogBreakpoint]?: T | undefined;\n};\n\n/**\n * A dimensional theme token: a plain scalar, or a breakpoint map that\n * resolves mobile-first (`base` below `sm`, each key from its min-width up).\n */\nexport type ResponsiveValue<T = string> = T | BlogBreakpointMap<T>;\n\n/**\n * Normalize a {@link ResponsiveValue} to its map form. The ONE place the\n * scalar-vs-map distinction is handled — downstream consumers (CSS var\n * emission, the style build) work off the map and never `typeof`-sniff.\n */\nexport function toBlogBreakpointMap<T extends string | number>(\n value: ResponsiveValue<T>,\n): BlogBreakpointMap<T> {\n return typeof value === \"object\" ? value : { base: value };\n}\n\n// ---------------------------------------------------------------------------\n// Value grammars (the sanitizer)\n// ---------------------------------------------------------------------------\n\nconst HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;\nconst FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla|oklch)\\([0-9a-zA-Z ,.%/-]*\\)$/;\nconst LENGTH = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)$/;\n// line-height additionally allows a unitless multiplier (\"1.7\").\nconst LINE_HEIGHT = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)?$/;\n// One font-family token: optionally quoted word characters/spaces/hyphens.\n// Rejects `url(`, `;`, `{`, `}`, `<`, `>` and every other CSS escape vector.\nconst FONT_FAMILY_TOKEN = /^['\"]?[a-zA-Z0-9 -]+['\"]?$/;\n// A single `var(--name)` reference — how next/font sites expose their real\n// (build-mangled) family names, e.g. `var(--font-sans)`. Exactly one balanced\n// paren pair and a custom-property name; no other characters can sneak in.\nconst FONT_FAMILY_VAR_TOKEN = /^var\\(--[a-zA-Z0-9-]+\\)$/;\n\nconst colorValueSchema = z.string().check(\n z.refine((v) => HEX_COLOR.test(v) || FUNCTIONAL_COLOR.test(v), {\n error: \"must be a #hex or rgb()/rgba()/hsl()/hsla()/oklch() color\",\n }),\n);\n\nconst lengthValueSchema = z\n .string()\n .check(z.regex(LENGTH, { error: \"must be a CSS length (px|rem|em|%|ch|vw)\" }));\n\nconst lineHeightValueSchema = z\n .string()\n .check(z.regex(LINE_HEIGHT, { error: \"must be a CSS length or unitless line-height\" }));\n\nconst fontWeightSchema = z.int().check(z.gte(100), z.lte(1000));\n\n// Text-alignment keyword (figcaption today). Scalar, non-responsive.\nconst alignValueSchema = z.enum([\"left\", \"center\", \"right\"]);\n\nconst fontFamilySchema = z.string().check(\n z.refine(\n (v) => {\n const tokens = v.split(\",\").map((t) => t.trim());\n return (\n tokens.length > 0 &&\n tokens.every((t) => FONT_FAMILY_TOKEN.test(t) || FONT_FAMILY_VAR_TOKEN.test(t))\n );\n },\n { error: \"must be a comma-separated list of font-family or var(--custom-property) tokens\" },\n ),\n);\n\n// The wire truth for every optional field is Pydantic's `model_dump()`, which\n// serializes `Type | None = None` fields as explicit `null`s (never omits\n// them). So every optional field here must be NULL-tolerant, not just\n// absent-tolerant: `.nullish()` + a transform that folds `null` back into\n// `undefined` so downstream TS consumers keep a single \"absent\" sentinel.\n// `z.toJSONSchema(..., { io: \"input\" })` emits the transform's INPUT schema\n// (nullable), which is exactly what the backend drift test compares against\n// the Pydantic mirror.\n//\n// The return type is inferred rather than declared: v4 builds this as a pipe,\n// and an explicit alias both names an implementation detail and loses the\n// key-is-optional signal the object inference depends on.\nexport function nullishField<S extends $ZodType>(schema: S) {\n // The outer `z.optional` is what keeps the OUTPUT key optional: Zod 4 types a\n // transform result as required-but-possibly-undefined, where Zod 3 made the\n // key itself optional. The input schema is unchanged (already nullish), so\n // the JSON Schema snapshot is unaffected.\n return z.optional(\n z.pipe(\n z.nullish(schema),\n z.transform((v) => v ?? undefined),\n ),\n );\n}\n\ntype NullishField<S extends $ZodType> = ReturnType<typeof nullishField<S>>;\n\n// A scalar CSS value schema (the tokens' grammars are all string-valued).\ntype ScalarValueSchema = $ZodType<string, string>;\n\ntype BreakpointMapSchema = $ZodType<\n BlogBreakpointMap<string>,\n { base: string } & { [K in BlogBreakpoint]?: string | null | undefined }\n>;\n\n// Breakpoint map for one token. `base` is required and validated with the\n// token's own grammar; each breakpoint key is optional, null-tolerant (wire\n// nulls) and individually DROPPED when invalid (`.catch(undefined)`) — the\n// same per-value sanitization philosophy as the rest of this file. Unknown\n// keys are stripped by zod's default object behavior.\nfunction breakpointMapSchema(valueSchema: ScalarValueSchema): BreakpointMapSchema {\n return z.object({\n base: valueSchema,\n sm: z.catch(nullishField(valueSchema), undefined),\n md: z.catch(nullishField(valueSchema), undefined),\n lg: z.catch(nullishField(valueSchema), undefined),\n xl: z.catch(nullishField(valueSchema), undefined),\n \"2xl\": z.catch(nullishField(valueSchema), undefined),\n \"3xl\": z.catch(nullishField(valueSchema), undefined),\n }) as unknown as BreakpointMapSchema;\n}\n\ntype ResponsiveSchema = z.ZodMiniUnion<[ScalarValueSchema, BreakpointMapSchema]>;\n\n/**\n * Schema factory for a responsive token: the token's scalar grammar OR a\n * {@link BlogBreakpointMap} of it. A map missing/failing `base` fails the\n * whole union (override parsing then drops the entire field via the\n * field-level `.catch`); an invalid non-base breakpoint value is dropped\n * per-value while the rest of the map survives.\n */\nexport function responsive(valueSchema: ScalarValueSchema): ResponsiveSchema {\n return z.union([valueSchema, breakpointMapSchema(valueSchema)]);\n}\n\n// ---------------------------------------------------------------------------\n// Full theme schemas (strict — every field required and valid)\n// ---------------------------------------------------------------------------\n\n// Flat color shape (every key → the plain color grammar). The full schema,\n// the deep-partial dark palette, and the override schema all derive from this\n// ONE shape so the optional `heading` never gets double-wrapped in nullish\n// layers (which would emit nested anyOfs in the JSON Schema snapshot).\nconst blogThemeColorsShape = {\n background: colorValueSchema,\n surface: colorValueSchema,\n text: colorValueSchema,\n text_muted: colorValueSchema,\n primary: colorValueSchema,\n primary_foreground: colorValueSchema,\n link: colorValueSchema,\n border: colorValueSchema,\n code_background: colorValueSchema,\n heading: colorValueSchema,\n} as const;\n\nexport const blogThemeColorsSchema = z.object({\n ...blogThemeColorsShape,\n // The ONE optional color in a resolved theme: when unset, headings fall back\n // to `text` in the stylesheet (`--_heading-color`), so `blogThemeToCssVars`\n // only emits `--ara-blog-color-heading` when a theme actually sets it.\n heading: nullishField(colorValueSchema),\n});\n\nexport const blogThemeTypographySchema = z.object({\n heading_family: fontFamilySchema,\n body_family: fontFamilySchema,\n heading_weight: fontWeightSchema,\n body_weight: fontWeightSchema,\n h1_size: responsive(lengthValueSchema),\n h2_size: responsive(lengthValueSchema),\n h3_size: responsive(lengthValueSchema),\n h4_size: responsive(lengthValueSchema),\n body_size: responsive(lengthValueSchema),\n body_line_height: responsive(lineHeightValueSchema),\n small_size: responsive(lengthValueSchema),\n // Scalar (non-responsive) length; the grammar allows negative em values.\n // h1/h2 consume it directly, h3/h4 at half strength (calc in the base CSS).\n heading_letter_spacing: lengthValueSchema,\n // Figure caption text alignment (left/center/right). Site-owner override;\n // default center in the base CSS if unset.\n caption_align: alignValueSchema,\n});\n\nexport const blogThemeSpacingSchema = z.object({\n content_max_width: responsive(lengthValueSchema),\n block_gap: responsive(lengthValueSchema),\n section_gap: responsive(lengthValueSchema),\n});\n\n// Deep-partial companion to `blogThemeColorsSchema` for the optional dark\n// palette: each color is individually optional AND null-tolerant (see\n// `nullishField`), mirroring the Pydantic side's `str | None = None` fields.\nconst blogThemeColorsPartialSchema = z.object(\n Object.fromEntries(\n Object.keys(blogThemeColorsShape).map((key) => [key, nullishField(colorValueSchema)]),\n ) as { [K in keyof typeof blogThemeColorsShape]: NullishField<typeof colorValueSchema> },\n);\n\nexport const blogThemeSchema = z.object({\n colors: blogThemeColorsSchema,\n colors_dark: nullishField(blogThemeColorsPartialSchema),\n typography: blogThemeTypographySchema,\n spacing: blogThemeSpacingSchema,\n radius: responsive(lengthValueSchema),\n});\n\nexport type BlogThemeColors = z.infer<typeof blogThemeColorsSchema>;\nexport type BlogThemeTypography = z.infer<typeof blogThemeTypographySchema>;\nexport type BlogThemeSpacing = z.infer<typeof blogThemeSpacingSchema>;\nexport type BlogTheme = z.infer<typeof blogThemeSchema>;\n\n// ---------------------------------------------------------------------------\n// Override schema (deep-partial + per-field drop-on-invalid)\n// ---------------------------------------------------------------------------\n\n// Each field is nullish (`null` on the wire — the backend's `model_dump()`\n// emits explicit nulls for unset fields — folds into `undefined`) +\n// `.catch(undefined)`: an invalid individual value is dropped (falls back to\n// the layer below at resolve time) without failing the group or the whole\n// override. A non-object group likewise drops to undefined.\n// `Record<string, z.ZodType>` rather than `z.ZodRawShape`: in Zod 4 the\n// `$ZodType` is the shared base both the classic and the Mini builders\n// implement.\nfunction dropInvalidFields<T extends Record<string, $ZodType>>(\n shape: T,\n): { [K in keyof T]: z.ZodMiniCatch<NullishField<T[K]>> } {\n const out: Record<string, $ZodType> = {};\n for (const [key, schema] of Object.entries(shape)) {\n out[key] = z.catch(nullTolerant(schema), undefined);\n }\n return out as { [K in keyof T]: z.ZodMiniCatch<NullishField<T[K]>> };\n}\n\n// Null-tolerant wrapper that keeps responsive unions FLAT on the wire.\n// Wrapping a union in `.nullable()` would emit nested `anyOf`s\n// (`anyOf: [anyOf: [scalar, map], null]`) in the JSON Schema snapshot, while\n// the Pydantic mirror's `str | Map | None` emits a flat three-branch `anyOf`\n// — so for unions we splice `z.null()` into the union itself instead.\nfunction nullTolerant(schema: $ZodType): $ZodType {\n if (schema instanceof z.ZodMiniUnion) {\n const options = (schema as z.ZodMiniUnion<[$ZodType, $ZodType, ...$ZodType[]]>).def.options;\n return z.pipe(\n z.optional(z.union([...options, z.null()])),\n z.transform((v: unknown) => v ?? undefined),\n );\n }\n return nullishField(schema);\n}\n\n// Built from the flat shape (NOT blogThemeColorsSchema.shape) so the optional\n// `heading` gets exactly one nullish layer, like every other override field.\nconst blogThemeColorsOverrideSchema = z.object(dropInvalidFields(blogThemeColorsShape));\nconst blogThemeTypographyOverrideSchema = z.object(\n dropInvalidFields(blogThemeTypographySchema.shape),\n);\nconst blogThemeSpacingOverrideSchema = z.object(dropInvalidFields(blogThemeSpacingSchema.shape));\n\nexport const blogThemeOverrideSchema = z.object(\n dropInvalidFields({\n colors: blogThemeColorsOverrideSchema,\n colors_dark: blogThemeColorsOverrideSchema,\n typography: blogThemeTypographyOverrideSchema,\n spacing: blogThemeSpacingOverrideSchema,\n radius: responsive(lengthValueSchema),\n }),\n);\n\nexport type BlogThemeOverride = z.infer<typeof blogThemeOverrideSchema>;\n\n// ---------------------------------------------------------------------------\n// Base theme — neutral editorial defaults\n// ---------------------------------------------------------------------------\n\nexport const BASE_BLOG_THEME: BlogTheme = {\n colors: {\n background: \"#ffffff\",\n surface: \"#f5f5f5\",\n text: \"#171717\",\n text_muted: \"#6b6b6b\",\n primary: \"#171717\",\n primary_foreground: \"#ffffff\",\n link: \"#1d4ed8\",\n border: \"#e5e5e5\",\n code_background: \"#f5f5f5\",\n },\n typography: {\n heading_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n body_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n // Responsive defaults: comfortable mobile sizes, larger from `md` up.\n // This replaces the hardcoded mobile media query the base CSS used to need.\n h1_size: { base: \"1.9rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n small_size: \"0.875rem\",\n heading_letter_spacing: \"-0.02em\",\n caption_align: \"center\",\n },\n spacing: {\n content_max_width: \"42rem\",\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"8px\",\n};\n\n// ---------------------------------------------------------------------------\n// Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitize an untrusted theme(-override) object. Invalid individual values are\n * dropped; total garbage (non-object, arrays, …) yields `{}`. Never throws.\n */\nexport function sanitizeBlogTheme(raw: unknown): BlogThemeOverride {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const parsed = blogThemeOverrideSchema.safeParse(raw);\n return parsed.success ? parsed.data : {};\n}\n\nfunction mergeGroup<T extends Record<string, unknown>>(base: T, over: Partial<T> | undefined): T {\n if (!over) return base;\n const out = { ...base };\n for (const [key, value] of Object.entries(over)) {\n if (value !== undefined) (out as Record<string, unknown>)[key] = value;\n }\n return out;\n}\n\n/**\n * Deep-merge override layers onto {@link BASE_BLOG_THEME}. Later layers win;\n * `null`/`undefined` layers and `undefined` fields are skipped, so the result\n * is always a fully-populated {@link BlogTheme}.\n *\n * Responsive tokens merge at TOKEN granularity: a later layer's value —\n * scalar or breakpoint map — REPLACES the whole token. There is deliberately\n * no per-breakpoint merge (a layer overriding `h1_size: \"2rem\"` on top of a\n * base map must not inherit the base's `md` entry).\n */\nexport function resolveBlogTheme(\n ...layers: Array<BlogThemeOverride | null | undefined>\n): BlogTheme {\n let resolved: BlogTheme = {\n colors: { ...BASE_BLOG_THEME.colors },\n colors_dark: BASE_BLOG_THEME.colors_dark ? { ...BASE_BLOG_THEME.colors_dark } : undefined,\n typography: { ...BASE_BLOG_THEME.typography },\n spacing: { ...BASE_BLOG_THEME.spacing },\n radius: BASE_BLOG_THEME.radius,\n };\n for (const layer of layers) {\n if (!layer) continue;\n resolved = {\n colors: mergeGroup(resolved.colors, layer.colors),\n colors_dark:\n layer.colors_dark || resolved.colors_dark\n ? mergeGroup(resolved.colors_dark ?? {}, layer.colors_dark)\n : undefined,\n typography: mergeGroup(resolved.typography, layer.typography),\n spacing: mergeGroup(resolved.spacing, layer.spacing),\n radius: layer.radius ?? resolved.radius,\n };\n }\n return resolved;\n}\n\n// (split/join instead of String#replaceAll — the package lib target is ES2020.)\nfunction kebab(key: string): string {\n return key.split(\"_\").join(\"-\");\n}\n\n// Emit one (possibly responsive) token: the base var carries the scalar (or\n// the map's `base`), and each PRESENT breakpoint key adds a `-<bp>` suffixed\n// var (e.g. `--ara-blog-type-h1-size-md`). Absent breakpoints emit nothing —\n// the stylesheet's media queries fall back to the base var.\nfunction emitResponsiveVar(\n vars: Record<string, string>,\n name: string,\n value: ResponsiveValue<string | number>,\n): void {\n const map = toBlogBreakpointMap(value);\n vars[name] = String(map.base);\n for (const bp of BLOG_BREAKPOINTS) {\n const bpValue = map[bp];\n if (bpValue !== undefined) vars[`${name}-${bp}`] = String(bpValue);\n }\n}\n\n/**\n * Flatten a resolved theme into `--ara-blog-*` CSS custom properties for an\n * inline `style` attribute. When `colors_dark` is present, color values are\n * emitted as `light-dark(lightVal, darkVal)` and `color-scheme: light dark`\n * is included so the browser picks the active side. Responsive tokens emit\n * their base var plus `-sm`/`-md`/… suffixed vars for present breakpoints.\n */\nexport function blogThemeToCssVars(theme: BlogTheme): Record<string, string> {\n const vars: Record<string, string> = {};\n const dark = theme.colors_dark;\n const hasDark = !!dark && Object.values(dark).some((v) => v !== undefined);\n\n // Union of light + dark keys: an optional color (only `heading` today) set\n // ONLY on the dark side still needs its var emitted.\n const colorKeys = new Set([...Object.keys(theme.colors), ...(dark ? Object.keys(dark) : [])]);\n for (const key of colorKeys) {\n const light = theme.colors[key as keyof BlogThemeColors];\n const darkValue = hasDark ? dark?.[key as keyof BlogThemeColors] : undefined;\n // Optional colors (only `heading` today) emit nothing when unset — the\n // stylesheet's fallback (`--_heading-color` → text) takes over. A theme\n // that sets ONLY the dark side still gets a var: the light side falls\n // back to `text`, which is what the stylesheet would use anyway.\n const lightValue = light ?? (darkValue !== undefined ? theme.colors.text : undefined);\n if (lightValue === undefined) continue;\n vars[`--ara-blog-color-${kebab(key)}`] = darkValue\n ? `light-dark(${lightValue}, ${darkValue})`\n : lightValue;\n }\n // camelCase: these vars land in a React `style` prop, which rejects kebab\n // names for real (non-custom) CSS properties.\n if (hasDark) vars.colorScheme = \"light dark\";\n\n for (const [key, value] of Object.entries(theme.typography)) {\n emitResponsiveVar(vars, `--ara-blog-type-${kebab(key)}`, value);\n }\n for (const [key, value] of Object.entries(theme.spacing)) {\n emitResponsiveVar(vars, `--ara-blog-space-${kebab(key)}`, value);\n }\n emitResponsiveVar(vars, \"--ara-blog-radius\", theme.radius);\n return vars;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport { DEFAULT_CDN_BASE_URL } from \"../tracking-config-runtime\";\nimport {\n parseBlogManifest,\n parseBlogPostContent,\n type BlogManifest,\n type BlogManifestEntry,\n type BlogPostContent,\n} from \"./schema\";\n\n/**\n * Blog CDN client — read-only fetchers over the published blog objects.\n *\n * Designed for Next.js server components but framework-free: the Next-specific\n * `next.revalidate` fetch extension is expressed as a local structural type\n * ({@link NextFetchInit}) so we never import `next` types and a plain\n * `fetch` (Node, tests) simply ignores the extra property.\n *\n * Error policy: reads are best-effort. Network failures, non-2xx responses,\n * and unparseable bodies all resolve to `null` / `[]` — a blog page must\n * degrade, never crash the host site.\n */\n\n/** `RequestInit` + the Next.js data-cache extension (structural — no `next` import). */\nexport type NextFetchInit = RequestInit & {\n next?: { revalidate?: number | false; tags?: string[] };\n};\n\nexport interface BlogClientConfig {\n /**\n * Reserved for future authenticated reads (draft previews fetched from the\n * API instead of the public CDN). Published content is world-readable, so\n * the key is currently NOT sent — but it is part of the config contract so\n * client sites don't need a breaking config change when draft reads ship.\n */\n apiKey: string;\n /**\n * CDN origin, e.g. `https://cdn.aranova.io` (trailing slash tolerated).\n * Blank or whitespace falls back to {@link DEFAULT_CDN_BASE_URL} — the same\n * contract the tracking-config resolver has had since PR #180. The blog\n * client originally shipped without the fallback, so an unset env var made\n * the manifest URL relative; server-side fetch rejected it and the fail-soft\n * error policy rendered a published blog as permanently empty.\n */\n cdnBaseUrl: string;\n /** Opaque per-business namespace under `blogs/v1/`. */\n namespace: string;\n /** Manifest ISR revalidate window (seconds). Default 300. */\n manifestRevalidateSeconds?: number;\n /** Injectable for tests / non-global-fetch runtimes. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface BlogClient {\n /** The mutable manifest (index of published posts), or `null` on any miss. */\n getManifest(): Promise<BlogManifest | null>;\n /** Manifest entries sorted `published_at` desc. `[]` on any miss. */\n listPosts(): Promise<BlogManifestEntry[]>;\n /** Full post content for a slug, or `null` when unknown/unfetchable. */\n getPost(slug: string): Promise<BlogPostContent | null>;\n /** `generateStaticParams()`-shaped slug list. */\n getStaticParams(): Promise<Array<{ slug: string }>>;\n}\n\nexport function createBlogClient(config: BlogClientConfig): BlogClient {\n registerCapability(\"blog_rendering\");\n // Blank-as-unset, mirroring `resolveTrackingConfigUrl`: a present-but-empty\n // env var must not produce a relative (same-origin) manifest URL.\n const base = (config.cdnBaseUrl.trim() || DEFAULT_CDN_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch: typeof fetch = (...args) => (config.fetchImpl ?? fetch)(...args);\n\n async function fetchJson(url: string, init: NextFetchInit): Promise<unknown | null> {\n try {\n const response = await doFetch(url, init);\n if (!response.ok) return null;\n return (await response.json()) as unknown;\n } catch {\n return null;\n }\n }\n\n async function getManifest(): Promise<BlogManifest | null> {\n // MUTABLE object (Cache-Control max-age=60 at the CDN) — cached through the\n // Next.js data cache with a revalidate window instead of force-cache.\n const init = {\n next: { revalidate: config.manifestRevalidateSeconds ?? 300 },\n } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/blogs/v1/${config.namespace}/manifest.json`, init);\n return raw === null ? null : parseBlogManifest(raw);\n }\n\n async function listPosts(): Promise<BlogManifestEntry[]> {\n const manifest = await getManifest();\n if (!manifest) return [];\n return [...manifest.posts].sort((a, b) => b.published_at.localeCompare(a.published_at));\n }\n\n async function getPost(slug: string): Promise<BlogPostContent | null> {\n const manifest = await getManifest();\n const entry = manifest?.posts.find((post) => post.slug === slug);\n if (!entry) return null;\n // IMMUTABLE object (hash-addressed, max-age=1y) — cache forever; a content\n // change publishes a new hash, which the manifest points at.\n const init = { cache: \"force-cache\" } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/${entry.content_key.replace(/^\\/+/, \"\")}`, init);\n return raw === null ? null : parseBlogPostContent(raw);\n }\n\n async function getStaticParams(): Promise<Array<{ slug: string }>> {\n const posts = await listPosts();\n return posts.map((post) => ({ slug: post.slug }));\n }\n\n return { getManifest, listPosts, getPost, getStaticParams };\n}\n","// Curated font/type-scale presets for the blog theme — React-free, part of\n// the `@aranova/tracking-next/blog` surface (re-exported via blog-public.ts).\n//\n// Each preset is a plain `BlogThemeOverride`: a font pairing with REAL\n// web-safe fallback stacks, a responsive type scale, weights, letter-spacing,\n// spacing, and radius. Colors are deliberately (almost entirely) UNSET so the\n// client site's own palette wins — the only exception is a subtle\n// `code_background` where the code surface is part of the preset's identity.\n//\n// Contract (tested in __tests__/presets.test.ts): every preset passes\n// `sanitizeBlogTheme(preset)` UNCHANGED — a preset must never rely on values\n// the sanitizer would drop.\n\nimport type { BlogThemeOverride } from \"./theme\";\n\nexport type BlogThemePresetName = \"editorial\" | \"modern\" | \"warm\" | \"technical\";\n\n/**\n * Named theme presets. Layer one under your site palette:\n *\n * ```ts\n * const theme = resolveBlogTheme(\n * createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\", // next/font variable\n * body: \"var(--font-sans)\",\n * }),\n * { colors: { primary: \"#0f766e\", link: \"#0f766e\" } },\n * );\n * ```\n */\nexport const BLOG_THEME_PRESETS: Record<BlogThemePresetName, BlogThemeOverride> = {\n /** Serif display headings over a humanist sans body — long-form magazine feel. */\n editorial: {\n typography: {\n heading_family: \"'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif\",\n body_family: \"'Source Sans 3', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"2rem\", md: \"2.5rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.875rem\" },\n h3_size: \"1.3rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.015em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"6px\",\n },\n /** Geometric sans pairing (Inter-first) — tight, contemporary product voice. */\n modern: {\n typography: {\n heading_family: \"'Inter', 'SF Pro Display', 'Segoe UI', Roboto, sans-serif\",\n body_family: \"'Inter', 'SF Pro Text', 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"2.1rem\", md: \"2.75rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n heading_letter_spacing: \"-0.03em\",\n },\n spacing: {\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"12px\",\n },\n /** Rounded-friendly pairing with a softer scale — approachable, human tone. */\n warm: {\n typography: {\n heading_family: \"'Nunito', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n body_family: \"'Nunito Sans', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"1.9rem\", md: \"2.35rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.7rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"16px\",\n },\n /** Workmanlike sans with a prominent code surface — docs/engineering feel. */\n technical: {\n colors: {\n // The one preset that ships a color: a cool code surface is part of the\n // \"technical\" identity. Everything else stays on the site palette.\n code_background: \"#eef1f5\",\n },\n colors_dark: {\n code_background: \"#1b1f27\",\n },\n typography: {\n heading_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n body_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"1.85rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.65rem\" },\n h3_size: \"1.2rem\",\n h4_size: \"1.0625rem\",\n body_size: \"1rem\",\n body_line_height: \"1.65\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.15rem\",\n section_gap: \"2.75rem\",\n },\n radius: \"6px\",\n },\n};\n\n/** Real font sources for a preset: next/font CSS variables (or literal family names). */\nexport interface BlogFontSources {\n /** e.g. `\"var(--font-serif)\"` from `next/font`'s `variable` option. */\n heading?: string;\n /** e.g. `\"var(--font-sans)\"`. */\n body?: string;\n}\n\n// Prepend a caller-supplied family (usually a next/font `var(--…)`) onto the\n// preset's fallback stack. The caller value may itself be a comma-separated\n// stack; either way the preset's web-safe fallbacks ride behind it.\nfunction prependFamily(front: string | undefined, stack: string | undefined): string | undefined {\n if (!front) return stack;\n return stack ? `${front}, ${stack}` : front;\n}\n\n/**\n * Wire a site's real fonts into a preset: returns a NEW override (no\n * mutation) whose heading/body families are `\"<font>, <preset fallback\n * stack>\"` — pass your `next/font` variable names and the preset's web-safe\n * fallbacks keep covering the pre-hydration/fallback paint.\n *\n * ```ts\n * const serif = Source_Serif_4({ subsets: [\"latin\"], variable: \"--font-serif\" });\n * const sans = Inter({ subsets: [\"latin\"], variable: \"--font-sans\" });\n * const override = createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\",\n * body: \"var(--font-sans)\",\n * });\n * ```\n */\nexport function createBlogFontTheme(\n preset: BlogThemeOverride,\n fonts: BlogFontSources,\n): BlogThemeOverride {\n const heading_family = prependFamily(fonts.heading, preset.typography?.heading_family);\n const body_family = prependFamily(fonts.body, preset.typography?.body_family);\n return {\n ...preset,\n typography: {\n ...preset.typography,\n ...(heading_family !== undefined ? { heading_family } : {}),\n ...(body_family !== undefined ? { body_family } : {}),\n },\n };\n}\n","// Client-side className overrides for the blog view — the escape hatch for\n// sites that want to hook a Tailwind/utility class onto a specific surface\n// without replacing the whole component (see `components` for that) or\n// writing global CSS against the fixed `.aranova-blog-*` selectors (still the\n// simplest option and requires no API — these classes are stable and public).\n//\n// `joinClassNames` is a plain append, not a Tailwind-merge: every base class\n// here is an SDK-internal selector (`aranova-blog-callout`, …), never a\n// utility class, so there is nothing for an override to conflict with —\n// dropping a `tailwind-merge` dependency into a framework-free package for a\n// non-conflict is unnecessary weight.\nexport function joinClassNames(base: string, extra?: string): string {\n return extra ? `${base} ${extra}` : base;\n}\n\n/**\n * One optional override per styleable surface. Every key merges onto its\n * fixed base class (never replaces it), so the `.aranova-blog-*` selector\n * still applies — this is additive, not a restyle mechanism.\n */\nexport interface BlogClassNameOverrides {\n /** The `<article>` root. */\n article?: string;\n /** The content/aside grid wrapper. */\n layout?: string;\n /** The content column. */\n content?: string;\n /** The aside rail (table of contents). */\n aside?: string;\n /** `<Callout>` MDX component. */\n callout?: string;\n /** `<Cta>` MDX component. */\n cta?: string;\n /** `<BlogImage>` MDX component's `<figure>` wrapper. */\n figure?: string;\n /** `<FaqItem>` MDX component's `<details>` wrapper. */\n faqItem?: string;\n /** `<FaqItem>`'s `<summary>`. */\n faqQuestion?: string;\n /** `<FaqItem>`'s answer `<div>`. */\n faqAnswer?: string;\n /** `<KeyTakeaways>` MDX component. */\n keyTakeaways?: string;\n /** The `<table>` overflow wrapper `<div>`. */\n tableWrap?: string;\n /** `BlogPostMeta` byline row. */\n meta?: string;\n /** `BlogBreadcrumbs` nav. */\n breadcrumbs?: string;\n /** `BlogTocShell` / the animated `TocPanel` island. */\n toc?: string;\n /** The `ReadingProgress` island. */\n progress?: string;\n}\n","import type { BlogManifestEntry, BlogPostContent } from \"./schema\";\n\n/**\n * SEO builders — React-free helpers that turn post content into Next.js\n * `Metadata`-shaped objects, BlogPosting JSON-LD, and sitemap entries.\n *\n * Deliberately does NOT import `next` types: {@link BlogPostMetadata} is a\n * local structural interface whose shape is spread-compatible with Next's\n * `Metadata`, so the helpers stay usable from any framework (and from the\n * internal dashboard's preview renderer).\n */\n\nexport interface BlogSeoOptions {\n /** Site origin, e.g. `https://client.example.com`. */\n siteUrl: string;\n /** Route the blog is mounted at. Default `/blog`. */\n basePath?: string;\n}\n\nexport interface BlogJsonLdOptions extends BlogSeoOptions {\n /** Emits `publisher: { \"@type\": \"Organization\", name }` when given. */\n publisherName?: string;\n}\n\nexport interface BlogPostMetadataImage {\n url: string;\n width?: number;\n height?: number;\n alt?: string;\n}\n\n/** Structural subset of Next.js `Metadata` — spread it into `generateMetadata`. */\nexport interface BlogPostMetadata {\n title: string;\n description: string;\n alternates: { canonical: string };\n openGraph: {\n title: string;\n description: string;\n type: \"article\";\n url: string;\n images?: BlogPostMetadataImage[];\n publishedTime?: string;\n };\n}\n\nexport interface BlogSitemapEntry {\n url: string;\n lastModified: string;\n}\n\n/** Join URL segments without doubling or dropping slashes. */\nfunction joinUrl(base: string, ...paths: Array<string | undefined>): string {\n let out = base.replace(/\\/+$/, \"\");\n for (const path of paths) {\n const segment = path?.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (segment) out += `/${segment}`;\n }\n return out;\n}\n\n// The canonical URL is ALWAYS derived from the client site's own routing\n// (`siteUrl` + `basePath` + slug) — the client mounts the route, so client\n// routing wins. The backend's `seo.canonical_path` is informational only: it\n// assumes a `/blog` mount and would emit a canonical pointing at a\n// non-existent path on sites mounted elsewhere (e.g. `/news`). This also\n// keeps metadata, JSON-LD, and sitemap URLs derived from one rule.\nfunction canonicalUrl(post: BlogPostContent, opts: BlogSeoOptions): string {\n return joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug);\n}\n\nfunction ogImages(post: BlogPostContent): BlogPostMetadataImage[] | undefined {\n if (post.seo.og_image) return [{ url: post.seo.og_image }];\n const hero = post.images.find((image) => image.role === \"hero\");\n if (!hero) return undefined;\n return [{ url: hero.url, width: hero.width, height: hero.height, alt: hero.alt }];\n}\n\n/** Build a Next `Metadata`-shaped object for a post's `generateMetadata`. */\nexport function buildBlogPostMetadata(\n post: BlogPostContent,\n opts: BlogSeoOptions,\n): BlogPostMetadata {\n const url = canonicalUrl(post, opts);\n const title = post.seo.meta_title || post.title;\n const description = post.seo.meta_description || post.description;\n const images = ogImages(post);\n return {\n title,\n description,\n alternates: { canonical: url },\n openGraph: {\n title,\n description,\n type: \"article\",\n url,\n ...(images ? { images } : {}),\n ...(post.published_at ? { publishedTime: post.published_at } : {}),\n },\n };\n}\n\n/**\n * Build a `BlogPosting` JSON-LD object. When the backend shipped a non-empty\n * `post.jsonld`, its values are spread over the locally derived defaults so\n * backend-authored enrichment (author, keywords, …) wins — EXCEPT the pinned\n * fields spread after it: `@context`/`@type` (type identity of the emitted\n * document) and `url`/`mainEntityOfPage` (client-domain truths derived from\n * the site's own routing) can never be overridden by backend data.\n */\nexport function buildBlogPostingJsonLd(\n post: BlogPostContent,\n opts: BlogJsonLdOptions,\n): Record<string, unknown> {\n const url = canonicalUrl(post, opts);\n const images = post.images.map((image) => image.url);\n const doc: Record<string, unknown> = {\n headline: post.title,\n description: post.seo.meta_description || post.description,\n datePublished: post.published_at,\n dateModified: post.updated_at ?? post.published_at,\n ...(images.length > 0 ? { image: images } : {}),\n ...(opts.publisherName\n ? { publisher: { \"@type\": \"Organization\", name: opts.publisherName } }\n : {}),\n };\n const backend = post.jsonld;\n const merged =\n backend && typeof backend === \"object\" && Object.keys(backend).length > 0\n ? { ...doc, ...backend }\n : doc;\n return {\n ...merged,\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n url,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": url },\n };\n}\n\nexport interface BlogFaqPair {\n question: string;\n answer: string;\n}\n\n// `<FaqItem question=\"…\">…</FaqItem>` (or '…') with the answer captured lazily.\nconst FAQ_ITEM_RE = /<FaqItem\\s+question=(?:\"([^\"]*)\"|'([^']*)')\\s*>([\\s\\S]*?)<\\/FaqItem>/g;\n// Fenced code blocks — FaqItem text inside a fence is code, not an FAQ.\nconst CODE_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?(?:^ {0,3}\\1`* *$|(?![\\s\\S]))/gm;\n// Component tags + inline code markers stripped from answer prose.\nconst ANSWER_TAG_RE = /<[^>]*>/g;\n\n/**\n * Extract `<FaqItem question>` question/answer pairs from a post's MDX.\n *\n * Deliberately a light regex, not a parser: it runs on documents that have\n * ALREADY passed `validateBlogMdx` at both the persist and render boundaries,\n * where `<FaqItem>` is guaranteed to carry exactly one quoted-string\n * `question` attribute and flow-markdown children. On anything else it simply\n * extracts fewer/no pairs — it never renders content, so there is no\n * injection surface. Fenced code blocks are dropped first so documentation\n * ABOUT the component is not read as an FAQ. Answers are flattened to plain\n * text (tags stripped, markdown markers left as-is, whitespace collapsed) —\n * Google accepts plain-text `acceptedAnswer.text`.\n */\nexport function extractFaqPairs(mdx: string): BlogFaqPair[] {\n const withoutCode = mdx.replaceAll(CODE_FENCE_RE, \"\");\n const pairs: BlogFaqPair[] = [];\n for (const match of withoutCode.matchAll(FAQ_ITEM_RE)) {\n const question = (match[1] ?? match[2] ?? \"\").trim();\n const answer = match[3].replaceAll(ANSWER_TAG_RE, \" \").replaceAll(/\\s+/g, \" \").trim();\n if (question && answer) pairs.push({ question, answer });\n }\n return pairs;\n}\n\n/**\n * Build a schema.org `FAQPage` JSON-LD object from extracted FAQ pairs\n * (see {@link extractFaqPairs}). Returns `null` when there are no pairs —\n * an empty `FAQPage` is invalid structured data, so callers can gate the\n * `<script type=\"application/ld+json\">` emission on the return value.\n */\nexport function buildFaqJsonLd(pairs: BlogFaqPair[]): Record<string, unknown> | null {\n if (pairs.length === 0) return null;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: pairs.map((pair) => ({\n \"@type\": \"Question\",\n name: pair.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: pair.answer },\n })),\n };\n}\n\nconst JSONLD_ESCAPES: Record<string, string> = {\n \"<\": \"\\\\u003c\",\n \">\": \"\\\\u003e\",\n \"&\": \"\\\\u0026\",\n \"\\u2028\": \"\\\\u2028\",\n \"\\u2029\": \"\\\\u2029\",\n};\n\n/**\n * Serialize a JSON-LD document for embedding in a\n * `<script type=\"application/ld+json\">` tag. Escapes `<`, `>`, `&` and the\n * U+2028/U+2029 line separators as `\\uXXXX` sequences (valid JSON, identical\n * parse result) so backend-influenced content can never break out of the\n * script context (`</script>` injection) or produce invalid JS source.\n */\nexport function serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data).replaceAll(/[<>&\\u2028\\u2029]/g, (ch) => JSONLD_ESCAPES[ch] ?? ch);\n}\n\n/** Sitemap rows for every published post (shape matches Next's `MetadataRoute.Sitemap`). */\nexport function buildBlogSitemapEntries(\n posts: BlogManifestEntry[],\n opts: BlogSeoOptions,\n): BlogSitemapEntry[] {\n return posts.map((post) => ({\n url: joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug),\n lastModified: post.updated_at,\n }));\n}\n","// React-free table-of-contents + reading-time helpers for blog MDX. Pure mdast\n// walk over the SAME front-end the validator uses (remark-parse + remark-mdx),\n// so heading extraction can never disagree with what the renderer compiles.\n//\n// Slug parity contract: ids are produced with a fresh `GithubSlugger` per\n// document, walked in document order over h2/h3 — EXACTLY what `rehype-slug`\n// does at render time in `BlogPostView`... almost. rehype-slug slugs EVERY\n// heading rank (h1–h6), and duplicate-slug `-N` suffixes are allocated from\n// that full sequence. To keep extracted ids byte-identical to rendered ids,\n// the walk below feeds every heading through the slugger but only REPORTS\n// h2/h3 (the ranks a table of contents links to). Verified by the corpus\n// parity test in `__tests__/toc.test.ts`.\n\nimport GithubSlugger from \"github-slugger\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\n/** One table-of-contents entry: an h2/h3 with its rendered anchor id. */\nexport interface BlogHeading {\n /** Heading rank — a TOC only links section (h2) and subsection (h3) heads. */\n depth: 2 | 3;\n /** Plain-text heading content (formatting stripped, code/emphasis flattened). */\n text: string;\n /** The `id` rehype-slug stamps on the rendered heading element. */\n id: string;\n}\n\n// Minimal structural view of the mdast nodes we walk (same approach as\n// validate-mdx.ts — keeps the public d.ts free of @types/mdast).\ninterface MdastNode {\n type: string;\n depth?: number;\n value?: string;\n children?: MdastNode[];\n}\n\n/** Textual leaf types whose `value` contributes to a heading's visible text. */\nconst TEXT_LEAF_TYPES = new Set([\"text\", \"inlineCode\"]);\n\nfunction headingText(node: MdastNode): string {\n let text = \"\";\n const stack: MdastNode[] = [];\n const roots = node.children ?? [];\n for (let i = roots.length - 1; i >= 0; i--) {\n stack.push(roots[i]);\n }\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n if (TEXT_LEAF_TYPES.has(current.type) && typeof current.value === \"string\") {\n text += current.value;\n }\n if (Array.isArray(current.children)) {\n for (let i = current.children.length - 1; i >= 0; i--) {\n stack.push(current.children[i]);\n }\n }\n }\n return text;\n}\n\n/**\n * Extract the h2/h3 outline of a blog MDX document with the exact anchor ids\n * `BlogPostView` renders (rehype-slug parity — see the module header). Returns\n * `[]` for unparseable input; feed it the same `mdx` the view receives.\n */\nexport function extractBlogHeadings(mdx: string): BlogHeading[] {\n let tree: MdastNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdastNode;\n } catch {\n return [];\n }\n\n const slugger = new GithubSlugger();\n const headings: BlogHeading[] = [];\n\n // Document-order walk. Headings are flow content — they appear at the root\n // or inside containers (blockquote, list, Callout) — and rehype-slug slugs\n // them wherever they are, so the walk must descend everywhere.\n const stack: MdastNode[] = [tree];\n while (stack.length > 0) {\n const node = stack.pop();\n if (!node) break;\n if (node.type === \"heading\" && typeof node.depth === \"number\") {\n const text = headingText(node);\n // Every rank consumes a slug (dedupe-counter parity with rehype-slug),\n // but only h2/h3 land in the TOC.\n const id = slugger.slug(text);\n if (node.depth === 2 || node.depth === 3) {\n headings.push({ depth: node.depth, text, id });\n }\n }\n if (Array.isArray(node.children)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return headings;\n}\n\n/** Average adult reading speed used for the estimate, in words per minute. */\nconst WORDS_PER_MINUTE = 220;\n\n// Crude MDX-syntax strip for word counting: JSX tags, code fences/backticks,\n// heading/list/quote markers, link targets. Word-count precision beyond this\n// is noise at a \"N min read\" granularity.\nconst MDX_SYNTAX_RE = /<[^>]*>|```[^`]*```|`|^[#>\\-*+\\s]+|\\]\\([^)]*\\)|[[\\]!]/gm;\n\n/**\n * Estimated reading time of a blog MDX document, in whole minutes (>= 1).\n * Deliberately crude — strips MDX/markdown syntax and counts words at\n * {@link WORDS_PER_MINUTE}.\n */\nexport function estimateReadingTime(mdx: string): number {\n const words = mdx\n .replaceAll(MDX_SYNTAX_RE, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 0).length;\n return Math.max(1, Math.round(words / WORDS_PER_MINUTE));\n}\n","// Mechanical MDX grammar enforcement for generated blog posts — Layer 1 (the\n// authoritative render boundary). Pure and framework-free: no React, no\n// next-mdx-remote. `BlogPostView` runs this unconditionally BEFORE compileMDX;\n// the API mirrors the same grammar at persist time\n// (apps/api/src/services/blog_mdx_validation.py).\n//\n// Why parse here at all: next-mdx-remote v6 strips expression *evaluation*,\n// but intrinsic JSX/HTML elements (<script src>, <iframe>, event-handler\n// attributes, javascript: URLs) flow straight into served HTML — stored XSS\n// on customer domains. The component allow-list passed to the renderer is not\n// a boundary (intrinsics bypass it), and prompt rules are only instructions.\n//\n// No parser differential by construction: we parse with the SAME mdast\n// front-end @mdx-js/mdx uses internally (remark-parse + remark-mdx, versions\n// aligned with what next-mdx-remote resolves), so whatever the compiler would\n// see, this validator saw first. The grammar is default-deny: any node type\n// not explicitly whitelisted rejects the document.\n\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\nexport interface BlogMdxViolation {\n /** Stable machine-readable rule id (safe to surface — never raw content). */\n rule: string;\n message: string;\n line?: number;\n}\n\nexport type BlogMdxValidationResult = { ok: true } | { ok: false; violations: BlogMdxViolation[] };\n\n/** DoS guard: maximum accepted MDX source size, in UTF-16 code units. */\nexport const BLOG_MDX_MAX_SIZE = 500_000;\n/** DoS guard: maximum AST nesting depth (realistic posts stay under ~10). */\nexport const BLOG_MDX_MAX_DEPTH = 64;\n/** DoS guard: maximum total AST node count. */\nexport const BLOG_MDX_MAX_NODES = 100_000;\n/** Cap on reported violations so a hostile document can't flood the caller. */\nconst MAX_VIOLATIONS = 50;\n\n/**\n * One allowed component of the blog MDX grammar, in serializable form.\n * `attrs` is the EXACT attribute set — every attribute is required, nothing\n * else allowed; `selfClosing` components must have no children.\n *\n * `cssClass`/`promptUsage`/`promptDescription` exist SOLELY so this one\n * record can be the entire cross-language/cross-surface registry — see the\n * grammar doc comment below for what derives from each field.\n */\nexport interface BlogMdxComponentGrammarEntry {\n name: string;\n attrs: readonly string[];\n urlAttrs: readonly string[];\n selfClosing: boolean;\n /** Base CSS class the default renderer applies, when the component is styled. */\n cssClass?: string;\n /** Example JSX usage shown to the drafting/editing agents. */\n promptUsage: string;\n /** One-line description of when/how to use it, shown to the agents. */\n promptDescription: string;\n}\n\n/**\n * The component grammar — the SINGLE source of truth for the closed\n * component set (name/attrs/urlAttrs/selfClosing/cssClass/prompt copy). This\n * is the only place to touch when adding, removing, or changing a component;\n * everything else derives from it instead of hand-mirroring it:\n *\n * - the MDX validator specs below (grammar enforcement),\n * - the `BlogComponents` renderer contract (a mapped-type assertion in\n * `view/components.tsx` makes an entry without a renderer a compile error),\n * - the `resources.blog_mdx_grammar` snapshot in `events.schema.json`\n * (emitted verbatim by `src/events/build-schema.ts`), which\n * `apps/api/src/services/blog_mdx_components.py` is GENERATED from\n * (`apps/api/scripts/generate_blog_mdx_grammar.py`) — the backend has no\n * hand-typed mirror to drift out of sync,\n * - the agent prompt templates (`blog/_mdx_grammar.j2`, via the generated\n * Python module's `prompt_usage`/`prompt_description` fields),\n * - `styles.css` (a completeness test asserts every `cssClass` here has a\n * rule), and the MDX conformance corpus (a completeness test asserts every\n * `name` here has at least one accept-case fixture).\n */\nexport const BLOG_MDX_COMPONENT_GRAMMAR = [\n {\n name: \"Callout\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-callout\",\n promptUsage: \"<Callout>...</Callout>\",\n promptDescription: \"a highlighted tip/note block; no attributes.\",\n },\n {\n name: \"Cta\",\n attrs: [\"href\", \"label\"],\n urlAttrs: [\"href\"],\n selfClosing: true,\n cssClass: \"aranova-blog-cta\",\n promptUsage: '<Cta href=\"...\" label=\"...\" />',\n promptDescription: \"a call-to-action button (self-closing).\",\n },\n {\n name: \"BlogImage\",\n attrs: [\"id\"],\n urlAttrs: [],\n selfClosing: true,\n promptUsage: '<BlogImage id=\"...\" />',\n promptDescription:\n \"a generated image placeholder (self-closing). The `id` references an \" +\n \"image brief id from the plan; the renderer resolves it.\",\n },\n {\n name: \"FaqItem\",\n attrs: [\"question\"],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-faq-item\",\n promptUsage: '<FaqItem question=\"...\">...</FaqItem>',\n promptDescription:\n \"one FAQ entry: the required `question` attribute carries the question, \" +\n \"the children are the prose answer. Group consecutive `<FaqItem>`s \" +\n \"under an FAQ heading near the end of the post when the topic suits a Q&A section.\",\n },\n {\n name: \"KeyTakeaways\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-key-takeaways\",\n promptUsage: \"<KeyTakeaways>...</KeyTakeaways>\",\n promptDescription:\n \"a TL;DR box containing a standard markdown bullet list of 3-5 takeaways; \" +\n \"place it right after the H1 (and hero image).\",\n },\n] as const satisfies readonly BlogMdxComponentGrammarEntry[];\n\n/** Union of the allowed component names, derived from the grammar. */\nexport type BlogMdxComponentName = (typeof BLOG_MDX_COMPONENT_GRAMMAR)[number][\"name\"];\n\n/** The closed component set of the blog grammar (mirrors the agent prompts). */\nexport const BLOG_MDX_ALLOWED_COMPONENTS: readonly BlogMdxComponentName[] =\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => entry.name);\n\n/**\n * Standard-markdown mdast node types the grammar accepts. Everything else —\n * including `html`, the MDX expression/ESM node types, and any type added by\n * a future plugin — is rejected by default.\n */\nconst ALLOWED_NODE_TYPES = new Set([\n \"root\",\n \"heading\",\n \"paragraph\",\n \"text\",\n \"emphasis\",\n \"strong\",\n \"delete\",\n \"link\",\n \"image\",\n \"linkReference\",\n \"imageReference\",\n \"definition\",\n \"list\",\n \"listItem\",\n \"blockquote\",\n \"code\",\n \"inlineCode\",\n \"table\",\n \"tableRow\",\n \"tableCell\",\n \"thematicBreak\",\n \"break\",\n]);\n\n/** Named rejections for the node types that are the actual attack surface. */\nconst FORBIDDEN_NODE_TYPES: Record<string, { rule: string; message: string }> = {\n html: { rule: \"raw-html\", message: \"Raw HTML is not allowed\" },\n mdxjsEsm: { rule: \"esm\", message: \"import/export statements are not allowed\" },\n mdxFlowExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n mdxTextExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n};\n\ninterface ComponentSpec {\n /** Exact attribute set — every attribute is required, nothing else allowed. */\n attrs: readonly string[];\n /** Attributes whose (string) value must pass the URL policy. */\n urlAttrs: readonly string[];\n /** Self-closing components must have no children. */\n allowChildren: boolean;\n}\n\nconst COMPONENT_SPECS: Record<string, ComponentSpec> = Object.fromEntries(\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => [\n entry.name,\n { attrs: entry.attrs, urlAttrs: entry.urlAttrs, allowChildren: !entry.selfClosing },\n ]),\n);\n\nconst URL_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/;\nconst ALLOWED_URL_SCHEMES = new Set([\"http\", \"https\", \"mailto\"]);\n\n/**\n * Scheme whitelist: http(s)/mailto/relative (schemeless) only. ASCII control\n * characters and whitespace are stripped BEFORE scheme detection — browsers\n * ignore them when parsing URLs, so `java\\tscript:` must not slip through.\n * Returns the offending scheme, or null when the URL is acceptable.\n */\nfunction forbiddenUrlScheme(rawUrl: string): string | null {\n // Intentional control-character class — the whole point is stripping the\n // characters browsers ignore in URLs before the scheme check.\n // eslint-disable-next-line no-control-regex\n const cleaned = rawUrl.replaceAll(/[\\u0000-\\u0020\\u007F]/g, \"\");\n const match = URL_SCHEME_RE.exec(cleaned);\n if (!match) return null; // relative / fragment / protocol-relative\n const scheme = match[1].toLowerCase();\n return ALLOWED_URL_SCHEMES.has(scheme) ? null : scheme;\n}\n\n// Loose structural view of the mdast/MDX node shapes we inspect. Kept local\n// (not the full @types/mdast surface) so the public d.ts stays dependency-free.\ninterface MdxAstAttribute {\n type: string; // \"mdxJsxAttribute\" | \"mdxJsxExpressionAttribute\"\n name?: string;\n value?: unknown; // string | null | { type: \"mdxJsxAttributeValueExpression\" }\n}\n\ninterface MdxAstNode {\n type: string;\n children?: MdxAstNode[];\n url?: string;\n name?: string | null;\n attributes?: MdxAstAttribute[];\n position?: { start?: { line?: number } };\n depth?: number;\n}\n\nfunction nodeLine(node: MdxAstNode): number | undefined {\n return node.position?.start?.line;\n}\n\nfunction checkComponent(node: MdxAstNode, violations: BlogMdxViolation[]): void {\n const line = nodeLine(node);\n const name = node.name;\n\n if (!name) {\n violations.push({\n rule: \"unknown-component\",\n message: \"JSX fragments (<>…</>) are not allowed\",\n line,\n });\n return;\n }\n\n const spec = COMPONENT_SPECS[name];\n if (!spec) {\n violations.push({\n rule: \"unknown-component\",\n message: `<${name}> is not an allowed element (allowed: ${BLOG_MDX_ALLOWED_COMPONENTS.join(\", \")})`,\n line,\n });\n return;\n }\n\n const seen = new Set<string>();\n for (const attr of node.attributes ?? []) {\n if (attr.type !== \"mdxJsxAttribute\" || typeof attr.name !== \"string\") {\n // Spread attributes ({...props}) or anything unrecognized.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> has a non-literal attribute`,\n line,\n });\n continue;\n }\n if (!spec.attrs.includes(attr.name)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> does not accept attribute \"${attr.name}\"`,\n line,\n });\n continue;\n }\n if (typeof attr.value !== \"string\") {\n // null = bare attribute; object = {expression} value.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name} ${attr.name}> must be a quoted string literal`,\n line,\n });\n continue;\n }\n seen.add(attr.name);\n if (spec.urlAttrs.includes(attr.name)) {\n const scheme = forbiddenUrlScheme(attr.value);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `<${name} ${attr.name}> uses forbidden URL scheme \"${scheme}:\"`,\n line,\n });\n }\n }\n }\n\n for (const required of spec.attrs) {\n if (!seen.has(required)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> requires attribute \"${required}\"`,\n line,\n });\n }\n }\n\n if (!spec.allowChildren && (node.children?.length ?? 0) > 0) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> must be self-closing (no children)`,\n line,\n });\n }\n}\n\nexport interface BlogMdxValidateOptions {\n /**\n * Reject any content before the H1 (frontmatter, a stray metadata block,\n * prose) — only meaningful for a whole post. Off by default because the\n * cross-language conformance corpus exercises single components in\n * isolation, not full documents; the render boundary (BlogPostView, the\n * only production caller) always validates a whole post and passes `true`.\n */\n requireLeadingH1?: boolean;\n}\n\n/**\n * Validate a generated blog MDX document against the frozen content grammar:\n * standard markdown + exactly the components in\n * {@link BLOG_MDX_COMPONENT_GRAMMAR} (`<Callout>`, `<Cta href label/>`,\n * `<BlogImage id/>`, `<FaqItem question>`, `<KeyTakeaways>`). Rejects ESM,\n * JSX expressions, raw HTML, every other JSX/HTML element, non-literal\n * attributes, and non-http(s)/mailto/relative URLs. Pure and synchronous —\n * safe anywhere (RSC, node, edge).\n */\nexport function validateBlogMdx(\n mdx: string,\n options?: BlogMdxValidateOptions,\n): BlogMdxValidationResult {\n const violations: BlogMdxViolation[] = [];\n\n if (typeof mdx !== \"string\" || mdx.length > BLOG_MDX_MAX_SIZE) {\n return {\n ok: false,\n violations: [\n {\n rule: \"size-limit\",\n message: `Document exceeds the ${BLOG_MDX_MAX_SIZE}-character limit`,\n },\n ],\n };\n }\n\n let tree: MdxAstNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdxAstNode;\n } catch (error) {\n const line = (error as { line?: number }).line;\n return {\n ok: false,\n violations: [\n {\n rule: \"parse-error\",\n message: \"Document is not valid MDX\",\n ...(typeof line === \"number\" ? { line } : {}),\n },\n ],\n };\n }\n\n if (options?.requireLeadingH1) {\n const firstChild = tree.children?.[0];\n if (!firstChild || firstChild.type !== \"heading\" || firstChild.depth !== 1) {\n violations.push({\n rule: \"leading-h1\",\n message:\n \"Document must start with the H1 title (`# ...`) — no frontmatter or metadata block before it\",\n line: nodeLine(firstChild ?? tree),\n });\n }\n }\n\n // Iterative default-deny walk with explicit depth tracking (DoS guard —\n // never recurse on attacker-shaped input).\n let nodeCount = 0;\n const stack: Array<{ node: MdxAstNode; depth: number }> = [{ node: tree, depth: 0 }];\n\n while (stack.length > 0 && violations.length < MAX_VIOLATIONS) {\n const item = stack.pop();\n if (!item) break;\n const { node, depth } = item;\n\n nodeCount += 1;\n if (nodeCount > BLOG_MDX_MAX_NODES) {\n violations.push({ rule: \"size-limit\", message: \"Document has too many nodes\" });\n break;\n }\n if (depth > BLOG_MDX_MAX_DEPTH) {\n violations.push({ rule: \"depth-limit\", message: \"Document nests too deeply\" });\n break;\n }\n\n const forbidden = FORBIDDEN_NODE_TYPES[node.type];\n if (forbidden) {\n violations.push({ ...forbidden, line: nodeLine(node) });\n continue; // never descend into forbidden subtrees\n }\n\n if (node.type === \"mdxJsxFlowElement\" || node.type === \"mdxJsxTextElement\") {\n checkComponent(node, violations);\n // Descend regardless: Callout children are flow content that must obey\n // the same grammar, and even rejected subtrees should report every rule\n // they break (useful pipeline feedback; the doc is already invalid).\n } else if (!ALLOWED_NODE_TYPES.has(node.type)) {\n violations.push({\n rule: \"disallowed-node\",\n message: `Markdown construct \"${node.type}\" is not allowed`,\n line: nodeLine(node),\n });\n continue;\n }\n\n if (\n (node.type === \"link\" || node.type === \"image\" || node.type === \"definition\") &&\n typeof node.url === \"string\"\n ) {\n const scheme = forbiddenUrlScheme(node.url);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `${node.type} uses forbidden URL scheme \"${scheme}:\"`,\n line: nodeLine(node),\n });\n }\n }\n\n if (Array.isArray(node.children)) {\n for (const child of node.children) {\n stack.push({ node: child, depth: depth + 1 });\n }\n }\n }\n\n return violations.length === 0 ? { ok: true } : { ok: false, violations };\n}\n"],"mappings":"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","names":["z","remarkParse","remarkGfm","remarkMdx","GithubSlugger","import_remark_gfm","import_remark_mdx","import_remark_parse","import_unified","remarkParse","remarkGfm","remarkMdx"]}
1
+ {"version":3,"sources":["../src/blog.ts","../../tracking-core/src/capabilities.ts","../../tracking-core/src/resources/tracking-config-runtime.ts","../../tracking-core/src/resources/blog/schema.ts","../../tracking-core/src/resources/blog/theme.ts","../../tracking-core/src/resources/blog/client.ts","../../tracking-core/src/resources/blog/presets.ts","../../tracking-core/src/resources/blog/view/class-names.ts","../../tracking-core/src/resources/blog/seo.ts","../../tracking-core/src/resources/blog/toc.ts","../../tracking-core/src/resources/blog/validate-mdx.ts"],"sourcesContent":["// React-free blog SDK entry — `@aranova/tracking-next/blog`. Safe to import\n// from metadata/sitemap/route-handler code (and client bundles) without pulling\n// React or the MDX renderer: schemas + parsers, theme resolution, the CDN blog\n// client, and the SEO builders. The RSC renderer lives on `./blog-server`.\n// The surface is authored once in tracking-core (see blog-public.ts); this file\n// exists only as the per-package tsup build entry.\nexport * from \"../../tracking-core/src/blog-public\";\n","// Capability self-report registry.\n//\n// Every SDK construct registers itself here at construction time, and the\n// registered set rides out on `TrackingClientContext.capabilities`, which the\n// backend persists on the session row. The dashboard derives what a client site\n// ACTUALLY has installed from those sessions, rather than trusting an operator\n// checkbox — see contracts/tracking-capabilities.json, which pins this id list\n// against the backend enum and the dashboard type union.\n//\n// Browser-only by design: a server-side client (calendar bookings) emits no\n// session, and its artifact is better evidence than a self-report anyway.\n//\n// This module deliberately imports nothing: it is the one thing every other\n// resource can import without adding an edge that could close a cycle.\n\n/**\n * Every capability an install can report. Pinned by\n * `contracts/tracking-capabilities.json` — adding one here without a catalog\n * entry fails the drift test.\n */\nexport const TRACKING_CAPABILITIES = [\n \"ad_tags_google\",\n \"ad_tags_meta\",\n \"base_tracking\",\n \"blog_rendering\",\n \"calendar_read\",\n \"calendar_write\",\n \"consent_controls\",\n \"conversion_goals_auto\",\n \"conversion_goals_manual\",\n \"cross_site_linker\",\n \"cta_click_capture\",\n \"form_capture\",\n \"phone_click_capture\",\n \"phone_fields\",\n] as const;\n\nexport type TrackingCapability = (typeof TRACKING_CAPABILITIES)[number];\n\n/**\n * DOM attribute a server-rendered surface stamps to report itself.\n *\n * Blog rendering never calls the Aranova API — it reads the CDN directly — so\n * neither the ingest payload nor the request-header path can see it. The\n * renderer stamps this attribute instead and the browser context collector\n * picks it up on the next batch.\n */\nexport const CAPABILITY_DOM_ATTRIBUTE = \"data-aranova-capability\";\n\nconst registered = new Set<TrackingCapability>();\n\n/**\n * Record that a capability is present in this runtime.\n *\n * Idempotent and side-effect free — safe to call on every construction,\n * including React strict-mode double mounts.\n */\nexport function registerCapability(capability: TrackingCapability): void {\n registered.add(capability);\n}\n\n/**\n * Capabilities registered so far in this runtime, sorted for a stable wire\n * value (so the backend can hash the array to skip redundant writes).\n *\n * Also collects `data-aranova-capability` markers from the DOM, which is how\n * server-rendered surfaces that never call our API report themselves.\n */\nexport function getRegisteredCapabilities(): TrackingCapability[] {\n const all = new Set<TrackingCapability>(registered);\n for (const marker of readDomMarkers()) all.add(marker);\n return Array.from(all).sort();\n}\n\nfunction readDomMarkers(): TrackingCapability[] {\n if (typeof document === \"undefined\") return [];\n const known = new Set<string>(TRACKING_CAPABILITIES);\n const found: TrackingCapability[] = [];\n // Unknown values are dropped rather than forwarded: the backend rejects ids\n // outside its enum, and a stray attribute on a client's page must never be\n // able to fail an otherwise-valid ingest batch.\n for (const node of document.querySelectorAll(`[${CAPABILITY_DOM_ATTRIBUTE}]`)) {\n const value = node.getAttribute(CAPABILITY_DOM_ATTRIBUTE);\n if (value && known.has(value)) found.push(value as TrackingCapability);\n }\n return found;\n}\n\n/** Test-only: drop everything registered so far. */\nexport function resetRegisteredCapabilitiesForTests(): void {\n registered.clear();\n}\n","import { stripLinkParam } from \"../cross-site-link\";\nimport { applyDefaultConsentState, isValidGtagId, loadGtagScript } from \"../gtag\";\nimport {\n parseConversionConfig,\n type ConversionConfig,\n type ConversionGoal,\n} from \"./conversion-config\";\nimport { fireConversionWithConsent } from \"./conversion-firing\";\nimport { getAutomaticTransactionId } from \"./automatic-transaction\";\nimport { automaticThresholdMet } from \"./automatic-trigger\";\nimport { fromMinor } from \"./sales/money\";\nimport type { SupportedCurrency } from \"./sales/schema\";\n\n/**\n * Where a business's published tracking config lives.\n *\n * `businessId` + `environment` are the stable identity; the ORIGIN it is fetched\n * from is deployment-specific (production CDN vs. a local object store), which is\n * why `cdnBaseUrl` exists. Codegen bakes only the identity, so one committed\n * generated file works in every environment and only an env var changes.\n *\n * `cdnUrl` remains supported and WINS when present — every client repo generated\n * before `cdnBaseUrl` passes one, and those must keep working untouched.\n */\nexport interface TrackingConfigReference {\n /**\n * Fully-resolved object URL. Optional: omit it and pass `cdnBaseUrl` (or rely\n * on the production default) to have it composed from the identity below.\n */\n cdnUrl?: string;\n /**\n * Origin (optionally with a path prefix) the config object is served from, e.g.\n * `https://demos.aranova.io` in production or\n * `http://localhost:9100/aranova-demos` against a local MinIO. Ignored when\n * `cdnUrl` is set; blank or omitted falls back to {@link DEFAULT_CDN_BASE_URL}.\n * `tracking-cli gen` wires this to an env var so retargeting is config, not code.\n */\n cdnBaseUrl?: string;\n businessId: string;\n /**\n * The site whose config object to load (ENG-284). Selects the per-site\n * `tracking-config/v1/sites/` key; omitted (older generated files) resolves\n * the legacy per-business key, which is the primary site's config.\n */\n siteId?: string;\n environment: \"production\" | \"test\";\n}\n\n/** Production object-storage origin — the default when nothing overrides it. */\nexport const DEFAULT_CDN_BASE_URL = \"https://demos.aranova.io\";\n\n/**\n * Object key for a config, mirroring the backend's\n * `core/storage.py::tracking_config_key` (per-site) and its legacy per-business\n * key. The backend stays the authority: it\n * publishes at this key and returns the absolute URL, which is what the CLI bakes\n * as the legacy `cdnUrl`. This is the composition path for references that carry\n * identity instead of a URL — the `v1` prefix is the schema MAJOR and must move in\n * lockstep with the backend if it is ever cut to `v2`.\n */\nfunction trackingConfigKey(ref: TrackingConfigReference): string {\n if (ref.siteId) return `tracking-config/v1/sites/${ref.siteId}-${ref.environment}.json`;\n return `tracking-config/v1/${ref.businessId}-${ref.environment}.json`;\n}\n\n/**\n * The URL a reference resolves to. An explicit `cdnUrl` wins (back-compat);\n * otherwise compose from `cdnBaseUrl` (or the production default).\n *\n * A blank/whitespace `cdnBaseUrl` is treated as UNSET rather than composed: the\n * generated module reads it from an env var, and a var that is present-but-empty\n * (a stray `NEXT_PUBLIC_ARANOVA_CDN_BASE_URL=` line) would otherwise produce\n * `/tracking-config/...` — a same-origin request to the client's own site.\n */\nexport function resolveTrackingConfigUrl(ref: TrackingConfigReference): string {\n if (ref.cdnUrl) return ref.cdnUrl;\n const configured = ref.cdnBaseUrl?.trim();\n const base = (configured || DEFAULT_CDN_BASE_URL).replace(/\\/+$/, \"\");\n return `${base}/${trackingConfigKey(ref)}`;\n}\n\ntype RuntimeState = \"unconfirmed\" | \"active\" | \"tombstone\";\n\ninterface CachedEntry {\n etag: string | null;\n config: ConversionConfig;\n}\n\ninterface QueuedConversion {\n key: string;\n value?: number | null;\n currency?: string | null;\n transactionId?: string | null;\n}\n\nexport type TrackingConfigConversionOptions = Omit<QueuedConversion, \"key\">;\n\ninterface QueuedPageView {\n href: string;\n title: string | null;\n referrer: string | null;\n}\n\ninterface QueuedAutomaticEvent {\n eventType: string;\n metadata: Record<string, unknown>;\n transactionPath: string;\n transactionScope: string;\n}\n\nconst CACHE_PREFIX = \"_aranova_cfg_runtime_\";\nconst AUTHORITY_TTL_MS = 60_000;\n// Queues are bounded rather than cleared while authority is unconfirmed: the\n// config can appear at any time (a first publish, or R2 recovering), and the\n// most recent items are still worth firing when it does. Unbounded, a site whose\n// config 404s accumulates an entry per page view for the life of the tab.\nconst MAX_QUEUE_LENGTH = 50;\n// While unconfirmed, revalidation is spaced by a growing delay instead of riding\n// every queued item. Without it a missing config produces one no-cache request\n// per page view, forever. `focus` / `visibilitychange` still revalidate, so\n// recovery stays prompt once the object exists.\nconst RETRY_BASE_MS = 5_000;\nconst RETRY_MAX_MS = 5 * 60_000;\n\nconst runtimes = new Map<string, TrackingConfigRuntime>();\nconst configuredIds = new Set<string>();\nlet scriptLoad: Promise<void> | null = null;\nlet jsInitialized = false;\n\nfunction cacheKey(url: string): string {\n return `${CACHE_PREFIX}${url}`;\n}\n\n/** Append, dropping the oldest entry once the cap is reached. */\nfunction pushBounded<T>(queue: T[], item: T): void {\n queue.push(item);\n if (queue.length > MAX_QUEUE_LENGTH) queue.splice(0, queue.length - MAX_QUEUE_LENGTH);\n}\n\nfunction readCache(url: string): CachedEntry | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.sessionStorage.getItem(cacheKey(url));\n if (!raw) return null;\n const parsed = JSON.parse(raw) as { etag?: unknown; config?: unknown };\n const config = parseConversionConfig(parsed.config);\n if (!config) return null;\n return {\n etag: typeof parsed.etag === \"string\" ? parsed.etag : null,\n config,\n };\n } catch {\n return null;\n }\n}\n\nfunction writeCache(url: string, entry: CachedEntry): void {\n if (typeof window === \"undefined\") return;\n try {\n window.sessionStorage.setItem(cacheKey(url), JSON.stringify(entry));\n } catch {\n // sessionStorage unavailable — runtime still works, just without a conditional candidate\n }\n}\n\nfunction isTombstone(config: ConversionConfig): boolean {\n return config.google_tracking_state === \"disabled\" || Object.keys(config.gtag_ids).length === 0;\n}\n\nfunction validateConfig(config: ConversionConfig, ref: TrackingConfigReference): boolean {\n return (\n config.business_id === ref.businessId &&\n config.environment === ref.environment &&\n (config.google_tracking_state === \"active\" || config.google_tracking_state === \"disabled\")\n );\n}\n\nfunction pageViewSnapshot(): QueuedPageView | null {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return null;\n return {\n href: stripLinkParam(window.location.href),\n title: document.title || null,\n referrer: document.referrer || null,\n };\n}\n\nfunction ensureScript(gtagId: string): Promise<void> {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return Promise.resolve();\n applyDefaultConsentState();\n loadGtagScript(gtagId);\n if (scriptLoad) return scriptLoad;\n scriptLoad = new Promise((resolve) => {\n const script = document.querySelector<HTMLScriptElement>(\n 'script[data-aranova-tracking=\"aranova-gtag-loader\"]',\n );\n if (!script) {\n resolve();\n return;\n }\n if ((script as HTMLScriptElement & { dataset: DOMStringMap }).dataset.loaded === \"true\") {\n resolve();\n return;\n }\n window.setTimeout(resolve, 0);\n script.addEventListener(\n \"load\",\n () => {\n script.dataset.loaded = \"true\";\n resolve();\n },\n { once: true },\n );\n script.addEventListener(\"error\", () => resolve(), { once: true });\n });\n return scriptLoad;\n}\n\nexport class TrackingConfigRuntime {\n private readonly fetchImpl: typeof fetch;\n /** Resolved config URL — see the constructor for why it is computed once. */\n private readonly url: string;\n private current: ConversionConfig | null = null;\n private etag: string | null = null;\n private stateValue: RuntimeState = \"unconfirmed\";\n private confirmedAt = 0;\n private authorityGeneration = 0;\n private inFlight: Promise<void> | null = null;\n private flushInFlight: Promise<void> | null = null;\n private flushRequested = false;\n private retryTimer: number | null = null;\n private started = false;\n /** Consecutive failed authority attempts — drives the revalidate backoff. */\n private authorityFailures = 0;\n /** Epoch ms before which `ensureAuthority` must not issue another request. */\n private nextAuthorityAttemptAt = 0;\n private readonly conversionQueue: QueuedConversion[] = [];\n private readonly automaticQueue: QueuedAutomaticEvent[] = [];\n private readonly pageQueue: QueuedPageView[] = [];\n private readonly listeners = new Set<() => void>();\n\n constructor(\n readonly ref: TrackingConfigReference,\n fetchImpl: typeof fetch = globalThis.fetch,\n ) {\n // Native browser fetch requires Window/WorkerGlobalScope as its receiver.\n // Calling an unbound native fetch stored on `this` throws \"Illegal invocation\".\n this.fetchImpl = fetchImpl.bind(globalThis);\n // Resolved once: the cache key, the fetch target, and the write-back key must\n // all be the same string, or a reference that composes its URL would read a\n // cache entry it never writes.\n this.url = resolveTrackingConfigUrl(ref);\n const cached = readCache(this.url);\n this.current = cached?.config ?? null;\n this.etag = cached?.etag ?? null;\n this.start();\n }\n\n /** The URL this runtime actually fetches (composed or explicit). */\n configUrl(): string {\n return this.url;\n }\n\n /**\n * Explicitly start authority resolution and Google-tag bootstrap.\n *\n * Idempotent so framework effects can call it after hydration without\n * depending on constructor timing.\n */\n start(): void {\n if (this.started) {\n void this.flush();\n return;\n }\n this.started = true;\n void this.revalidate();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState === \"visible\") void this.revalidate();\n });\n window.addEventListener(\"focus\", () => void this.revalidate());\n }\n }\n\n state(): RuntimeState {\n return this.stateValue;\n }\n\n config(): ConversionConfig | null {\n return this.stateValue === \"active\" || this.stateValue === \"tombstone\" ? this.current : null;\n }\n\n __unsafeExpireAuthorityForTests(): void {\n this.confirmedAt = Number.NEGATIVE_INFINITY;\n this.nextAuthorityAttemptAt = 0;\n }\n\n /** Queue depths — asserted by tests to pin the bound. */\n __queueDepthsForTests(): { conversions: number; automatic: number; pages: number } {\n return {\n conversions: this.conversionQueue.length,\n automatic: this.automaticQueue.length,\n pages: this.pageQueue.length,\n };\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n async ensureAuthority(): Promise<boolean> {\n if (this.stateValue !== \"unconfirmed\" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS) {\n return true;\n }\n // Backoff applies only to this implicit path (driven by queued work). An\n // explicit `revalidate()` — the visibility/focus listeners, or a caller —\n // always goes to the network so recovery is never delayed by the timer.\n if (Date.now() < this.nextAuthorityAttemptAt) return false;\n await this.revalidateAuthority();\n return this.stateValue !== \"unconfirmed\" && Date.now() - this.confirmedAt < AUTHORITY_TTL_MS;\n }\n\n async revalidate(): Promise<void> {\n this.nextAuthorityAttemptAt = 0;\n await this.revalidateAuthority();\n await this.flush();\n }\n\n private async revalidateAuthority(): Promise<void> {\n if (typeof window === \"undefined\" || !this.fetchImpl) return;\n if (this.inFlight) return this.inFlight;\n this.inFlight = this.revalidateNow().finally(() => {\n this.inFlight = null;\n });\n return this.inFlight;\n }\n\n queuePageView(snapshot: QueuedPageView | null = pageViewSnapshot()): void {\n if (!snapshot) return;\n pushBounded(this.pageQueue, snapshot);\n void this.flush();\n }\n\n fireConversion(key: string, options?: TrackingConfigConversionOptions): void {\n pushBounded(this.conversionQueue, { key, ...options });\n void this.flush();\n }\n\n queueAutomaticEvent(\n eventType: string,\n metadata: Record<string, unknown>,\n transactionPath: string,\n transactionScope: string,\n ): void {\n pushBounded(this.automaticQueue, {\n eventType,\n metadata,\n transactionPath,\n transactionScope,\n });\n void this.flush();\n }\n\n listGoals(): ConversionGoal[] {\n return this.current?.goals ?? [];\n }\n\n /** Sibling hostnames for the linker — read from the cached object even while unconfirmed. */\n linkedHostnames(): readonly string[] {\n return this.current?.linked_hostnames ?? [];\n }\n\n private async revalidateNow(): Promise<void> {\n try {\n const headers: Record<string, string> = {};\n if (this.etag) headers[\"If-None-Match\"] = this.etag;\n const response = await this.fetchImpl(this.url, {\n method: \"GET\",\n headers,\n cache: \"no-cache\",\n });\n if (response.status === 304 && this.current && validateConfig(this.current, this.ref)) {\n this.confirm(this.current, this.etag);\n return;\n }\n if (!response.ok) {\n this.expireAuthority();\n return;\n }\n const next = parseConversionConfig(await response.json());\n if (!next || !validateConfig(next, this.ref)) {\n this.expireAuthority();\n return;\n }\n if (this.current && next.config_version < this.current.config_version) {\n this.expireAuthority();\n return;\n }\n this.confirm(next, response.headers.get(\"ETag\"));\n } catch {\n this.expireAuthority();\n }\n }\n\n private expireAuthority(): void {\n this.authorityGeneration += 1;\n if (this.stateValue !== \"unconfirmed\") this.stateValue = \"unconfirmed\";\n // Exponential, capped. Queued work keeps accumulating (bounded), so the\n // config appearing later still fires the most recent items.\n this.authorityFailures += 1;\n const delay = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** (this.authorityFailures - 1));\n this.nextAuthorityAttemptAt = Date.now() + delay;\n }\n\n private confirm(config: ConversionConfig, etag: string | null): void {\n this.authorityGeneration += 1;\n this.current = config;\n this.etag = etag;\n this.confirmedAt = Date.now();\n this.authorityFailures = 0;\n this.nextAuthorityAttemptAt = 0;\n this.stateValue = isTombstone(config) ? \"tombstone\" : \"active\";\n writeCache(this.url, { etag, config });\n if (this.stateValue === \"tombstone\") {\n if (this.retryTimer !== null) {\n window.clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n this.conversionQueue.length = 0;\n this.automaticQueue.length = 0;\n this.pageQueue.length = 0;\n }\n for (const listener of this.listeners) listener();\n }\n\n private async flush(): Promise<void> {\n if (this.flushInFlight) {\n this.flushRequested = true;\n return this.flushInFlight;\n }\n this.flushRequested = false;\n this.flushInFlight = this.flushNow().finally(() => {\n this.flushInFlight = null;\n if (this.flushRequested && this.stateValue === \"active\") void this.flush();\n });\n return this.flushInFlight;\n }\n\n private scheduleRetry(): void {\n if (this.retryTimer !== null || typeof window === \"undefined\") return;\n this.retryTimer = window.setTimeout(() => {\n this.retryTimer = null;\n void this.flush();\n }, 1000);\n }\n\n private async flushNow(): Promise<void> {\n if (!(await this.ensureAuthority())) return;\n if (this.stateValue !== \"active\" || !this.current) return;\n const config = this.current;\n const generation = this.authorityGeneration;\n const ids = Object.values(config.gtag_ids).filter(\n (id): id is string => typeof id === \"string\" && isValidGtagId(id),\n );\n if (ids.length === 0) return;\n await ensureScript(ids[0]!);\n if (\n this.authorityGeneration !== generation ||\n this.stateValue !== \"active\" ||\n this.current !== config\n ) {\n return;\n }\n const gtag = window.gtag;\n if (typeof gtag !== \"function\") return;\n try {\n if (!jsInitialized) {\n gtag(\"js\", new Date());\n jsInitialized = true;\n }\n for (const id of ids) {\n if (configuredIds.has(id)) continue;\n gtag(\"config\", id, { send_page_view: false });\n configuredIds.add(id);\n }\n } catch {\n this.scheduleRetry();\n return;\n }\n while (this.pageQueue.length > 0) {\n const page = this.pageQueue[0]!;\n try {\n gtag(\"event\", \"page_view\", {\n page_location: page.href,\n page_title: page.title ?? undefined,\n page_referrer: page.referrer ?? undefined,\n });\n this.pageQueue.shift();\n } catch {\n this.scheduleRetry();\n return;\n }\n }\n while (this.automaticQueue.length > 0) {\n const event = this.automaticQueue[0]!;\n for (const goal of config.goals) {\n if (goal.kind !== \"event\" || !goal.firing) continue;\n if (!automaticThresholdMet(goal, event.eventType, event.metadata)) continue;\n pushBounded(this.conversionQueue, {\n key: goal.key,\n transactionId: getAutomaticTransactionId(\n event.transactionScope,\n goal.key,\n event.transactionPath,\n ),\n });\n }\n this.automaticQueue.shift();\n }\n while (this.conversionQueue.length > 0) {\n const item = this.conversionQueue[0]!;\n const goal = config.goals.find((g) => g.key === item.key);\n const firing = goal?.firing;\n if (!firing) {\n this.conversionQueue.shift();\n continue;\n }\n const currency = item.currency ?? firing.currency ?? null;\n const value =\n item.value ??\n (firing.value_cents != null && currency\n ? fromMinor(firing.value_cents, currency as SupportedCurrency)\n : null);\n const outcome = fireConversionWithConsent({\n sendTo: firing.send_to,\n value,\n currency,\n transactionId: item.transactionId ?? null,\n });\n if (outcome === \"retryable\") {\n this.scheduleRetry();\n return;\n }\n this.conversionQueue.shift();\n }\n }\n}\n\nexport function getTrackingConfigRuntime(\n ref: TrackingConfigReference,\n fetchImpl?: typeof fetch,\n): TrackingConfigRuntime {\n // Keyed on the RESOLVED url: `{cdnUrl}` and an equivalent\n // `{cdnBaseUrl,businessId,environment}` name the same object and must share one\n // runtime, or each would keep its own authority state and gtag bootstrap.\n const key = `${resolveTrackingConfigUrl(ref)}|${ref.businessId}|${ref.environment}`;\n const existing = runtimes.get(key);\n if (existing) return existing;\n const runtime = new TrackingConfigRuntime(ref, fetchImpl ?? globalThis.fetch);\n runtimes.set(key, runtime);\n return runtime;\n}\n\nexport function resetTrackingConfigRuntimesForTests(): void {\n runtimes.clear();\n configuredIds.clear();\n scriptLoad = null;\n jsInitialized = false;\n}\n","import * as z from \"zod/mini\";\n\nimport { blogThemeOverrideSchema, nullishField } from \"./theme\";\n\n/**\n * Blog wire schemas — the client-side source of truth for the CDN-published\n * blog objects (ENG-31 Area C).\n *\n * {cdnBaseUrl}/blogs/v1/{namespace}/manifest.json (mutable)\n * {cdnBaseUrl}/blogs/v1/{namespace}/posts/{slug}/{content_hash}.json (immutable)\n *\n * Mirrored by the backend publisher and checked by the schema-drift contract\n * (the `resources` section of `events.schema.json`). Field names are FROZEN.\n *\n * These are TOLERANT readers (same philosophy as `conversion-config.ts`): the\n * backend may add fields freely — unknown fields are stripped, malformed\n * optional fields fall back to a safe default via `z.catch()`, and only a\n * structurally unusable object makes the parse helpers return `null`. They\n * never throw.\n */\n\n// The wire major, pinned to the version this reader understands. Within v1\n// the contract is additive-only (unknown fields are stripped), but a future\n// v2 object is by definition not readable under v1 assumptions — the literal\n// makes the whole-object parse fail, so `parseBlog*` returns `null` (the\n// tolerant \"unusable object\" path) instead of misreading it.\nconst schemaVersionSchema = z.literal(1);\n\nexport const blogHeroImageSchema = z.object({\n url: z.string(),\n alt: z.catch(z.string(), \"\"),\n width: z.int(),\n height: z.int(),\n});\n\nexport const blogManifestEntrySchema = z.object({\n slug: z.string(),\n title: z.string(),\n description: z.catch(z.string(), \"\"),\n content_hash: z.string(),\n content_key: z.string(),\n published_at: z.string(),\n updated_at: z.string(),\n // Estimated read time in whole minutes (computed at publish from the post\n // body). 0 when unknown; index cards render \"N min read\".\n reading_minutes: z.catch(z.int(), 0),\n // A malformed tags array / hero_image degrades gracefully instead of\n // dropping the whole entry.\n tags: z.catch(z.array(z.string()), []),\n hero_image: z.catch(z.nullable(blogHeroImageSchema), null),\n});\n\n// WIRE schema — what the backend publisher actually writes (emitted into\n// `events.schema.json` and drift-checked against the Pydantic `BlogManifest`).\n// The runtime reader below layers per-entry drop-tolerance on top of it: a\n// writer never emits `null` entries, so that tolerance is a READER refinement,\n// deliberately kept out of the wire contract.\nexport const blogManifestWireSchema = z.object({\n schema_version: schemaVersionSchema,\n namespace: z.string(),\n generated_at: z.string(),\n posts: z.catch(z.array(blogManifestEntrySchema), []),\n});\n\nexport const blogManifestSchema = z.extend(blogManifestWireSchema, {\n // A single malformed entry is dropped; the rest of the manifest survives.\n posts: z.catch(\n z.pipe(\n z.array(z.catch(z.nullable(blogManifestEntrySchema), null)),\n z.transform((entries) => entries.filter((e): e is BlogManifestEntry => e !== null)),\n ),\n [],\n ),\n});\n\nexport const blogImageSchema = z.object({\n id: z.string(),\n url: z.string(),\n alt: z.catch(z.string(), \"\"),\n caption: z.catch(z.nullable(z.string()), null),\n role: z.catch(z.enum([\"hero\", \"inline\"]), \"inline\"),\n width: z.int(),\n height: z.int(),\n mime: z.catch(z.string(), \"\"),\n});\n\nexport const blogSeoSchema = z.object({\n meta_title: z.catch(z.string(), \"\"),\n meta_description: z.catch(z.string(), \"\"),\n canonical_path: z.catch(z.string(), \"\"),\n og_image: z.catch(z.nullable(z.string()), null),\n});\n\n// WIRE schema — see `blogManifestWireSchema` for the wire/reader split.\nexport const blogPostContentWireSchema = z.object({\n schema_version: schemaVersionSchema,\n slug: z.string(),\n title: z.string(),\n description: z.catch(z.string(), \"\"),\n mdx: z.string(),\n images: z.catch(z.array(blogImageSchema), []),\n // Already schema-validated at publish time, but re-sanitized on read (and\n // again before rendering) — the override schema itself drops invalid values.\n // Nullish: the backend serializes an unset override as an explicit `null`\n // (Pydantic `model_dump()`); it folds into `undefined` for TS consumers.\n theme_overrides: z.catch(nullishField(blogThemeOverrideSchema), undefined),\n seo: z.catch(blogSeoSchema, {\n meta_title: \"\",\n meta_description: \"\",\n canonical_path: \"\",\n og_image: null,\n }),\n jsonld: z.catch(z.record(z.string(), z.unknown()), {}),\n published_at: z.string(),\n // The applied_at of the version this content was published from; `null`\n // when the backend has no separate modified date. Nullish on the wire\n // (Pydantic `model_dump()` emits explicit `null`s), folded to `null` for a\n // single TS sentinel; a malformed value degrades to `null` instead of\n // dropping the post.\n updated_at: z.catch(\n z.pipe(\n z.nullish(z.string()),\n z.transform((v) => v ?? null),\n ),\n null,\n ),\n version_number: z.catch(z.int(), 0),\n content_hash: z.string(),\n});\n\nexport const blogPostContentSchema = z.extend(blogPostContentWireSchema, {\n // A single malformed image record is dropped; the post survives.\n images: z.catch(\n z.pipe(\n z.array(z.catch(z.nullable(blogImageSchema), null)),\n z.transform((images) => images.filter((i): i is BlogImage => i !== null)),\n ),\n [],\n ),\n});\n\nexport type BlogHeroImage = z.infer<typeof blogHeroImageSchema>;\nexport type BlogManifestEntry = z.infer<typeof blogManifestEntrySchema>;\nexport type BlogManifest = z.infer<typeof blogManifestSchema>;\nexport type BlogImage = z.infer<typeof blogImageSchema>;\nexport type BlogSeo = z.infer<typeof blogSeoSchema>;\nexport type BlogPostContent = z.infer<typeof blogPostContentSchema>;\n\n/** Tolerant parse of a raw manifest object — `null` only when unusable. Never throws. */\nexport function parseBlogManifest(raw: unknown): BlogManifest | null {\n const parsed = z.safeParse(blogManifestSchema, raw);\n return parsed.success ? parsed.data : null;\n}\n\n/** Tolerant parse of a raw post-content object — `null` only when unusable. Never throws. */\nexport function parseBlogPostContent(raw: unknown): BlogPostContent | null {\n const parsed = z.safeParse(blogPostContentSchema, raw);\n return parsed.success ? parsed.data : null;\n}\n","import type { $ZodType } from \"zod/v4/core\";\nimport * as z from \"zod/mini\";\n\n/**\n * Blog theme — the typed styling contract for CDN-published blog posts.\n *\n * Three layers resolve into one fully-populated {@link BlogTheme}\n * (later layers win):\n *\n * 1. {@link BASE_BLOG_THEME} — neutral editorial defaults (always complete)\n * 2. the client-registered default theme (client code, per site)\n * 3. per-post `theme_overrides` from the content JSON (backend-authored)\n *\n * Because layers 2 and 3 cross a trust boundary (they end up in inline\n * `style` attributes and CSS custom properties), **sanitization lives in the\n * zod schemas themselves**: every value is validated against a strict grammar\n * (hex/functional colors, unit lengths, clamped weights, token-list font\n * families) and any invalid individual value is DROPPED via `.catch(undefined)`\n * — never passed through to CSS.\n */\n\n// ---------------------------------------------------------------------------\n// Responsive values (Chakra-style breakpoint maps)\n// ---------------------------------------------------------------------------\n\n/**\n * Fixed, SDK-owned breakpoints (mobile-first `min-width` px). Ordered smallest\n * to largest — CSS emission and any future style build must iterate in this\n * order so later (wider) breakpoints cascade over earlier ones.\n */\nexport const BLOG_BREAKPOINTS = [\"sm\", \"md\", \"lg\", \"xl\", \"2xl\", \"3xl\"] as const;\n\nexport type BlogBreakpoint = (typeof BLOG_BREAKPOINTS)[number];\n\nexport const BLOG_BREAKPOINT_MIN_WIDTHS_PX = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n \"2xl\": 1536,\n \"3xl\": 1920,\n} as const satisfies Record<BlogBreakpoint, number>;\n\n/** A breakpoint map: `base` is required, wider breakpoints are optional. */\nexport type BlogBreakpointMap<T = string> = { base: T } & {\n [K in BlogBreakpoint]?: T | undefined;\n};\n\n/**\n * A dimensional theme token: a plain scalar, or a breakpoint map that\n * resolves mobile-first (`base` below `sm`, each key from its min-width up).\n */\nexport type ResponsiveValue<T = string> = T | BlogBreakpointMap<T>;\n\n/**\n * Normalize a {@link ResponsiveValue} to its map form. The ONE place the\n * scalar-vs-map distinction is handled — downstream consumers (CSS var\n * emission, the style build) work off the map and never `typeof`-sniff.\n */\nexport function toBlogBreakpointMap<T extends string | number>(\n value: ResponsiveValue<T>,\n): BlogBreakpointMap<T> {\n return typeof value === \"object\" ? value : { base: value };\n}\n\n// ---------------------------------------------------------------------------\n// Value grammars (the sanitizer)\n// ---------------------------------------------------------------------------\n\nconst HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;\nconst FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla|oklch)\\([0-9a-zA-Z ,.%/-]*\\)$/;\nconst LENGTH = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)$/;\n// line-height additionally allows a unitless multiplier (\"1.7\").\nconst LINE_HEIGHT = /^-?\\d+(\\.\\d+)?(px|rem|em|%|ch|vw)?$/;\n// One font-family token: optionally quoted word characters/spaces/hyphens.\n// Rejects `url(`, `;`, `{`, `}`, `<`, `>` and every other CSS escape vector.\nconst FONT_FAMILY_TOKEN = /^['\"]?[a-zA-Z0-9 -]+['\"]?$/;\n// A single `var(--name)` reference — how next/font sites expose their real\n// (build-mangled) family names, e.g. `var(--font-sans)`. Exactly one balanced\n// paren pair and a custom-property name; no other characters can sneak in.\nconst FONT_FAMILY_VAR_TOKEN = /^var\\(--[a-zA-Z0-9-]+\\)$/;\n\nconst colorValueSchema = z.string().check(\n z.refine((v) => HEX_COLOR.test(v) || FUNCTIONAL_COLOR.test(v), {\n error: \"must be a #hex or rgb()/rgba()/hsl()/hsla()/oklch() color\",\n }),\n);\n\nconst lengthValueSchema = z\n .string()\n .check(z.regex(LENGTH, { error: \"must be a CSS length (px|rem|em|%|ch|vw)\" }));\n\nconst lineHeightValueSchema = z\n .string()\n .check(z.regex(LINE_HEIGHT, { error: \"must be a CSS length or unitless line-height\" }));\n\nconst fontWeightSchema = z.int().check(z.gte(100), z.lte(1000));\n\n// Text-alignment keyword (figcaption today). Scalar, non-responsive.\nconst alignValueSchema = z.enum([\"left\", \"center\", \"right\"]);\n\nconst fontFamilySchema = z.string().check(\n z.refine(\n (v) => {\n const tokens = v.split(\",\").map((t) => t.trim());\n return (\n tokens.length > 0 &&\n tokens.every((t) => FONT_FAMILY_TOKEN.test(t) || FONT_FAMILY_VAR_TOKEN.test(t))\n );\n },\n { error: \"must be a comma-separated list of font-family or var(--custom-property) tokens\" },\n ),\n);\n\n// The wire truth for every optional field is Pydantic's `model_dump()`, which\n// serializes `Type | None = None` fields as explicit `null`s (never omits\n// them). So every optional field here must be NULL-tolerant, not just\n// absent-tolerant: `.nullish()` + a transform that folds `null` back into\n// `undefined` so downstream TS consumers keep a single \"absent\" sentinel.\n// `z.toJSONSchema(..., { io: \"input\" })` emits the transform's INPUT schema\n// (nullable), which is exactly what the backend drift test compares against\n// the Pydantic mirror.\n//\n// The return type is inferred rather than declared: v4 builds this as a pipe,\n// and an explicit alias both names an implementation detail and loses the\n// key-is-optional signal the object inference depends on.\nexport function nullishField<S extends $ZodType>(schema: S) {\n // The outer `z.optional` is what keeps the OUTPUT key optional: Zod 4 types a\n // transform result as required-but-possibly-undefined, where Zod 3 made the\n // key itself optional. The input schema is unchanged (already nullish), so\n // the JSON Schema snapshot is unaffected.\n return z.optional(\n z.pipe(\n z.nullish(schema),\n z.transform((v) => v ?? undefined),\n ),\n );\n}\n\ntype NullishField<S extends $ZodType> = ReturnType<typeof nullishField<S>>;\n\n// A scalar CSS value schema (the tokens' grammars are all string-valued).\ntype ScalarValueSchema = $ZodType<string, string>;\n\ntype BreakpointMapSchema = $ZodType<\n BlogBreakpointMap<string>,\n { base: string } & { [K in BlogBreakpoint]?: string | null | undefined }\n>;\n\n// Breakpoint map for one token. `base` is required and validated with the\n// token's own grammar; each breakpoint key is optional, null-tolerant (wire\n// nulls) and individually DROPPED when invalid (`.catch(undefined)`) — the\n// same per-value sanitization philosophy as the rest of this file. Unknown\n// keys are stripped by zod's default object behavior.\nfunction breakpointMapSchema(valueSchema: ScalarValueSchema): BreakpointMapSchema {\n return z.object({\n base: valueSchema,\n sm: z.catch(nullishField(valueSchema), undefined),\n md: z.catch(nullishField(valueSchema), undefined),\n lg: z.catch(nullishField(valueSchema), undefined),\n xl: z.catch(nullishField(valueSchema), undefined),\n \"2xl\": z.catch(nullishField(valueSchema), undefined),\n \"3xl\": z.catch(nullishField(valueSchema), undefined),\n }) as unknown as BreakpointMapSchema;\n}\n\ntype ResponsiveSchema = z.ZodMiniUnion<[ScalarValueSchema, BreakpointMapSchema]>;\n\n/**\n * Schema factory for a responsive token: the token's scalar grammar OR a\n * {@link BlogBreakpointMap} of it. A map missing/failing `base` fails the\n * whole union (override parsing then drops the entire field via the\n * field-level `.catch`); an invalid non-base breakpoint value is dropped\n * per-value while the rest of the map survives.\n */\nexport function responsive(valueSchema: ScalarValueSchema): ResponsiveSchema {\n return z.union([valueSchema, breakpointMapSchema(valueSchema)]);\n}\n\n// ---------------------------------------------------------------------------\n// Full theme schemas (strict — every field required and valid)\n// ---------------------------------------------------------------------------\n\n// Flat color shape (every key → the plain color grammar). The full schema,\n// the deep-partial dark palette, and the override schema all derive from this\n// ONE shape so the optional `heading` never gets double-wrapped in nullish\n// layers (which would emit nested anyOfs in the JSON Schema snapshot).\nconst blogThemeColorsShape = {\n background: colorValueSchema,\n surface: colorValueSchema,\n text: colorValueSchema,\n text_muted: colorValueSchema,\n primary: colorValueSchema,\n primary_foreground: colorValueSchema,\n link: colorValueSchema,\n border: colorValueSchema,\n code_background: colorValueSchema,\n heading: colorValueSchema,\n} as const;\n\nexport const blogThemeColorsSchema = z.object({\n ...blogThemeColorsShape,\n // The ONE optional color in a resolved theme: when unset, headings fall back\n // to `text` in the stylesheet (`--_heading-color`), so `blogThemeToCssVars`\n // only emits `--ara-blog-color-heading` when a theme actually sets it.\n heading: nullishField(colorValueSchema),\n});\n\nexport const blogThemeTypographySchema = z.object({\n heading_family: fontFamilySchema,\n body_family: fontFamilySchema,\n heading_weight: fontWeightSchema,\n body_weight: fontWeightSchema,\n h1_size: responsive(lengthValueSchema),\n h2_size: responsive(lengthValueSchema),\n h3_size: responsive(lengthValueSchema),\n h4_size: responsive(lengthValueSchema),\n body_size: responsive(lengthValueSchema),\n body_line_height: responsive(lineHeightValueSchema),\n small_size: responsive(lengthValueSchema),\n // Scalar (non-responsive) length; the grammar allows negative em values.\n // h1/h2 consume it directly, h3/h4 at half strength (calc in the base CSS).\n heading_letter_spacing: lengthValueSchema,\n // Figure caption text alignment (left/center/right). Site-owner override;\n // default center in the base CSS if unset.\n caption_align: alignValueSchema,\n});\n\nexport const blogThemeSpacingSchema = z.object({\n content_max_width: responsive(lengthValueSchema),\n block_gap: responsive(lengthValueSchema),\n section_gap: responsive(lengthValueSchema),\n});\n\n// Deep-partial companion to `blogThemeColorsSchema` for the optional dark\n// palette: each color is individually optional AND null-tolerant (see\n// `nullishField`), mirroring the Pydantic side's `str | None = None` fields.\nconst blogThemeColorsPartialSchema = z.object(\n Object.fromEntries(\n Object.keys(blogThemeColorsShape).map((key) => [key, nullishField(colorValueSchema)]),\n ) as { [K in keyof typeof blogThemeColorsShape]: NullishField<typeof colorValueSchema> },\n);\n\nexport const blogThemeSchema = z.object({\n colors: blogThemeColorsSchema,\n colors_dark: nullishField(blogThemeColorsPartialSchema),\n typography: blogThemeTypographySchema,\n spacing: blogThemeSpacingSchema,\n radius: responsive(lengthValueSchema),\n});\n\nexport type BlogThemeColors = z.infer<typeof blogThemeColorsSchema>;\nexport type BlogThemeTypography = z.infer<typeof blogThemeTypographySchema>;\nexport type BlogThemeSpacing = z.infer<typeof blogThemeSpacingSchema>;\nexport type BlogTheme = z.infer<typeof blogThemeSchema>;\n\n// ---------------------------------------------------------------------------\n// Override schema (deep-partial + per-field drop-on-invalid)\n// ---------------------------------------------------------------------------\n\n// Each field is nullish (`null` on the wire — the backend's `model_dump()`\n// emits explicit nulls for unset fields — folds into `undefined`) +\n// `.catch(undefined)`: an invalid individual value is dropped (falls back to\n// the layer below at resolve time) without failing the group or the whole\n// override. A non-object group likewise drops to undefined.\n// `Record<string, z.ZodType>` rather than `z.ZodRawShape`: in Zod 4 the\n// `$ZodType` is the shared base both the classic and the Mini builders\n// implement.\nfunction dropInvalidFields<T extends Record<string, $ZodType>>(\n shape: T,\n): { [K in keyof T]: z.ZodMiniCatch<NullishField<T[K]>> } {\n const out: Record<string, $ZodType> = {};\n for (const [key, schema] of Object.entries(shape)) {\n out[key] = z.catch(nullTolerant(schema), undefined);\n }\n return out as { [K in keyof T]: z.ZodMiniCatch<NullishField<T[K]>> };\n}\n\n// Null-tolerant wrapper that keeps responsive unions FLAT on the wire.\n// Wrapping a union in `.nullable()` would emit nested `anyOf`s\n// (`anyOf: [anyOf: [scalar, map], null]`) in the JSON Schema snapshot, while\n// the Pydantic mirror's `str | Map | None` emits a flat three-branch `anyOf`\n// — so for unions we splice `z.null()` into the union itself instead.\nfunction nullTolerant(schema: $ZodType): $ZodType {\n if (schema instanceof z.ZodMiniUnion) {\n const options = (schema as z.ZodMiniUnion<[$ZodType, $ZodType, ...$ZodType[]]>).def.options;\n return z.pipe(\n z.optional(z.union([...options, z.null()])),\n z.transform((v: unknown) => v ?? undefined),\n );\n }\n return nullishField(schema);\n}\n\n// Built from the flat shape (NOT blogThemeColorsSchema.shape) so the optional\n// `heading` gets exactly one nullish layer, like every other override field.\nconst blogThemeColorsOverrideSchema = z.object(dropInvalidFields(blogThemeColorsShape));\nconst blogThemeTypographyOverrideSchema = z.object(\n dropInvalidFields(blogThemeTypographySchema.shape),\n);\nconst blogThemeSpacingOverrideSchema = z.object(dropInvalidFields(blogThemeSpacingSchema.shape));\n\nexport const blogThemeOverrideSchema = z.object(\n dropInvalidFields({\n colors: blogThemeColorsOverrideSchema,\n colors_dark: blogThemeColorsOverrideSchema,\n typography: blogThemeTypographyOverrideSchema,\n spacing: blogThemeSpacingOverrideSchema,\n radius: responsive(lengthValueSchema),\n }),\n);\n\nexport type BlogThemeOverride = z.infer<typeof blogThemeOverrideSchema>;\n\n// ---------------------------------------------------------------------------\n// Base theme — neutral editorial defaults\n// ---------------------------------------------------------------------------\n\nexport const BASE_BLOG_THEME: BlogTheme = {\n colors: {\n background: \"#ffffff\",\n surface: \"#f5f5f5\",\n text: \"#171717\",\n text_muted: \"#6b6b6b\",\n primary: \"#171717\",\n primary_foreground: \"#ffffff\",\n link: \"#1d4ed8\",\n border: \"#e5e5e5\",\n code_background: \"#f5f5f5\",\n },\n typography: {\n heading_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n body_family: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n // Responsive defaults: comfortable mobile sizes, larger from `md` up.\n // This replaces the hardcoded mobile media query the base CSS used to need.\n h1_size: { base: \"1.9rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n small_size: \"0.875rem\",\n heading_letter_spacing: \"-0.02em\",\n caption_align: \"center\",\n },\n spacing: {\n content_max_width: \"42rem\",\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"8px\",\n};\n\n// ---------------------------------------------------------------------------\n// Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitize an untrusted theme(-override) object. Invalid individual values are\n * dropped; total garbage (non-object, arrays, …) yields `{}`. Never throws.\n */\nexport function sanitizeBlogTheme(raw: unknown): BlogThemeOverride {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const parsed = blogThemeOverrideSchema.safeParse(raw);\n return parsed.success ? parsed.data : {};\n}\n\nfunction mergeGroup<T extends Record<string, unknown>>(base: T, over: Partial<T> | undefined): T {\n if (!over) return base;\n const out = { ...base };\n for (const [key, value] of Object.entries(over)) {\n if (value !== undefined) (out as Record<string, unknown>)[key] = value;\n }\n return out;\n}\n\n/**\n * Deep-merge override layers onto {@link BASE_BLOG_THEME}. Later layers win;\n * `null`/`undefined` layers and `undefined` fields are skipped, so the result\n * is always a fully-populated {@link BlogTheme}.\n *\n * Responsive tokens merge at TOKEN granularity: a later layer's value —\n * scalar or breakpoint map — REPLACES the whole token. There is deliberately\n * no per-breakpoint merge (a layer overriding `h1_size: \"2rem\"` on top of a\n * base map must not inherit the base's `md` entry).\n */\nexport function resolveBlogTheme(\n ...layers: Array<BlogThemeOverride | null | undefined>\n): BlogTheme {\n let resolved: BlogTheme = {\n colors: { ...BASE_BLOG_THEME.colors },\n colors_dark: BASE_BLOG_THEME.colors_dark ? { ...BASE_BLOG_THEME.colors_dark } : undefined,\n typography: { ...BASE_BLOG_THEME.typography },\n spacing: { ...BASE_BLOG_THEME.spacing },\n radius: BASE_BLOG_THEME.radius,\n };\n for (const layer of layers) {\n if (!layer) continue;\n resolved = {\n colors: mergeGroup(resolved.colors, layer.colors),\n colors_dark:\n layer.colors_dark || resolved.colors_dark\n ? mergeGroup(resolved.colors_dark ?? {}, layer.colors_dark)\n : undefined,\n typography: mergeGroup(resolved.typography, layer.typography),\n spacing: mergeGroup(resolved.spacing, layer.spacing),\n radius: layer.radius ?? resolved.radius,\n };\n }\n return resolved;\n}\n\n// (split/join instead of String#replaceAll — the package lib target is ES2020.)\nfunction kebab(key: string): string {\n return key.split(\"_\").join(\"-\");\n}\n\n// Emit one (possibly responsive) token: the base var carries the scalar (or\n// the map's `base`), and each PRESENT breakpoint key adds a `-<bp>` suffixed\n// var (e.g. `--ara-blog-type-h1-size-md`). Absent breakpoints emit nothing —\n// the stylesheet's media queries fall back to the base var.\nfunction emitResponsiveVar(\n vars: Record<string, string>,\n name: string,\n value: ResponsiveValue<string | number>,\n): void {\n const map = toBlogBreakpointMap(value);\n vars[name] = String(map.base);\n for (const bp of BLOG_BREAKPOINTS) {\n const bpValue = map[bp];\n if (bpValue !== undefined) vars[`${name}-${bp}`] = String(bpValue);\n }\n}\n\n/**\n * Flatten a resolved theme into `--ara-blog-*` CSS custom properties for an\n * inline `style` attribute. When `colors_dark` is present, color values are\n * emitted as `light-dark(lightVal, darkVal)` and `color-scheme: light dark`\n * is included so the browser picks the active side. Responsive tokens emit\n * their base var plus `-sm`/`-md`/… suffixed vars for present breakpoints.\n */\nexport function blogThemeToCssVars(theme: BlogTheme): Record<string, string> {\n const vars: Record<string, string> = {};\n const dark = theme.colors_dark;\n const hasDark = !!dark && Object.values(dark).some((v) => v !== undefined);\n\n // Union of light + dark keys: an optional color (only `heading` today) set\n // ONLY on the dark side still needs its var emitted.\n const colorKeys = new Set([...Object.keys(theme.colors), ...(dark ? Object.keys(dark) : [])]);\n for (const key of colorKeys) {\n const light = theme.colors[key as keyof BlogThemeColors];\n const darkValue = hasDark ? dark?.[key as keyof BlogThemeColors] : undefined;\n // Optional colors (only `heading` today) emit nothing when unset — the\n // stylesheet's fallback (`--_heading-color` → text) takes over. A theme\n // that sets ONLY the dark side still gets a var: the light side falls\n // back to `text`, which is what the stylesheet would use anyway.\n const lightValue = light ?? (darkValue !== undefined ? theme.colors.text : undefined);\n if (lightValue === undefined) continue;\n vars[`--ara-blog-color-${kebab(key)}`] = darkValue\n ? `light-dark(${lightValue}, ${darkValue})`\n : lightValue;\n }\n // camelCase: these vars land in a React `style` prop, which rejects kebab\n // names for real (non-custom) CSS properties.\n if (hasDark) vars.colorScheme = \"light dark\";\n\n for (const [key, value] of Object.entries(theme.typography)) {\n emitResponsiveVar(vars, `--ara-blog-type-${kebab(key)}`, value);\n }\n for (const [key, value] of Object.entries(theme.spacing)) {\n emitResponsiveVar(vars, `--ara-blog-space-${kebab(key)}`, value);\n }\n emitResponsiveVar(vars, \"--ara-blog-radius\", theme.radius);\n return vars;\n}\n","import { registerCapability } from \"../../capabilities\";\nimport { DEFAULT_CDN_BASE_URL } from \"../tracking-config-runtime\";\nimport {\n parseBlogManifest,\n parseBlogPostContent,\n type BlogManifest,\n type BlogManifestEntry,\n type BlogPostContent,\n} from \"./schema\";\n\n/**\n * Blog CDN client — read-only fetchers over the published blog objects.\n *\n * Designed for Next.js server components but framework-free: the Next-specific\n * `next.revalidate` fetch extension is expressed as a local structural type\n * ({@link NextFetchInit}) so we never import `next` types and a plain\n * `fetch` (Node, tests) simply ignores the extra property.\n *\n * Error policy: reads are best-effort. Network failures, non-2xx responses,\n * and unparseable bodies all resolve to `null` / `[]` — a blog page must\n * degrade, never crash the host site.\n */\n\n/** `RequestInit` + the Next.js data-cache extension (structural — no `next` import). */\nexport type NextFetchInit = RequestInit & {\n next?: { revalidate?: number | false; tags?: string[] };\n};\n\nexport interface BlogClientConfig {\n /**\n * Reserved for future authenticated reads (draft previews fetched from the\n * API instead of the public CDN). Published content is world-readable, so\n * the key is currently NOT sent — but it is part of the config contract so\n * client sites don't need a breaking config change when draft reads ship.\n */\n apiKey: string;\n /**\n * CDN origin, e.g. `https://cdn.aranova.io` (trailing slash tolerated).\n * Blank or whitespace falls back to {@link DEFAULT_CDN_BASE_URL} — the same\n * contract the tracking-config resolver has had since PR #180. The blog\n * client originally shipped without the fallback, so an unset env var made\n * the manifest URL relative; server-side fetch rejected it and the fail-soft\n * error policy rendered a published blog as permanently empty.\n */\n cdnBaseUrl: string;\n /** Opaque per-business namespace under `blogs/v1/`. */\n namespace: string;\n /** Manifest ISR revalidate window (seconds). Default 300. */\n manifestRevalidateSeconds?: number;\n /** Injectable for tests / non-global-fetch runtimes. */\n fetchImpl?: typeof fetch;\n}\n\nexport interface BlogClient {\n /** The mutable manifest (index of published posts), or `null` on any miss. */\n getManifest(): Promise<BlogManifest | null>;\n /** Manifest entries sorted `published_at` desc. `[]` on any miss. */\n listPosts(): Promise<BlogManifestEntry[]>;\n /** Full post content for a slug, or `null` when unknown/unfetchable. */\n getPost(slug: string): Promise<BlogPostContent | null>;\n /** `generateStaticParams()`-shaped slug list. */\n getStaticParams(): Promise<Array<{ slug: string }>>;\n}\n\nexport function createBlogClient(config: BlogClientConfig): BlogClient {\n registerCapability(\"blog_rendering\");\n // Blank-as-unset, mirroring `resolveTrackingConfigUrl`: a present-but-empty\n // env var must not produce a relative (same-origin) manifest URL.\n const base = (config.cdnBaseUrl.trim() || DEFAULT_CDN_BASE_URL).replace(/\\/+$/, \"\");\n const doFetch: typeof fetch = (...args) => (config.fetchImpl ?? fetch)(...args);\n\n async function fetchJson(url: string, init: NextFetchInit): Promise<unknown | null> {\n try {\n const response = await doFetch(url, init);\n if (!response.ok) return null;\n return (await response.json()) as unknown;\n } catch {\n return null;\n }\n }\n\n async function getManifest(): Promise<BlogManifest | null> {\n // MUTABLE object (Cache-Control max-age=60 at the CDN) — cached through the\n // Next.js data cache with a revalidate window instead of force-cache.\n const init = {\n next: { revalidate: config.manifestRevalidateSeconds ?? 300 },\n } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/blogs/v1/${config.namespace}/manifest.json`, init);\n return raw === null ? null : parseBlogManifest(raw);\n }\n\n async function listPosts(): Promise<BlogManifestEntry[]> {\n const manifest = await getManifest();\n if (!manifest) return [];\n return [...manifest.posts].sort((a, b) => b.published_at.localeCompare(a.published_at));\n }\n\n async function getPost(slug: string): Promise<BlogPostContent | null> {\n const manifest = await getManifest();\n const entry = manifest?.posts.find((post) => post.slug === slug);\n if (!entry) return null;\n // IMMUTABLE object (hash-addressed, max-age=1y) — cache forever; a content\n // change publishes a new hash, which the manifest points at.\n const init = { cache: \"force-cache\" } satisfies NextFetchInit;\n const raw = await fetchJson(`${base}/${entry.content_key.replace(/^\\/+/, \"\")}`, init);\n return raw === null ? null : parseBlogPostContent(raw);\n }\n\n async function getStaticParams(): Promise<Array<{ slug: string }>> {\n const posts = await listPosts();\n return posts.map((post) => ({ slug: post.slug }));\n }\n\n return { getManifest, listPosts, getPost, getStaticParams };\n}\n","// Curated font/type-scale presets for the blog theme — React-free, part of\n// the `@aranova/tracking-next/blog` surface (re-exported via blog-public.ts).\n//\n// Each preset is a plain `BlogThemeOverride`: a font pairing with REAL\n// web-safe fallback stacks, a responsive type scale, weights, letter-spacing,\n// spacing, and radius. Colors are deliberately (almost entirely) UNSET so the\n// client site's own palette wins — the only exception is a subtle\n// `code_background` where the code surface is part of the preset's identity.\n//\n// Contract (tested in __tests__/presets.test.ts): every preset passes\n// `sanitizeBlogTheme(preset)` UNCHANGED — a preset must never rely on values\n// the sanitizer would drop.\n\nimport type { BlogThemeOverride } from \"./theme\";\n\nexport type BlogThemePresetName = \"editorial\" | \"modern\" | \"warm\" | \"technical\";\n\n/**\n * Named theme presets. Layer one under your site palette:\n *\n * ```ts\n * const theme = resolveBlogTheme(\n * createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\", // next/font variable\n * body: \"var(--font-sans)\",\n * }),\n * { colors: { primary: \"#0f766e\", link: \"#0f766e\" } },\n * );\n * ```\n */\nexport const BLOG_THEME_PRESETS: Record<BlogThemePresetName, BlogThemeOverride> = {\n /** Serif display headings over a humanist sans body — long-form magazine feel. */\n editorial: {\n typography: {\n heading_family: \"'Source Serif 4', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif\",\n body_family: \"'Source Sans 3', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"2rem\", md: \"2.5rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.875rem\" },\n h3_size: \"1.3rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.015em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"6px\",\n },\n /** Geometric sans pairing (Inter-first) — tight, contemporary product voice. */\n modern: {\n typography: {\n heading_family: \"'Inter', 'SF Pro Display', 'Segoe UI', Roboto, sans-serif\",\n body_family: \"'Inter', 'SF Pro Text', 'Segoe UI', Roboto, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"2.1rem\", md: \"2.75rem\" },\n h2_size: { base: \"1.5rem\", md: \"1.75rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1rem\",\n body_line_height: \"1.7\",\n heading_letter_spacing: \"-0.03em\",\n },\n spacing: {\n block_gap: \"1.25rem\",\n section_gap: \"3rem\",\n },\n radius: \"12px\",\n },\n /** Rounded-friendly pairing with a softer scale — approachable, human tone. */\n warm: {\n typography: {\n heading_family: \"'Nunito', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n body_family: \"'Nunito Sans', 'Avenir Next', 'Trebuchet MS', Verdana, sans-serif\",\n heading_weight: 700,\n body_weight: 400,\n h1_size: { base: \"1.9rem\", md: \"2.35rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.7rem\" },\n h3_size: \"1.25rem\",\n h4_size: \"1.125rem\",\n body_size: \"1.0625rem\",\n body_line_height: \"1.75\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.35rem\",\n section_gap: \"3.25rem\",\n },\n radius: \"16px\",\n },\n /** Workmanlike sans with a prominent code surface — docs/engineering feel. */\n technical: {\n colors: {\n // The one preset that ships a color: a cool code surface is part of the\n // \"technical\" identity. Everything else stays on the site palette.\n code_background: \"#eef1f5\",\n },\n colors_dark: {\n code_background: \"#1b1f27\",\n },\n typography: {\n heading_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n body_family: \"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif\",\n heading_weight: 600,\n body_weight: 400,\n h1_size: { base: \"1.85rem\", md: \"2.25rem\" },\n h2_size: { base: \"1.45rem\", md: \"1.65rem\" },\n h3_size: \"1.2rem\",\n h4_size: \"1.0625rem\",\n body_size: \"1rem\",\n body_line_height: \"1.65\",\n heading_letter_spacing: \"-0.01em\",\n },\n spacing: {\n block_gap: \"1.15rem\",\n section_gap: \"2.75rem\",\n },\n radius: \"6px\",\n },\n};\n\n/** Real font sources for a preset: next/font CSS variables (or literal family names). */\nexport interface BlogFontSources {\n /** e.g. `\"var(--font-serif)\"` from `next/font`'s `variable` option. */\n heading?: string;\n /** e.g. `\"var(--font-sans)\"`. */\n body?: string;\n}\n\n// Prepend a caller-supplied family (usually a next/font `var(--…)`) onto the\n// preset's fallback stack. The caller value may itself be a comma-separated\n// stack; either way the preset's web-safe fallbacks ride behind it.\nfunction prependFamily(front: string | undefined, stack: string | undefined): string | undefined {\n if (!front) return stack;\n return stack ? `${front}, ${stack}` : front;\n}\n\n/**\n * Wire a site's real fonts into a preset: returns a NEW override (no\n * mutation) whose heading/body families are `\"<font>, <preset fallback\n * stack>\"` — pass your `next/font` variable names and the preset's web-safe\n * fallbacks keep covering the pre-hydration/fallback paint.\n *\n * ```ts\n * const serif = Source_Serif_4({ subsets: [\"latin\"], variable: \"--font-serif\" });\n * const sans = Inter({ subsets: [\"latin\"], variable: \"--font-sans\" });\n * const override = createBlogFontTheme(BLOG_THEME_PRESETS.editorial, {\n * heading: \"var(--font-serif)\",\n * body: \"var(--font-sans)\",\n * });\n * ```\n */\nexport function createBlogFontTheme(\n preset: BlogThemeOverride,\n fonts: BlogFontSources,\n): BlogThemeOverride {\n const heading_family = prependFamily(fonts.heading, preset.typography?.heading_family);\n const body_family = prependFamily(fonts.body, preset.typography?.body_family);\n return {\n ...preset,\n typography: {\n ...preset.typography,\n ...(heading_family !== undefined ? { heading_family } : {}),\n ...(body_family !== undefined ? { body_family } : {}),\n },\n };\n}\n","// Client-side className overrides for the blog view — the escape hatch for\n// sites that want to hook a Tailwind/utility class onto a specific surface\n// without replacing the whole component (see `components` for that) or\n// writing global CSS against the fixed `.aranova-blog-*` selectors (still the\n// simplest option and requires no API — these classes are stable and public).\n//\n// `joinClassNames` is a plain append, not a Tailwind-merge: every base class\n// here is an SDK-internal selector (`aranova-blog-callout`, …), never a\n// utility class, so there is nothing for an override to conflict with —\n// dropping a `tailwind-merge` dependency into a framework-free package for a\n// non-conflict is unnecessary weight.\nexport function joinClassNames(base: string, extra?: string): string {\n return extra ? `${base} ${extra}` : base;\n}\n\n/**\n * One optional override per styleable surface. Every key merges onto its\n * fixed base class (never replaces it), so the `.aranova-blog-*` selector\n * still applies — this is additive, not a restyle mechanism.\n */\nexport interface BlogClassNameOverrides {\n /** The `<article>` root. */\n article?: string;\n /** The content/aside grid wrapper. */\n layout?: string;\n /** The content column. */\n content?: string;\n /** The aside rail (table of contents). */\n aside?: string;\n /** `<Callout>` MDX component. */\n callout?: string;\n /** `<Cta>` MDX component. */\n cta?: string;\n /** `<BlogImage>` MDX component's `<figure>` wrapper. */\n figure?: string;\n /** `<FaqItem>` MDX component's `<details>` wrapper. */\n faqItem?: string;\n /** `<FaqItem>`'s `<summary>`. */\n faqQuestion?: string;\n /** `<FaqItem>`'s answer `<div>`. */\n faqAnswer?: string;\n /** `<KeyTakeaways>` MDX component. */\n keyTakeaways?: string;\n /** The `<table>` overflow wrapper `<div>`. */\n tableWrap?: string;\n /** `BlogPostMeta` byline row. */\n meta?: string;\n /** `BlogBreadcrumbs` nav. */\n breadcrumbs?: string;\n /** `BlogTocShell` / the animated `TocPanel` island. */\n toc?: string;\n /** The `ReadingProgress` island. */\n progress?: string;\n}\n","import type { BlogManifestEntry, BlogPostContent } from \"./schema\";\n\n/**\n * SEO builders — React-free helpers that turn post content into Next.js\n * `Metadata`-shaped objects, BlogPosting JSON-LD, and sitemap entries.\n *\n * Deliberately does NOT import `next` types: {@link BlogPostMetadata} is a\n * local structural interface whose shape is spread-compatible with Next's\n * `Metadata`, so the helpers stay usable from any framework (and from the\n * internal dashboard's preview renderer).\n */\n\nexport interface BlogSeoOptions {\n /** Site origin, e.g. `https://client.example.com`. */\n siteUrl: string;\n /** Route the blog is mounted at. Default `/blog`. */\n basePath?: string;\n}\n\nexport interface BlogJsonLdOptions extends BlogSeoOptions {\n /** Emits `publisher: { \"@type\": \"Organization\", name }` when given. */\n publisherName?: string;\n}\n\nexport interface BlogPostMetadataImage {\n url: string;\n width?: number;\n height?: number;\n alt?: string;\n}\n\n/** Structural subset of Next.js `Metadata` — spread it into `generateMetadata`. */\nexport interface BlogPostMetadata {\n title: string;\n description: string;\n alternates: { canonical: string };\n openGraph: {\n title: string;\n description: string;\n type: \"article\";\n url: string;\n images?: BlogPostMetadataImage[];\n publishedTime?: string;\n };\n}\n\nexport interface BlogSitemapEntry {\n url: string;\n lastModified: string;\n}\n\n/** Join URL segments without doubling or dropping slashes. */\nfunction joinUrl(base: string, ...paths: Array<string | undefined>): string {\n let out = base.replace(/\\/+$/, \"\");\n for (const path of paths) {\n const segment = path?.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (segment) out += `/${segment}`;\n }\n return out;\n}\n\n// The canonical URL is ALWAYS derived from the client site's own routing\n// (`siteUrl` + `basePath` + slug) — the client mounts the route, so client\n// routing wins. The backend's `seo.canonical_path` is informational only: it\n// assumes a `/blog` mount and would emit a canonical pointing at a\n// non-existent path on sites mounted elsewhere (e.g. `/news`). This also\n// keeps metadata, JSON-LD, and sitemap URLs derived from one rule.\nfunction canonicalUrl(post: BlogPostContent, opts: BlogSeoOptions): string {\n return joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug);\n}\n\nfunction ogImages(post: BlogPostContent): BlogPostMetadataImage[] | undefined {\n if (post.seo.og_image) return [{ url: post.seo.og_image }];\n const hero = post.images.find((image) => image.role === \"hero\");\n if (!hero) return undefined;\n return [{ url: hero.url, width: hero.width, height: hero.height, alt: hero.alt }];\n}\n\n/** Build a Next `Metadata`-shaped object for a post's `generateMetadata`. */\nexport function buildBlogPostMetadata(\n post: BlogPostContent,\n opts: BlogSeoOptions,\n): BlogPostMetadata {\n const url = canonicalUrl(post, opts);\n const title = post.seo.meta_title || post.title;\n const description = post.seo.meta_description || post.description;\n const images = ogImages(post);\n return {\n title,\n description,\n alternates: { canonical: url },\n openGraph: {\n title,\n description,\n type: \"article\",\n url,\n ...(images ? { images } : {}),\n ...(post.published_at ? { publishedTime: post.published_at } : {}),\n },\n };\n}\n\n/**\n * Build a `BlogPosting` JSON-LD object. When the backend shipped a non-empty\n * `post.jsonld`, its values are spread over the locally derived defaults so\n * backend-authored enrichment (author, keywords, …) wins — EXCEPT the pinned\n * fields spread after it: `@context`/`@type` (type identity of the emitted\n * document) and `url`/`mainEntityOfPage` (client-domain truths derived from\n * the site's own routing) can never be overridden by backend data.\n */\nexport function buildBlogPostingJsonLd(\n post: BlogPostContent,\n opts: BlogJsonLdOptions,\n): Record<string, unknown> {\n const url = canonicalUrl(post, opts);\n const images = post.images.map((image) => image.url);\n const doc: Record<string, unknown> = {\n headline: post.title,\n description: post.seo.meta_description || post.description,\n datePublished: post.published_at,\n dateModified: post.updated_at ?? post.published_at,\n ...(images.length > 0 ? { image: images } : {}),\n ...(opts.publisherName\n ? { publisher: { \"@type\": \"Organization\", name: opts.publisherName } }\n : {}),\n };\n const backend = post.jsonld;\n const merged =\n backend && typeof backend === \"object\" && Object.keys(backend).length > 0\n ? { ...doc, ...backend }\n : doc;\n return {\n ...merged,\n \"@context\": \"https://schema.org\",\n \"@type\": \"BlogPosting\",\n url,\n mainEntityOfPage: { \"@type\": \"WebPage\", \"@id\": url },\n };\n}\n\nexport interface BlogFaqPair {\n question: string;\n answer: string;\n}\n\n// `<FaqItem question=\"…\">…</FaqItem>` (or '…') with the answer captured lazily.\nconst FAQ_ITEM_RE = /<FaqItem\\s+question=(?:\"([^\"]*)\"|'([^']*)')\\s*>([\\s\\S]*?)<\\/FaqItem>/g;\n// Fenced code blocks — FaqItem text inside a fence is code, not an FAQ.\nconst CODE_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[^\\n]*\\n[\\s\\S]*?(?:^ {0,3}\\1`* *$|(?![\\s\\S]))/gm;\n// Component tags + inline code markers stripped from answer prose.\nconst ANSWER_TAG_RE = /<[^>]*>/g;\n\n/**\n * Extract `<FaqItem question>` question/answer pairs from a post's MDX.\n *\n * Deliberately a light regex, not a parser: it runs on documents that have\n * ALREADY passed `validateBlogMdx` at both the persist and render boundaries,\n * where `<FaqItem>` is guaranteed to carry exactly one quoted-string\n * `question` attribute and flow-markdown children. On anything else it simply\n * extracts fewer/no pairs — it never renders content, so there is no\n * injection surface. Fenced code blocks are dropped first so documentation\n * ABOUT the component is not read as an FAQ. Answers are flattened to plain\n * text (tags stripped, markdown markers left as-is, whitespace collapsed) —\n * Google accepts plain-text `acceptedAnswer.text`.\n */\nexport function extractFaqPairs(mdx: string): BlogFaqPair[] {\n const withoutCode = mdx.replaceAll(CODE_FENCE_RE, \"\");\n const pairs: BlogFaqPair[] = [];\n for (const match of withoutCode.matchAll(FAQ_ITEM_RE)) {\n const question = (match[1] ?? match[2] ?? \"\").trim();\n const answer = match[3].replaceAll(ANSWER_TAG_RE, \" \").replaceAll(/\\s+/g, \" \").trim();\n if (question && answer) pairs.push({ question, answer });\n }\n return pairs;\n}\n\n/**\n * Build a schema.org `FAQPage` JSON-LD object from extracted FAQ pairs\n * (see {@link extractFaqPairs}). Returns `null` when there are no pairs —\n * an empty `FAQPage` is invalid structured data, so callers can gate the\n * `<script type=\"application/ld+json\">` emission on the return value.\n */\nexport function buildFaqJsonLd(pairs: BlogFaqPair[]): Record<string, unknown> | null {\n if (pairs.length === 0) return null;\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: pairs.map((pair) => ({\n \"@type\": \"Question\",\n name: pair.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: pair.answer },\n })),\n };\n}\n\nconst JSONLD_ESCAPES: Record<string, string> = {\n \"<\": \"\\\\u003c\",\n \">\": \"\\\\u003e\",\n \"&\": \"\\\\u0026\",\n \"\\u2028\": \"\\\\u2028\",\n \"\\u2029\": \"\\\\u2029\",\n};\n\n/**\n * Serialize a JSON-LD document for embedding in a\n * `<script type=\"application/ld+json\">` tag. Escapes `<`, `>`, `&` and the\n * U+2028/U+2029 line separators as `\\uXXXX` sequences (valid JSON, identical\n * parse result) so backend-influenced content can never break out of the\n * script context (`</script>` injection) or produce invalid JS source.\n */\nexport function serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data).replaceAll(/[<>&\\u2028\\u2029]/g, (ch) => JSONLD_ESCAPES[ch] ?? ch);\n}\n\n/** Sitemap rows for every published post (shape matches Next's `MetadataRoute.Sitemap`). */\nexport function buildBlogSitemapEntries(\n posts: BlogManifestEntry[],\n opts: BlogSeoOptions,\n): BlogSitemapEntry[] {\n return posts.map((post) => ({\n url: joinUrl(opts.siteUrl, opts.basePath ?? \"/blog\", post.slug),\n lastModified: post.updated_at,\n }));\n}\n","// React-free table-of-contents + reading-time helpers for blog MDX. Pure mdast\n// walk over the SAME front-end the validator uses (remark-parse + remark-mdx),\n// so heading extraction can never disagree with what the renderer compiles.\n//\n// Slug parity contract: ids are produced with a fresh `GithubSlugger` per\n// document, walked in document order over h2/h3 — EXACTLY what `rehype-slug`\n// does at render time in `BlogPostView`... almost. rehype-slug slugs EVERY\n// heading rank (h1–h6), and duplicate-slug `-N` suffixes are allocated from\n// that full sequence. To keep extracted ids byte-identical to rendered ids,\n// the walk below feeds every heading through the slugger but only REPORTS\n// h2/h3 (the ranks a table of contents links to). Verified by the corpus\n// parity test in `__tests__/toc.test.ts`.\n\nimport GithubSlugger from \"github-slugger\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\n/** One table-of-contents entry: an h2/h3 with its rendered anchor id. */\nexport interface BlogHeading {\n /** Heading rank — a TOC only links section (h2) and subsection (h3) heads. */\n depth: 2 | 3;\n /** Plain-text heading content (formatting stripped, code/emphasis flattened). */\n text: string;\n /** The `id` rehype-slug stamps on the rendered heading element. */\n id: string;\n}\n\n// Minimal structural view of the mdast nodes we walk (same approach as\n// validate-mdx.ts — keeps the public d.ts free of @types/mdast).\ninterface MdastNode {\n type: string;\n depth?: number;\n value?: string;\n children?: MdastNode[];\n}\n\n/** Textual leaf types whose `value` contributes to a heading's visible text. */\nconst TEXT_LEAF_TYPES = new Set([\"text\", \"inlineCode\"]);\n\nfunction headingText(node: MdastNode): string {\n let text = \"\";\n const stack: MdastNode[] = [];\n const roots = node.children ?? [];\n for (let i = roots.length - 1; i >= 0; i--) {\n stack.push(roots[i]);\n }\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current) break;\n if (TEXT_LEAF_TYPES.has(current.type) && typeof current.value === \"string\") {\n text += current.value;\n }\n if (Array.isArray(current.children)) {\n for (let i = current.children.length - 1; i >= 0; i--) {\n stack.push(current.children[i]);\n }\n }\n }\n return text;\n}\n\n/**\n * Extract the h2/h3 outline of a blog MDX document with the exact anchor ids\n * `BlogPostView` renders (rehype-slug parity — see the module header). Returns\n * `[]` for unparseable input; feed it the same `mdx` the view receives.\n */\nexport function extractBlogHeadings(mdx: string): BlogHeading[] {\n let tree: MdastNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdastNode;\n } catch {\n return [];\n }\n\n const slugger = new GithubSlugger();\n const headings: BlogHeading[] = [];\n\n // Document-order walk. Headings are flow content — they appear at the root\n // or inside containers (blockquote, list, Callout) — and rehype-slug slugs\n // them wherever they are, so the walk must descend everywhere.\n const stack: MdastNode[] = [tree];\n while (stack.length > 0) {\n const node = stack.pop();\n if (!node) break;\n if (node.type === \"heading\" && typeof node.depth === \"number\") {\n const text = headingText(node);\n // Every rank consumes a slug (dedupe-counter parity with rehype-slug),\n // but only h2/h3 land in the TOC.\n const id = slugger.slug(text);\n if (node.depth === 2 || node.depth === 3) {\n headings.push({ depth: node.depth, text, id });\n }\n }\n if (Array.isArray(node.children)) {\n for (let i = node.children.length - 1; i >= 0; i--) {\n stack.push(node.children[i]);\n }\n }\n }\n return headings;\n}\n\n/** Average adult reading speed used for the estimate, in words per minute. */\nconst WORDS_PER_MINUTE = 220;\n\n// Crude MDX-syntax strip for word counting: JSX tags, code fences/backticks,\n// heading/list/quote markers, link targets. Word-count precision beyond this\n// is noise at a \"N min read\" granularity.\nconst MDX_SYNTAX_RE = /<[^>]*>|```[^`]*```|`|^[#>\\-*+\\s]+|\\]\\([^)]*\\)|[[\\]!]/gm;\n\n/**\n * Estimated reading time of a blog MDX document, in whole minutes (>= 1).\n * Deliberately crude — strips MDX/markdown syntax and counts words at\n * {@link WORDS_PER_MINUTE}.\n */\nexport function estimateReadingTime(mdx: string): number {\n const words = mdx\n .replaceAll(MDX_SYNTAX_RE, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 0).length;\n return Math.max(1, Math.round(words / WORDS_PER_MINUTE));\n}\n","// Mechanical MDX grammar enforcement for generated blog posts — Layer 1 (the\n// authoritative render boundary). Pure and framework-free: no React, no\n// next-mdx-remote. `BlogPostView` runs this unconditionally BEFORE compileMDX;\n// the API mirrors the same grammar at persist time\n// (apps/api/src/services/blog_mdx_validation.py).\n//\n// Why parse here at all: next-mdx-remote v6 strips expression *evaluation*,\n// but intrinsic JSX/HTML elements (<script src>, <iframe>, event-handler\n// attributes, javascript: URLs) flow straight into served HTML — stored XSS\n// on customer domains. The component allow-list passed to the renderer is not\n// a boundary (intrinsics bypass it), and prompt rules are only instructions.\n//\n// No parser differential by construction: we parse with the SAME mdast\n// front-end @mdx-js/mdx uses internally (remark-parse + remark-mdx, versions\n// aligned with what next-mdx-remote resolves), so whatever the compiler would\n// see, this validator saw first. The grammar is default-deny: any node type\n// not explicitly whitelisted rejects the document.\n\nimport remarkGfm from \"remark-gfm\";\nimport remarkMdx from \"remark-mdx\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\n\nexport interface BlogMdxViolation {\n /** Stable machine-readable rule id (safe to surface — never raw content). */\n rule: string;\n message: string;\n line?: number;\n}\n\nexport type BlogMdxValidationResult = { ok: true } | { ok: false; violations: BlogMdxViolation[] };\n\n/** DoS guard: maximum accepted MDX source size, in UTF-16 code units. */\nexport const BLOG_MDX_MAX_SIZE = 500_000;\n/** DoS guard: maximum AST nesting depth (realistic posts stay under ~10). */\nexport const BLOG_MDX_MAX_DEPTH = 64;\n/** DoS guard: maximum total AST node count. */\nexport const BLOG_MDX_MAX_NODES = 100_000;\n/** Cap on reported violations so a hostile document can't flood the caller. */\nconst MAX_VIOLATIONS = 50;\n\n/**\n * One allowed component of the blog MDX grammar, in serializable form.\n * `attrs` is the EXACT attribute set — every attribute is required, nothing\n * else allowed; `selfClosing` components must have no children.\n *\n * `cssClass`/`promptUsage`/`promptDescription` exist SOLELY so this one\n * record can be the entire cross-language/cross-surface registry — see the\n * grammar doc comment below for what derives from each field.\n */\nexport interface BlogMdxComponentGrammarEntry {\n name: string;\n attrs: readonly string[];\n urlAttrs: readonly string[];\n selfClosing: boolean;\n /** Base CSS class the default renderer applies, when the component is styled. */\n cssClass?: string;\n /** Example JSX usage shown to the drafting/editing agents. */\n promptUsage: string;\n /** One-line description of when/how to use it, shown to the agents. */\n promptDescription: string;\n}\n\n/**\n * The component grammar — the SINGLE source of truth for the closed\n * component set (name/attrs/urlAttrs/selfClosing/cssClass/prompt copy). This\n * is the only place to touch when adding, removing, or changing a component;\n * everything else derives from it instead of hand-mirroring it:\n *\n * - the MDX validator specs below (grammar enforcement),\n * - the `BlogComponents` renderer contract (a mapped-type assertion in\n * `view/components.tsx` makes an entry without a renderer a compile error),\n * - the `resources.blog_mdx_grammar` snapshot in `events.schema.json`\n * (emitted verbatim by `src/events/build-schema.ts`), which\n * `apps/api/src/services/blog_mdx_components.py` is GENERATED from\n * (`apps/api/scripts/generate_blog_mdx_grammar.py`) — the backend has no\n * hand-typed mirror to drift out of sync,\n * - the agent prompt templates (`blog/_mdx_grammar.j2`, via the generated\n * Python module's `prompt_usage`/`prompt_description` fields),\n * - `styles.css` (a completeness test asserts every `cssClass` here has a\n * rule), and the MDX conformance corpus (a completeness test asserts every\n * `name` here has at least one accept-case fixture).\n */\nexport const BLOG_MDX_COMPONENT_GRAMMAR = [\n {\n name: \"Callout\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-callout\",\n promptUsage: \"<Callout>...</Callout>\",\n promptDescription: \"a highlighted tip/note block; no attributes.\",\n },\n {\n name: \"Cta\",\n attrs: [\"href\", \"label\"],\n urlAttrs: [\"href\"],\n selfClosing: true,\n cssClass: \"aranova-blog-cta\",\n promptUsage: '<Cta href=\"...\" label=\"...\" />',\n promptDescription: \"a call-to-action button (self-closing).\",\n },\n {\n name: \"BlogImage\",\n attrs: [\"id\"],\n urlAttrs: [],\n selfClosing: true,\n promptUsage: '<BlogImage id=\"...\" />',\n promptDescription:\n \"a generated image placeholder (self-closing). The `id` references an \" +\n \"image brief id from the plan; the renderer resolves it.\",\n },\n {\n name: \"FaqItem\",\n attrs: [\"question\"],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-faq-item\",\n promptUsage: '<FaqItem question=\"...\">...</FaqItem>',\n promptDescription:\n \"one FAQ entry: the required `question` attribute carries the question, \" +\n \"the children are the prose answer. Group consecutive `<FaqItem>`s \" +\n \"under an FAQ heading near the end of the post when the topic suits a Q&A section.\",\n },\n {\n name: \"KeyTakeaways\",\n attrs: [],\n urlAttrs: [],\n selfClosing: false,\n cssClass: \"aranova-blog-key-takeaways\",\n promptUsage: \"<KeyTakeaways>...</KeyTakeaways>\",\n promptDescription:\n \"a TL;DR box containing a standard markdown bullet list of 3-5 takeaways; \" +\n \"place it right after the H1 (and hero image).\",\n },\n] as const satisfies readonly BlogMdxComponentGrammarEntry[];\n\n/** Union of the allowed component names, derived from the grammar. */\nexport type BlogMdxComponentName = (typeof BLOG_MDX_COMPONENT_GRAMMAR)[number][\"name\"];\n\n/** The closed component set of the blog grammar (mirrors the agent prompts). */\nexport const BLOG_MDX_ALLOWED_COMPONENTS: readonly BlogMdxComponentName[] =\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => entry.name);\n\n/**\n * Standard-markdown mdast node types the grammar accepts. Everything else —\n * including `html`, the MDX expression/ESM node types, and any type added by\n * a future plugin — is rejected by default.\n */\nconst ALLOWED_NODE_TYPES = new Set([\n \"root\",\n \"heading\",\n \"paragraph\",\n \"text\",\n \"emphasis\",\n \"strong\",\n \"delete\",\n \"link\",\n \"image\",\n \"linkReference\",\n \"imageReference\",\n \"definition\",\n \"list\",\n \"listItem\",\n \"blockquote\",\n \"code\",\n \"inlineCode\",\n \"table\",\n \"tableRow\",\n \"tableCell\",\n \"thematicBreak\",\n \"break\",\n]);\n\n/** Named rejections for the node types that are the actual attack surface. */\nconst FORBIDDEN_NODE_TYPES: Record<string, { rule: string; message: string }> = {\n html: { rule: \"raw-html\", message: \"Raw HTML is not allowed\" },\n mdxjsEsm: { rule: \"esm\", message: \"import/export statements are not allowed\" },\n mdxFlowExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n mdxTextExpression: { rule: \"expression\", message: \"JSX expressions ({…}) are not allowed\" },\n};\n\ninterface ComponentSpec {\n /** Exact attribute set — every attribute is required, nothing else allowed. */\n attrs: readonly string[];\n /** Attributes whose (string) value must pass the URL policy. */\n urlAttrs: readonly string[];\n /** Self-closing components must have no children. */\n allowChildren: boolean;\n}\n\nconst COMPONENT_SPECS: Record<string, ComponentSpec> = Object.fromEntries(\n BLOG_MDX_COMPONENT_GRAMMAR.map((entry) => [\n entry.name,\n { attrs: entry.attrs, urlAttrs: entry.urlAttrs, allowChildren: !entry.selfClosing },\n ]),\n);\n\nconst URL_SCHEME_RE = /^([A-Za-z][A-Za-z0-9+.-]*):/;\nconst ALLOWED_URL_SCHEMES = new Set([\"http\", \"https\", \"mailto\"]);\n\n/**\n * Scheme whitelist: http(s)/mailto/relative (schemeless) only. ASCII control\n * characters and whitespace are stripped BEFORE scheme detection — browsers\n * ignore them when parsing URLs, so `java\\tscript:` must not slip through.\n * Returns the offending scheme, or null when the URL is acceptable.\n */\nfunction forbiddenUrlScheme(rawUrl: string): string | null {\n // Intentional control-character class — the whole point is stripping the\n // characters browsers ignore in URLs before the scheme check.\n // eslint-disable-next-line no-control-regex\n const cleaned = rawUrl.replaceAll(/[\\u0000-\\u0020\\u007F]/g, \"\");\n const match = URL_SCHEME_RE.exec(cleaned);\n if (!match) return null; // relative / fragment / protocol-relative\n const scheme = match[1].toLowerCase();\n return ALLOWED_URL_SCHEMES.has(scheme) ? null : scheme;\n}\n\n// Loose structural view of the mdast/MDX node shapes we inspect. Kept local\n// (not the full @types/mdast surface) so the public d.ts stays dependency-free.\ninterface MdxAstAttribute {\n type: string; // \"mdxJsxAttribute\" | \"mdxJsxExpressionAttribute\"\n name?: string;\n value?: unknown; // string | null | { type: \"mdxJsxAttributeValueExpression\" }\n}\n\ninterface MdxAstNode {\n type: string;\n children?: MdxAstNode[];\n url?: string;\n name?: string | null;\n attributes?: MdxAstAttribute[];\n position?: { start?: { line?: number } };\n depth?: number;\n}\n\nfunction nodeLine(node: MdxAstNode): number | undefined {\n return node.position?.start?.line;\n}\n\nfunction checkComponent(node: MdxAstNode, violations: BlogMdxViolation[]): void {\n const line = nodeLine(node);\n const name = node.name;\n\n if (!name) {\n violations.push({\n rule: \"unknown-component\",\n message: \"JSX fragments (<>…</>) are not allowed\",\n line,\n });\n return;\n }\n\n const spec = COMPONENT_SPECS[name];\n if (!spec) {\n violations.push({\n rule: \"unknown-component\",\n message: `<${name}> is not an allowed element (allowed: ${BLOG_MDX_ALLOWED_COMPONENTS.join(\", \")})`,\n line,\n });\n return;\n }\n\n const seen = new Set<string>();\n for (const attr of node.attributes ?? []) {\n if (attr.type !== \"mdxJsxAttribute\" || typeof attr.name !== \"string\") {\n // Spread attributes ({...props}) or anything unrecognized.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> has a non-literal attribute`,\n line,\n });\n continue;\n }\n if (!spec.attrs.includes(attr.name)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> does not accept attribute \"${attr.name}\"`,\n line,\n });\n continue;\n }\n if (typeof attr.value !== \"string\") {\n // null = bare attribute; object = {expression} value.\n violations.push({\n rule: \"component-attribute\",\n message: `<${name} ${attr.name}> must be a quoted string literal`,\n line,\n });\n continue;\n }\n seen.add(attr.name);\n if (spec.urlAttrs.includes(attr.name)) {\n const scheme = forbiddenUrlScheme(attr.value);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `<${name} ${attr.name}> uses forbidden URL scheme \"${scheme}:\"`,\n line,\n });\n }\n }\n }\n\n for (const required of spec.attrs) {\n if (!seen.has(required)) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> requires attribute \"${required}\"`,\n line,\n });\n }\n }\n\n if (!spec.allowChildren && (node.children?.length ?? 0) > 0) {\n violations.push({\n rule: \"component-attribute\",\n message: `<${name}> must be self-closing (no children)`,\n line,\n });\n }\n}\n\nexport interface BlogMdxValidateOptions {\n /**\n * Reject any content before the H1 (frontmatter, a stray metadata block,\n * prose) — only meaningful for a whole post. Off by default because the\n * cross-language conformance corpus exercises single components in\n * isolation, not full documents; the render boundary (BlogPostView, the\n * only production caller) always validates a whole post and passes `true`.\n */\n requireLeadingH1?: boolean;\n}\n\n/**\n * Validate a generated blog MDX document against the frozen content grammar:\n * standard markdown + exactly the components in\n * {@link BLOG_MDX_COMPONENT_GRAMMAR} (`<Callout>`, `<Cta href label/>`,\n * `<BlogImage id/>`, `<FaqItem question>`, `<KeyTakeaways>`). Rejects ESM,\n * JSX expressions, raw HTML, every other JSX/HTML element, non-literal\n * attributes, and non-http(s)/mailto/relative URLs. Pure and synchronous —\n * safe anywhere (RSC, node, edge).\n */\nexport function validateBlogMdx(\n mdx: string,\n options?: BlogMdxValidateOptions,\n): BlogMdxValidationResult {\n const violations: BlogMdxViolation[] = [];\n\n if (typeof mdx !== \"string\" || mdx.length > BLOG_MDX_MAX_SIZE) {\n return {\n ok: false,\n violations: [\n {\n rule: \"size-limit\",\n message: `Document exceeds the ${BLOG_MDX_MAX_SIZE}-character limit`,\n },\n ],\n };\n }\n\n let tree: MdxAstNode;\n try {\n tree = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMdx)\n .parse(mdx) as unknown as MdxAstNode;\n } catch (error) {\n const line = (error as { line?: number }).line;\n return {\n ok: false,\n violations: [\n {\n rule: \"parse-error\",\n message: \"Document is not valid MDX\",\n ...(typeof line === \"number\" ? { line } : {}),\n },\n ],\n };\n }\n\n if (options?.requireLeadingH1) {\n const firstChild = tree.children?.[0];\n if (!firstChild || firstChild.type !== \"heading\" || firstChild.depth !== 1) {\n violations.push({\n rule: \"leading-h1\",\n message:\n \"Document must start with the H1 title (`# ...`) — no frontmatter or metadata block before it\",\n line: nodeLine(firstChild ?? tree),\n });\n }\n }\n\n // Iterative default-deny walk with explicit depth tracking (DoS guard —\n // never recurse on attacker-shaped input).\n let nodeCount = 0;\n const stack: Array<{ node: MdxAstNode; depth: number }> = [{ node: tree, depth: 0 }];\n\n while (stack.length > 0 && violations.length < MAX_VIOLATIONS) {\n const item = stack.pop();\n if (!item) break;\n const { node, depth } = item;\n\n nodeCount += 1;\n if (nodeCount > BLOG_MDX_MAX_NODES) {\n violations.push({ rule: \"size-limit\", message: \"Document has too many nodes\" });\n break;\n }\n if (depth > BLOG_MDX_MAX_DEPTH) {\n violations.push({ rule: \"depth-limit\", message: \"Document nests too deeply\" });\n break;\n }\n\n const forbidden = FORBIDDEN_NODE_TYPES[node.type];\n if (forbidden) {\n violations.push({ ...forbidden, line: nodeLine(node) });\n continue; // never descend into forbidden subtrees\n }\n\n if (node.type === \"mdxJsxFlowElement\" || node.type === \"mdxJsxTextElement\") {\n checkComponent(node, violations);\n // Descend regardless: Callout children are flow content that must obey\n // the same grammar, and even rejected subtrees should report every rule\n // they break (useful pipeline feedback; the doc is already invalid).\n } else if (!ALLOWED_NODE_TYPES.has(node.type)) {\n violations.push({\n rule: \"disallowed-node\",\n message: `Markdown construct \"${node.type}\" is not allowed`,\n line: nodeLine(node),\n });\n continue;\n }\n\n if (\n (node.type === \"link\" || node.type === \"image\" || node.type === \"definition\") &&\n typeof node.url === \"string\"\n ) {\n const scheme = forbiddenUrlScheme(node.url);\n if (scheme !== null) {\n violations.push({\n rule: \"url-scheme\",\n message: `${node.type} uses forbidden URL scheme \"${scheme}:\"`,\n line: nodeLine(node),\n });\n }\n }\n\n if (Array.isArray(node.children)) {\n for (const child of node.children) {\n stack.push({ node: child, depth: depth + 1 });\n }\n }\n }\n\n return violations.length === 0 ? { ok: true } : { ok: false, violations };\n}\n"],"mappings":"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","names":["z","remarkParse","remarkGfm","remarkMdx","GithubSlugger","import_remark_gfm","import_remark_mdx","import_remark_parse","import_unified","remarkParse","remarkGfm","remarkMdx"]}