@inneropen/marvin-astro 1.3.0 → 2.0.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.
package/README.md CHANGED
@@ -132,20 +132,26 @@ entry's image assets, then the list item's `featuredAsset`.
132
132
  ## Site and chrome
133
133
 
134
134
  ```ts
135
- const site = await marvin.getSite(); // identity, SEO, brand assets — memoized
135
+ const site = await marvin.getSite(); // identity, SEO, logo/favicon URLs — memoized
136
136
  const chrome = await marvin.getSiteChrome(); // nav, footer, legal, social, inquiry — memoized
137
137
  ```
138
138
 
139
- `getSite()` resolves every `site_metadata_json.brand.<name>` asset slug to a URL in one pass, so
140
- a site adds a shared brand asset with one config line and reads `site.brand.<name>` — no code
141
- change per asset. `logo`, `favicon` and `seal` are aliased onto the top level.
139
+ `getSite()` takes `logo`, `favicon` and `seo.image` as the URLs Marvin now returns (no asset
140
+ lookups). `site.brand.<name>` collects the `site_metadata_json.brand.<name>` values that are
141
+ URLs, root-relative paths or data URIs, used as-is; bare asset slugs are left out because the
142
+ publishing API no longer serves assets by slug. `brand.logo`, `brand.favicon` and `brand.seal`
143
+ win over the top-level `logo`/`favicon`/`seal`.
142
144
 
143
145
  `getSiteChrome()` reads `main-navigation` and `footer-navigation` collections, splits
144
146
  `role: 'legal'` entries into `legalLinks`, and groups the rest into footer columns. Social links
145
147
  come from a `social-links` collection when the workspace has one (each entry's label, href and
146
- optional `icon` field, in order), otherwise from the site's `social` map. A collection that exists
147
- but is empty renders nothing: the static `chrome.fallback` links stand in only when Marvin has no
148
- such collection or can't be reached. A nav entry's
148
+ optional `icon` field, in order), otherwise from the site's `social` map.
149
+
150
+ Fallbacks follow one build-time check: did the site configuration come from Marvin
151
+ (`siteLoader.online()`)? If it did, Marvin's content is final — an empty, hidden or missing
152
+ collection renders nothing, and the static `site.fallback.social` / `chrome.fallback` links are
153
+ never mixed in. Only when Marvin is down or unconfigured does the whole chrome come from the static
154
+ fallbacks. A nav entry's
149
155
  route comes from an explicit `href`/`url`/`path` field if it has one, otherwise from
150
156
  `resolveHref`:
151
157
 
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { MarkedOptions } from 'marked';
2
2
  import { M as MarvinEmbeds, a as MarvinEmbed } from './embeds-BTNryQA6.js';
3
3
  export { b as MarvinEmbedIframe, c as MarvinEmbedKind, d as MarvinEmbedLink, e as MarvinEmbedMode, f as MarvinEmbedStatus, g as MarvinSiteEmbeds, h as entryEmbeds, i as findEmbed, s as siteEmbeds } from './embeds-BTNryQA6.js';
4
4
  import * as _inneropen_marvin_sdk from '@inneropen/marvin-sdk';
5
- import { MarvinClient, MarvinEntry, Entry, PublishedCollectionSummary, Collection, CollectionEntry, MarvinSite, Workspace, MarvinAsset, MarvinResource } from '@inneropen/marvin-sdk';
5
+ import { MarvinClient, MarvinEntry, Entry, PublishedCollectionSummary, Collection, CollectionEntry, MarvinSite, Workspace } from '@inneropen/marvin-sdk';
6
6
  import { MarvinContentEntry, ApiSite, ApiSeo, ApiSiteChrome, ApiNavigationLink, ApiSocialLink, AssetSelectOptions, ApiImage, ApiResourceLink, SectionLanding } from './types.js';
7
7
  export { ApiActionVariant, ApiHero, ApiHeroAction, ApiPageAction } from './types.js';
8
8
 
@@ -179,9 +179,6 @@ declare function createFetcher(backend: MarvinBackend): {
179
179
  entry: (slug: string) => Promise<Entry | null>;
180
180
  site(): Promise<MarvinSite | null>;
181
181
  workspace(): Promise<Workspace | null>;
182
- assets(type?: string): Promise<MarvinAsset[]>;
183
- asset(slugOrId: string): Promise<MarvinAsset | null>;
184
- resources(resourceType?: string): Promise<MarvinResource[]>;
185
182
  };
186
183
 
187
184
  /**
@@ -189,8 +186,8 @@ declare function createFetcher(backend: MarvinBackend): {
189
186
  *
190
187
  * Marvin's site configuration is optional almost everywhere; a template that had to guard every
191
188
  * field would be unreadable. This resolves it once into an `ApiSite` where title, description,
192
- * siteName, locale and timezone are always present, and where brand asset slugs have already
193
- * become URLs.
189
+ * siteName, locale and timezone are always present. Logo, favicon and SEO image are the URLs
190
+ * Marvin returns, used as-is.
194
191
  */
195
192
 
196
193
  type SiteOptions = {
@@ -219,6 +216,12 @@ declare function buildSeo(raw: Record<string, unknown> | undefined, identity: {
219
216
  declare function buildFallbackSite(options?: SiteOptions): ApiSite;
220
217
  type SiteLoader = {
221
218
  get(): Promise<ApiSite>;
219
+ /**
220
+ * The build's one "is Marvin up?" answer: true when the site configuration came from Marvin.
221
+ * When it did, Marvin's content is final (an empty or hidden collection shows nothing); the
222
+ * static fallbacks stand in only when it didn't.
223
+ */
224
+ online(): Promise<boolean>;
222
225
  reset(): void;
223
226
  };
224
227
  declare function createSiteLoader(fetcher: MarvinFetcher, options?: SiteOptions): SiteLoader;
@@ -259,9 +262,9 @@ type ChromeOptions = {
259
262
  /** Collection holding the footer nav. Default `'footer-navigation'`. */
260
263
  footerCollection?: string;
261
264
  /**
262
- * Collection holding the social links. Default `'social-links'`. When it exists, its entries
263
- * (label, href, optional `icon` field) are exactly the social links, in order, and the site
264
- * `social` map is left out; when it doesn't, the social map is used.
265
+ * Collection holding the social links. Default `'social-links'`. When it has entries, they
266
+ * (label, href, optional `icon` field) are exactly the social links, in order; otherwise the
267
+ * site `social` map is used.
265
268
  */
266
269
  socialCollection?: string;
267
270
  /**
@@ -614,14 +617,11 @@ declare function createMarvinContent(options?: MarvinContentOptions): {
614
617
  entry: (slug: string) => Promise<_inneropen_marvin_sdk.Entry | null>;
615
618
  site(): Promise<_inneropen_marvin_sdk.MarvinSite | null>;
616
619
  workspace(): Promise<_inneropen_marvin_sdk.Workspace | null>;
617
- assets(type?: string): Promise<_inneropen_marvin_sdk.MarvinAsset[]>;
618
- asset(slugOrId: string): Promise<_inneropen_marvin_sdk.MarvinAsset | null>;
619
- resources(resourceType?: string): Promise<_inneropen_marvin_sdk.MarvinResource[]>;
620
620
  };
621
621
  renderMarkdown: MarkdownRenderer;
622
622
  /** True when Marvin is configured and not currently latched out as unreachable. */
623
623
  hasBackend: () => boolean;
624
- /** Resolved site identity, SEO and brand assets. Memoized per process. */
624
+ /** Site identity, SEO and logo/favicon URLs. Memoized per process. */
625
625
  getSite(): Promise<ApiSite>;
626
626
  /** Navigation, footer, legal, social and inquiry links. Memoized per process. */
627
627
  getSiteChrome(): Promise<ApiSiteChrome>;
package/dist/index.js CHANGED
@@ -362,35 +362,30 @@ function createChromeLoader(fetcher, siteLoader, options = {}) {
362
362
  role: collectionRole(entry, collectionSlug)
363
363
  };
364
364
  }
365
- async function navigationCollection(slug, context, staticLinks) {
366
- if (!fetcher.backend.hasBackend()) return { links: staticLinks, entries: [], fromBackend: false };
365
+ async function navigationCollection(slug, context, online) {
366
+ if (!online) return { links: [], entries: [] };
367
367
  const entries = await fetcher.hydratedCollectionEntries(slug);
368
- if (entries.length === 0) {
369
- const exists = await fetcher.collection(slug) !== null;
370
- return { links: exists ? [] : staticLinks, entries, fromBackend: exists };
371
- }
372
- const links = entries.map((entry) => entryToLink(entry, slug, context));
373
- return { links, entries, fromBackend: true };
368
+ return { links: entries.map((entry) => entryToLink(entry, slug, context)), entries };
374
369
  }
375
370
  function groupFooterLinks(links) {
376
371
  if (links.length <= footerColumnThreshold) return [links];
377
372
  const midpoint = Math.ceil(links.length / 2);
378
373
  return [links.slice(0, midpoint), links.slice(midpoint)];
379
374
  }
380
- async function socialEntries() {
381
- const social = await navigationCollection(socialCollection, "footer", []);
382
- if (!social.fromBackend) return null;
375
+ async function socialEntries(online) {
376
+ const social = await navigationCollection(socialCollection, "footer", online);
377
+ if (social.links.length === 0) return null;
383
378
  return social.links.map((link, index) => ({
384
379
  ...link,
385
380
  icon: asString(entryField(social.entries[index], "icon")) ?? socialIconFor(link.href, link.label)
386
381
  }));
387
382
  }
388
- function siteSocialLinks(site) {
383
+ function siteSocialLinks(site, online) {
389
384
  const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));
390
385
  if (site.email && !links.some((link) => link.icon === "email")) {
391
386
  links.push(socialLinkFromKey("email", `mailto:${site.email}`));
392
387
  }
393
- if (links.length > 0) return links;
388
+ if (links.length > 0 || online) return links;
394
389
  return (fallback.socialLinks ?? []).map((link) => ({
395
390
  ...toNavigationLink(link),
396
391
  icon: link.icon
@@ -402,18 +397,19 @@ function createChromeLoader(fetcher, siteLoader, options = {}) {
402
397
  const staticFooter = (fallback.footerNavigation ?? []).map(
403
398
  (group) => group.map(toNavigationLink)
404
399
  );
405
- const main = await navigationCollection(mainCollection, "main", staticMain);
406
- const footer = await navigationCollection(footerCollection, "footer", staticFooter.flat());
407
- const social = await socialEntries();
400
+ const online = await siteLoader.online();
401
+ const main = await navigationCollection(mainCollection, "main", online);
402
+ const footer = await navigationCollection(footerCollection, "footer", online);
403
+ const social = await socialEntries(online);
408
404
  const columnLinks = footer.links.filter((link) => link.role !== legalRole);
409
405
  const staticLegal = (fallback.legalLinks ?? []).map(toNavigationLink);
410
406
  return {
411
407
  site,
412
- mainNavigation: main.links,
413
- footerNavigation: footer.fromBackend ? groupFooterLinks(columnLinks) : staticFooter,
414
- legalLinks: footer.fromBackend ? footer.links.filter((link) => link.role === legalRole) : staticLegal,
415
- socialLinks: social ?? siteSocialLinks(site),
416
- inquiry: metadataLink(site.metadata.inquiry) ?? (fallback.inquiry ? toNavigationLink(fallback.inquiry) : void 0)
408
+ mainNavigation: online ? main.links : staticMain,
409
+ footerNavigation: online ? groupFooterLinks(columnLinks) : staticFooter,
410
+ legalLinks: online ? footer.links.filter((link) => link.role === legalRole) : staticLegal,
411
+ socialLinks: social ?? siteSocialLinks(site, online),
412
+ inquiry: online ? metadataLink(site.metadata.inquiry) : fallback.inquiry && toNavigationLink(fallback.inquiry)
417
413
  };
418
414
  }
419
415
  return {
@@ -550,15 +546,6 @@ function createFetcher(backend) {
550
546
  },
551
547
  workspace() {
552
548
  return guarded("Workspace", null, () => backend.client().getWorkspace());
553
- },
554
- assets(type) {
555
- return guarded("Assets", [], () => backend.client().assets.list({ type }));
556
- },
557
- asset(slugOrId) {
558
- return guarded(`Asset "${slugOrId}"`, null, () => backend.client().assets.get(slugOrId));
559
- },
560
- resources(resourceType) {
561
- return guarded("Resources", [], () => backend.client().resources.list({ resourceType }));
562
549
  }
563
550
  };
564
551
  }
@@ -1069,7 +1056,8 @@ function transformMarvinSite(marvinSite, fallback) {
1069
1056
  timezone: asString(config.timezone) ?? fallback.timezone,
1070
1057
  email: asString(config.contactEmail) ?? fallback.email,
1071
1058
  imprint: asString(metadata.imprint) ?? fallback.imprint,
1072
- social: { ...fallback.social, ...normalizeSocial(config.social) },
1059
+ // Marvin's social map only: once Marvin answers, the static one must not leak back in.
1060
+ social: normalizeSocial(config.social),
1073
1061
  // Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older
1074
1062
  // SDKs drop the typed field but pass the blob through untouched, and workspaces configured
1075
1063
  // before the typed field existed still only have the blob.
@@ -1086,49 +1074,29 @@ function transformMarvinSite(marvinSite, fallback) {
1086
1074
  metadata
1087
1075
  };
1088
1076
  }
1089
- function looksLikeAssetSlug(value) {
1090
- return !/^https?:\/\//.test(value) && !value.startsWith("/") && !value.startsWith("data:");
1077
+ function isUrlLike(value) {
1078
+ return /^https?:\/\//.test(value) || value.startsWith("/") || value.startsWith("data:");
1091
1079
  }
1092
1080
  function createSiteLoader(fetcher, options = {}) {
1093
1081
  const fallback = buildFallbackSite(options);
1094
1082
  let promise = null;
1095
- async function resolveAssetSlug(slug) {
1096
- if (!slug) return void 0;
1097
- return assetUrl(await fetcher.asset(slug));
1098
- }
1099
- async function resolveMaybeSlug(value) {
1100
- if (!value || !looksLikeAssetSlug(value)) return value;
1101
- return await resolveAssetSlug(value) ?? value;
1102
- }
1083
+ let fromMarvin = false;
1103
1084
  async function load() {
1104
1085
  const marvinSite = await fetcher.site();
1086
+ fromMarvin = marvinSite !== null;
1105
1087
  if (!marvinSite) return fallback;
1106
1088
  const site = transformMarvinSite(marvinSite, fallback);
1107
- const brandPairs = Object.entries(asRecord(site.metadata.brand)).filter(
1108
- (entry) => typeof entry[1] === "string" && entry[1].trim().length > 0
1109
- );
1110
- const resolved = await Promise.all(
1111
- brandPairs.map(async ([name, slug]) => [name, await resolveAssetSlug(slug)])
1112
- );
1113
1089
  const brand = {};
1114
- const brandBySlug = /* @__PURE__ */ new Map();
1115
- for (const [index, [name, url]] of resolved.entries()) {
1116
- if (!url) continue;
1117
- brand[name] = url;
1118
- brandBySlug.set(brandPairs[index][1], url);
1090
+ for (const [name, value] of Object.entries(asRecord(site.metadata.brand))) {
1091
+ if (typeof value === "string" && isUrlLike(value.trim())) brand[name] = value.trim();
1119
1092
  }
1120
- const rawImage = site.seo.image;
1121
- const ogImage = rawImage && looksLikeAssetSlug(rawImage) ? brandBySlug.get(rawImage) ?? await resolveAssetSlug(rawImage) : void 0;
1122
- const logo = brand.logo ?? await resolveMaybeSlug(site.logo);
1123
- const favicon = brand.favicon ?? await resolveMaybeSlug(site.favicon);
1124
1093
  return {
1125
1094
  ...site,
1126
1095
  brand,
1127
1096
  // Convenience aliases for common callers; all of these also live in `site.brand`.
1128
- logo,
1129
- favicon,
1130
- seal: brand.seal ?? site.seal,
1131
- seo: { ...site.seo, image: ogImage ?? site.seo.image }
1097
+ logo: brand.logo ?? site.logo,
1098
+ favicon: brand.favicon ?? site.favicon,
1099
+ seal: brand.seal ?? site.seal
1132
1100
  };
1133
1101
  }
1134
1102
  return {
@@ -1136,8 +1104,13 @@ function createSiteLoader(fetcher, options = {}) {
1136
1104
  promise ??= load();
1137
1105
  return promise;
1138
1106
  },
1107
+ async online() {
1108
+ await this.get();
1109
+ return fromMarvin;
1110
+ },
1139
1111
  reset() {
1140
1112
  promise = null;
1113
+ fromMarvin = false;
1141
1114
  }
1142
1115
  };
1143
1116
  }
@@ -1160,7 +1133,7 @@ function createMarvinContent(options = {}) {
1160
1133
  renderMarkdown,
1161
1134
  /** True when Marvin is configured and not currently latched out as unreachable. */
1162
1135
  hasBackend: () => backend.hasBackend(),
1163
- /** Resolved site identity, SEO and brand assets. Memoized per process. */
1136
+ /** Site identity, SEO and logo/favicon URLs. Memoized per process. */
1164
1137
  getSite() {
1165
1138
  return siteLoader.get();
1166
1139
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/config.ts","../src/normalize.ts","../src/chrome.ts","../src/fetch.ts","../src/markdown.ts","../src/embeds.ts","../src/format.ts","../src/fields.ts","../src/repository.ts","../src/sections.ts","../src/site.ts","../src/index.ts"],"sourcesContent":["/**\n * SDK client construction and the network-failure latch.\n *\n * The latch exists because a static build asks for content once per path. When the backend is\n * down, that is N failed requests with N timeouts before the build gives up. The latch trips on\n * the first network failure and short-circuits the rest.\n *\n * Unlike the hand-rolled original it is *retryable*: it expires after `retryAfterMs` so a dev\n * server recovers on its own when the backend comes back, instead of serving stale static data\n * until someone restarts it. Production keeps the permanent latch — a build should fail fast and\n * consistently rather than half-succeed.\n */\n\nimport { createMarvinClient } from '@inneropen/marvin-sdk';\nimport type { MarvinClient } from '@inneropen/marvin-sdk';\nimport {\n describeConfig,\n resolveConfig,\n type MarvinAstroConfig,\n type ResolvedConfig,\n} from './config.js';\n\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\n/**\n * A network failure means \"the backend is unreachable\", not \"this request was bad\". Only the\n * former should trip the latch — a 404 on one entry says nothing about the next one.\n */\nexport function isNetworkFailure(error: unknown): boolean {\n const message = errorMessage(error).toLowerCase();\n return (\n message.includes('network error') ||\n message.includes('fetch failed') ||\n message.includes('econnrefused') ||\n message.includes('enotfound') ||\n message.includes('etimedout')\n );\n}\n\nexport type MarvinBackend = {\n readonly config: ResolvedConfig;\n /** True when Marvin is configured AND not currently latched out. */\n hasBackend(): boolean;\n /** The SDK client. Throws when Marvin is not configured. */\n client(): MarvinClient;\n /** Trip the latch if this error indicates the backend is unreachable. */\n remember(error: unknown): void;\n /** True while the latch is closed (backend considered unreachable). */\n isLatched(): boolean;\n /** Manually re-open the latch, e.g. after a deploy. */\n clearLatch(): void;\n warn(message: string): void;\n};\n\nexport function createBackend(options: MarvinAstroConfig = {}): MarvinBackend {\n const config = resolveConfig(options);\n let client: MarvinClient | null = null;\n let latchedAt: number | null = null;\n let envLogged = false;\n\n function isLatched(): boolean {\n if (latchedAt === null) return false;\n if (config.retryAfterMs === Number.POSITIVE_INFINITY) return true;\n if (config.now() - latchedAt < config.retryAfterMs) return true;\n\n // Expired — re-open and let the next call find out whether the backend is back.\n latchedAt = null;\n return false;\n }\n\n function hasBackend(): boolean {\n if (config.debug && !envLogged) {\n envLogged = true;\n config.logger.log('[marvin-astro] env check:', describeConfig(config));\n }\n if (!config.configured) return false;\n return !isLatched();\n }\n\n return {\n config,\n hasBackend,\n isLatched,\n clearLatch() {\n latchedAt = null;\n },\n remember(error: unknown) {\n if (isNetworkFailure(error)) latchedAt = config.now();\n },\n warn(message: string) {\n config.logger.warn(`[marvin-astro] ${message}`);\n },\n client(): MarvinClient {\n if (!config.configured) {\n throw new Error(\n '[marvin-astro] Marvin is not configured. Set MARVIN_API_URL, ' +\n 'MARVIN_SITE_CLIENT_TOKEN and MARVIN_WORKSPACE_SLUG, or pass them to createMarvinContent().'\n );\n }\n client ??= (config.createClient\n ? config.createClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })\n : createMarvinClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })) as MarvinClient;\n return client;\n },\n };\n}\n","/**\n * Configuration + environment resolution.\n *\n * Precedence for every value: explicit option → `import.meta.env` → `process.env`.\n * Astro exposes server-side env through `import.meta.env`; a plain Node script\n * (a smoke test, a build hook) only has `process.env`. Both work, unchanged.\n */\n\nimport type { MarkdownOptions } from './markdown.js';\n\nexport type MarvinLogger = {\n log: (message: string, ...args: unknown[]) => void;\n warn: (message: string, ...args: unknown[]) => void;\n error: (message: string, ...args: unknown[]) => void;\n};\n\nexport const ENV_KEYS = {\n apiUrl: 'MARVIN_API_URL',\n siteClientToken: 'MARVIN_SITE_CLIENT_TOKEN',\n workspaceSlug: 'MARVIN_WORKSPACE_SLUG',\n debug: 'MARVIN_DEBUG',\n} as const;\n\n/** Dev default for the network-failure latch: retry a downed backend after 30s. */\nexport const DEFAULT_DEV_RETRY_MS = 30_000;\n\n/** How many entry reads `hydrate` keeps in flight at once. */\nexport const DEFAULT_HYDRATE_CONCURRENCY = 6;\n\nexport type MarvinAstroConfig = {\n apiUrl?: string;\n siteClientToken?: string;\n workspaceSlug?: string;\n /** Log the resolved env once at startup, and enable SDK request logging. */\n debug?: boolean;\n /**\n * How long the network-failure latch stays closed before the backend is retried.\n * Defaults to 30s in dev and `Infinity` in production — a build should fail fast and\n * consistently rather than half-succeed with some pages live and some pages static.\n */\n retryAfterMs?: number;\n /**\n * Maximum entry reads in flight at once while hydrating a collection. Firing every read at\n * once makes a large collection time out on itself; the default of 6 keeps a 280-entry\n * hydration well inside the SDK's per-request timeout.\n */\n hydrateConcurrency?: number;\n logger?: MarvinLogger;\n /** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */\n markdown?: MarkdownOptions;\n /** Explicit env source. Highest priority after direct options — mainly a test seam. */\n env?: Record<string, string | undefined>;\n /** Injectable clock, so latch expiry is testable without waiting. */\n now?: () => number;\n /** Injectable SDK client factory — a test seam; defaults to the SDK's `createMarvinClient`. */\n createClient?: (config: MarvinClientOptions) => unknown;\n};\n\nexport type MarvinClientOptions = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n};\n\nexport type ResolvedConfig = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n retryAfterMs: number;\n hydrateConcurrency: number;\n logger: MarvinLogger;\n markdown?: MarkdownOptions;\n now: () => number;\n createClient?: (config: MarvinClientOptions) => unknown;\n /** True when all three connection values are present. */\n configured: boolean;\n};\n\nfunction metaEnv(): Record<string, unknown> | undefined {\n try {\n return (import.meta as unknown as { env?: Record<string, unknown> }).env;\n } catch {\n return undefined;\n }\n}\n\nfunction processEnv(): Record<string, string | undefined> | undefined {\n try {\n return typeof process !== 'undefined' ? process.env : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction nonEmpty(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\n/** Read one env var, honouring the documented precedence. */\nexport function readEnv(\n key: string,\n override?: Record<string, string | undefined>\n): string | undefined {\n return nonEmpty(override?.[key]) ?? nonEmpty(metaEnv()?.[key]) ?? nonEmpty(processEnv()?.[key]);\n}\n\nfunction isProduction(override?: Record<string, string | undefined>): boolean {\n const prodFlag = metaEnv()?.PROD;\n if (typeof prodFlag === 'boolean') return prodFlag;\n const mode = readEnv('NODE_ENV', override) ?? readEnv('MODE', override);\n return mode === 'production';\n}\n\nexport function resolveConfig(options: MarvinAstroConfig = {}): ResolvedConfig {\n const env = options.env;\n const apiUrl = options.apiUrl ?? readEnv(ENV_KEYS.apiUrl, env) ?? '';\n const siteClientToken = options.siteClientToken ?? readEnv(ENV_KEYS.siteClientToken, env) ?? '';\n const workspaceSlug = options.workspaceSlug ?? readEnv(ENV_KEYS.workspaceSlug, env) ?? '';\n const debug = options.debug ?? readEnv(ENV_KEYS.debug, env) === 'true';\n\n return {\n apiUrl,\n siteClientToken,\n workspaceSlug,\n debug,\n retryAfterMs:\n options.retryAfterMs ??\n (isProduction(env) ? Number.POSITIVE_INFINITY : DEFAULT_DEV_RETRY_MS),\n hydrateConcurrency: Math.max(1, Math.floor(options.hydrateConcurrency ?? DEFAULT_HYDRATE_CONCURRENCY)),\n logger: options.logger ?? console,\n markdown: options.markdown,\n now: options.now ?? (() => Date.now()),\n createClient: options.createClient,\n configured: Boolean(apiUrl && siteClientToken && workspaceSlug),\n };\n}\n\n/** One-line summary of the resolved env, with the token masked. For `debug` logging. */\nexport function describeConfig(config: ResolvedConfig): Record<string, unknown> {\n const token = config.siteClientToken;\n return {\n [ENV_KEYS.apiUrl]: config.apiUrl || 'MISSING',\n [ENV_KEYS.siteClientToken]: token ? `${token.slice(0, 12)}${'*'.repeat(20)}` : 'MISSING',\n [ENV_KEYS.workspaceSlug]: config.workspaceSlug || 'MISSING',\n hasAll: config.configured,\n };\n}\n","/**\n * Reading Marvin payloads.\n *\n * The published API returns the same logical value in several shapes depending on which\n * endpoint produced it (list item vs full read, SDK `Entry` wrapper vs raw JSON, placement\n * metadata vs asset metadata). Every reader here is total: it takes `unknown`, checks, and\n * returns `undefined` rather than throwing.\n */\n\nimport type { AssetSelectOptions, MarvinContentEntry } from './types.js';\n\n/** A non-empty string, or `undefined`. Empty/whitespace strings are treated as absent. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};\n}\n\nexport function asNumber(value: unknown): number | undefined {\n if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\nexport function asStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n\n const items = value.filter(\n (item): item is string => typeof item === 'string' && item.trim().length > 0\n );\n return items.length > 0 ? items : undefined;\n}\n\n/** `metadata_json` — the free-form blob every entry carries, whatever its schema. */\nexport function entryMetadata(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { metadata?: unknown; metadataJson?: unknown };\n return asRecord(value.metadata ?? value.metadataJson);\n}\n\n/** `data_json` — the schema-defined fields. Absent on list items until hydrated. */\nexport function entryData(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { data?: unknown; dataJson?: unknown };\n return asRecord(value.data ?? value.dataJson);\n}\n\n/**\n * Read a schema field (`data_json`) from an entry, through the SDK's `Entry` accessor when\n * present and off the raw payload otherwise.\n */\nexport function entryField<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const accessor = (entry as { field?: (key: string) => unknown }).field;\n if (typeof accessor === 'function') {\n return accessor.call(entry, key) as T | undefined;\n }\n return entryData(entry)[key] as T | undefined;\n}\n\n/**\n * The precedence rule: structured schema field first (`data_json`), legacy blob second\n * (`metadata_json`).\n *\n * An empty string counts as absent, which matters more than it sounds — an entry type that\n * declares a field the author left blank stores `\"\"`, and without this the legacy value that\n * *is* set would never surface.\n */\nexport function field<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const value = entryField(entry, key);\n if (value !== undefined && value !== null && value !== '') return value as T;\n const fallback = entryMetadata(entry)[key];\n return (fallback === '' ? undefined : fallback) as T | undefined;\n}\n\n/**\n * Read a value off a published asset placement, checking every level the API might have put it\n * at: the placement itself, its metadata blobs, then the underlying asset and its blobs.\n */\nexport function assetField<T = unknown>(asset: unknown, key: string): T | undefined {\n if (!asset || typeof asset !== 'object') return undefined;\n\n const value = asset as Record<string, unknown>;\n const nested = asRecord(value.asset);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.placementMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/** Same layered read as {@link assetField}, for resource relationships. */\nexport function resourceField<T = unknown>(resource: unknown, key: string): T | undefined {\n if (!resource || typeof resource !== 'object') return undefined;\n\n const value = resource as Record<string, unknown>;\n const nested = asRecord(value.resource);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/**\n * The entry's collection memberships as slugs. Collections arrive as bare slugs on list items,\n * flat `{slug}` objects, or the SDK's nested `{collection: {slug}}` — all three are read here.\n */\nexport function collectionSlugs(entry: MarvinContentEntry): string[] {\n const direct = (entry as { collectionSlugs?: unknown }).collectionSlugs;\n if (Array.isArray(direct)) {\n return direct.filter((slug): slug is string => typeof slug === 'string');\n }\n\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return [];\n\n return raw\n .map((item) =>\n typeof item === 'string' ? item : (asRecord(asRecord(item).collection).slug ?? asRecord(item).slug)\n )\n .filter((slug): slug is string => typeof slug === 'string');\n}\n\n/**\n * The entry's membership role within one specific collection (e.g. `'legal'` in\n * `footer-navigation`). A hydrated entry carries all of its memberships; this finds the one for\n * the given collection.\n */\nexport function collectionRole(\n entry: MarvinContentEntry,\n collectionSlug: string\n): string | undefined {\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return undefined;\n\n for (const item of raw) {\n if (typeof item === 'string') continue;\n const record = asRecord(item);\n const slug = asString(record.slug) ?? asString(asRecord(record.collection).slug);\n if (slug === collectionSlug) {\n return asString(asRecord(record.entryMetadata).role) ?? asString(record.role);\n }\n }\n return undefined;\n}\n\n/**\n * A CSS `object-position` value from a focal point. Marvin stores these either pre-formatted\n * (`\"40% 60%\"`) or as `{x, y}` in 0–1 or 0–100 units.\n */\nexport function focalPoint(value: unknown): string | undefined {\n const direct = asString(value);\n if (direct) return direct;\n\n const point = asRecord(value);\n const x = typeof point.x === 'number' ? point.x : undefined;\n const y = typeof point.y === 'number' ? point.y : undefined;\n if (x == null || y == null) return undefined;\n\n return `${x > 1 ? x : x * 100}% ${y > 1 ? y : y * 100}%`;\n}\n\nexport function entryAssets(entry: MarvinContentEntry): Record<string, unknown>[] {\n const assets = (entry as { assets?: unknown }).assets;\n return Array.isArray(assets) ? (assets as Record<string, unknown>[]) : [];\n}\n\nexport function entryResources(entry: MarvinContentEntry): Record<string, unknown>[] {\n const resources = (entry as { resources?: unknown }).resources;\n return Array.isArray(resources) ? (resources as Record<string, unknown>[]) : [];\n}\n\nfunction includesPreferred(actual: string | undefined, exact?: string, any?: string[]): boolean {\n if (!exact && !any?.length) return true;\n if (!actual) return false;\n return actual === exact || Boolean(any?.includes(actual));\n}\n\nfunction assetMatches(asset: Record<string, unknown>, options: AssetSelectOptions): boolean {\n const role = asString(assetField(asset, 'role'));\n const usage = asString(assetField(asset, 'usage'));\n const type = asString(assetField(asset, 'assetType'));\n const mimeType = asString(assetField(asset, 'mimeType'));\n\n const hasRoleCriteria = Boolean(options.role || options.roles?.length);\n const hasUsageCriteria = Boolean(options.usage || options.usages?.length);\n const roleMatches = includesPreferred(role, options.role, options.roles);\n const usageMatches = includesPreferred(usage, options.usage, options.usages);\n const typeMatches = includesPreferred(type, options.type, options.types);\n const mimeMatches = options.mimeType ? mimeType === options.mimeType : true;\n const unroledAllowed = Boolean(options.allowUnroled) && !role && !usage;\n\n // Role and usage are alternatives, not a conjunction: an asset placed with `role: hero` and\n // one placed with `usage: hero` both satisfy a hero request.\n const relationshipMatches =\n hasRoleCriteria || hasUsageCriteria ? roleMatches || usageMatches || unroledAllowed : true;\n\n return relationshipMatches && typeMatches && mimeMatches;\n}\n\nexport function selectEntryAsset(\n entry: MarvinContentEntry,\n options: AssetSelectOptions = {}\n): Record<string, unknown> | undefined {\n return entryAssets(entry).find((asset) => assetMatches(asset, options));\n}\n\n/**\n * Match on role ALONE, exactly. `selectEntryAsset` ORs role against a vacuously-true usage\n * check, so `{roles: ['hero-grade']}` there matches the entry's first asset whatever its role.\n * When you mean \"the asset whose role is exactly this\", use this.\n */\nexport function selectAssetByRole(\n entry: MarvinContentEntry,\n ...roles: string[]\n): Record<string, unknown> | undefined {\n for (const role of roles) {\n const found = entryAssets(entry).find((asset) => asString(assetField(asset, 'role')) === role);\n if (found) return found;\n }\n return undefined;\n}\n\n/** The list-item shorthand: `featuredAsset` is present on collection entries. */\nexport function selectFeaturedAsset(\n entry: MarvinContentEntry\n): Record<string, unknown> | undefined {\n const featured = asRecord((entry as { featuredAsset?: unknown }).featuredAsset);\n return Object.keys(featured).length > 0 ? featured : undefined;\n}\n\nexport function selectImageAsset(\n entry: MarvinContentEntry,\n options: Omit<AssetSelectOptions, 'type' | 'types'> = {}\n): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, { ...options, type: 'image' }) ??\n selectEntryAsset(entry, { ...options, types: ['image'], allowUnroled: true })\n );\n}\n\nexport function selectIconAsset(entry: MarvinContentEntry): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n mimeType: 'image/svg+xml',\n allowUnroled: true,\n }) ??\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n allowUnroled: true,\n })\n );\n}\n\nexport function assetUrl(asset: unknown): string | undefined {\n return asString(assetField(asset, 'publicUrl'));\n}\n\nexport function assetAlt(asset: unknown, fallback?: string): string | undefined {\n return asString(assetField(asset, 'altText')) ?? fallback;\n}\n\nexport function resourceRole(resource: unknown): string | undefined {\n return asString(resourceField(resource, 'role'));\n}\n\nexport function isExternalHref(href: string): boolean {\n return /^(https?:|mailto:|tel:)/.test(href);\n}\n","/**\n * Site chrome: main nav, footer columns, legal strip, social links, inquiry link.\n *\n * All of it comes from ordinary Marvin collections, so a workspace can reorder its own\n * navigation without a deploy. Every piece has a static fallback for when the backend is down.\n */\n\nimport type { MarvinFetcher, HydratedEntry } from './fetch.js';\nimport {\n asRecord,\n asString,\n collectionRole,\n collectionSlugs,\n entryField,\n isExternalHref,\n} from './normalize.js';\nimport type { SiteLoader } from './site.js';\nimport type {\n ApiNavigationLink,\n ApiSite,\n ApiSiteChrome,\n ApiSocialLink,\n MarvinContentEntry,\n} from './types.js';\n\nexport type NavigationContext = 'main' | 'footer';\n\nexport type HrefContext = {\n /** Where this link is being rendered. */\n context: NavigationContext;\n /** The nav collection the entry was read from. */\n collectionSlug: string;\n slug: string;\n /** Every collection the entry belongs to, nav collections included. */\n collections: string[];\n /** The collection slugs treated as navigation, i.e. not route prefixes. */\n navCollections: Set<string>;\n};\n\nexport type ResolveHref = (entry: MarvinContentEntry, context: HrefContext) => string;\n\n/** A link authored in site code rather than in the CMS. */\nexport type NavigationLinkInput = {\n label: string;\n href: string;\n description?: string;\n role?: string;\n};\n\nexport type SocialLinkInput = NavigationLinkInput & { icon: string };\n\nexport type ChromeOptions = {\n /** Collection holding the main nav. Default `'main-navigation'`. */\n mainCollection?: string;\n /** Collection holding the footer nav. Default `'footer-navigation'`. */\n footerCollection?: string;\n /**\n * Collection holding the social links. Default `'social-links'`. When it exists, its entries\n * (label, href, optional `icon` field) are exactly the social links, in order, and the site\n * `social` map is left out; when it doesn't, the social map is used.\n */\n socialCollection?: string;\n /**\n * Route for a nav entry that carries no explicit `href`/`url`/`path` field.\n *\n * The default prefixes the entry's own (non-navigation) collection: an entry in\n * `workshop-reference` becomes `/workshop-reference/<slug>`, and an entry in no other\n * collection becomes `/<slug>`. Override when routes don't mirror collections.\n */\n resolveHref?: ResolveHref;\n /** Membership role that marks a footer link as legal (Terms, Privacy…). Default `'legal'`. */\n legalRole?: string;\n /** Footer links per column once split. Columns are only created above this count. */\n footerColumnThreshold?: number;\n fallback?: {\n mainNavigation?: NavigationLinkInput[];\n footerNavigation?: NavigationLinkInput[][];\n legalLinks?: NavigationLinkInput[];\n socialLinks?: SocialLinkInput[];\n inquiry?: NavigationLinkInput;\n };\n};\n\nexport function toNavigationLink(link: NavigationLinkInput): ApiNavigationLink {\n return {\n label: link.label,\n href: link.href,\n description: link.description,\n external: isExternalHref(link.href),\n role: link.role,\n };\n}\n\n/** Collection-prefixed routing: `/<owning-collection>/<slug>`, else `/<slug>`. */\nexport const defaultResolveHref: ResolveHref = (_entry, context) => {\n const owning = context.collections.find((slug) => !context.navCollections.has(slug));\n return owning ? `/${owning}/${context.slug}` : `/${context.slug}`;\n};\n\n/** A `{label, href}` object stored loose in site metadata (e.g. `metadata.inquiry`). */\nexport function metadataLink(value: unknown): ApiNavigationLink | undefined {\n const link = asRecord(value);\n const label = asString(link.label);\n const href = asString(link.href);\n if (!label || !href) return undefined;\n\n return {\n label,\n href,\n description: asString(link.description) ?? asString(link.subject),\n external: isExternalHref(href),\n };\n}\n\n/** Title-case a social key: `x_handle` → `X Handle`, `email` → `Email`. */\nfunction socialLabel(key: string): string {\n return key.replace(/[-_]/g, ' ').replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nexport function socialLinkFromKey(key: string, href: string): ApiSocialLink {\n return {\n label: socialLabel(key),\n href,\n icon: key,\n external: isExternalHref(href),\n };\n}\n\n/**\n * Icon key for a social link with no `icon` field: `mailto:` → `email`, else the network's\n * name from the host (`www.instagram.com` → `instagram`, `x.com` → `x`), else the label.\n */\nexport function socialIconFor(href: string, label: string): string {\n if (href.startsWith('mailto:')) return 'email';\n try {\n const host = new URL(href).hostname.replace(/^(www|m)\\./, '');\n return host.split('.')[0] ?? label.toLowerCase();\n } catch {\n return label.toLowerCase();\n }\n}\n\nexport type ChromeLoader = {\n get(): Promise<ApiSiteChrome>;\n reset(): void;\n};\n\nexport function createChromeLoader(\n fetcher: MarvinFetcher,\n siteLoader: SiteLoader,\n options: ChromeOptions = {}\n): ChromeLoader {\n const mainCollection = options.mainCollection ?? 'main-navigation';\n const footerCollection = options.footerCollection ?? 'footer-navigation';\n const socialCollection = options.socialCollection ?? 'social-links';\n const legalRole = options.legalRole ?? 'legal';\n const footerColumnThreshold = options.footerColumnThreshold ?? 4;\n const resolveHref = options.resolveHref ?? defaultResolveHref;\n const navCollections = new Set([mainCollection, footerCollection, socialCollection]);\n const fallback = options.fallback ?? {};\n\n let promise: Promise<ApiSiteChrome> | null = null;\n\n function entryToLink(\n entry: HydratedEntry,\n collectionSlug: string,\n context: NavigationContext\n ): ApiNavigationLink {\n const slug = entry.slug ?? '';\n const href =\n asString(entryField(entry, 'href')) ??\n asString(entryField(entry, 'url')) ??\n asString(entryField(entry, 'path')) ??\n resolveHref(entry, {\n context,\n collectionSlug,\n slug,\n collections: collectionSlugs(entry),\n navCollections,\n });\n const label =\n asString(entryField(entry, 'label')) ??\n asString(asRecord((entry as { metadataJson?: unknown }).metadataJson).label) ??\n asString(entry.title) ??\n href;\n\n return {\n label,\n href,\n description:\n asString(entry.summary) ?? asString((entry as { description?: unknown }).description),\n external: isExternalHref(href),\n role: collectionRole(entry, collectionSlug),\n };\n }\n\n async function navigationCollection(\n slug: string,\n context: NavigationContext,\n staticLinks: ApiNavigationLink[]\n ): Promise<{ links: ApiNavigationLink[]; entries: HydratedEntry[]; fromBackend: boolean }> {\n if (!fetcher.backend.hasBackend()) return { links: staticLinks, entries: [], fromBackend: false };\n\n // Hydrated: a nav entry's `href`/`label` overrides are schema fields, absent from list items.\n const entries = await fetcher.hydratedCollectionEntries(slug);\n // An existing collection that is empty means \"show nothing\"; the static links stand in only\n // when Marvin has no such collection or can't be reached.\n if (entries.length === 0) {\n const exists = (await fetcher.collection(slug)) !== null;\n return { links: exists ? [] : staticLinks, entries, fromBackend: exists };\n }\n\n const links = entries.map((entry) => entryToLink(entry, slug, context));\n return { links, entries, fromBackend: true };\n }\n\n function groupFooterLinks(links: ApiNavigationLink[]): ApiNavigationLink[][] {\n if (links.length <= footerColumnThreshold) return [links];\n\n const midpoint = Math.ceil(links.length / 2);\n return [links.slice(0, midpoint), links.slice(midpoint)];\n }\n\n /** The social-links collection as links, or null when Marvin has no such collection. */\n async function socialEntries(): Promise<ApiSocialLink[] | null> {\n const social = await navigationCollection(socialCollection, 'footer', []);\n if (!social.fromBackend) return null;\n\n return social.links.map((link, index) => ({\n ...link,\n icon:\n asString(entryField(social.entries[index], 'icon')) ?? socialIconFor(link.href, link.label),\n }));\n }\n\n /** The site `social` map plus the contact email; the static social links when both are empty. */\n function siteSocialLinks(site: ApiSite): ApiSocialLink[] {\n const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));\n if (site.email && !links.some((link) => link.icon === 'email')) {\n links.push(socialLinkFromKey('email', `mailto:${site.email}`));\n }\n if (links.length > 0) return links;\n\n return (fallback.socialLinks ?? []).map((link) => ({\n ...toNavigationLink(link),\n icon: link.icon,\n }));\n }\n\n async function load(): Promise<ApiSiteChrome> {\n const site = await siteLoader.get();\n const staticMain = (fallback.mainNavigation ?? []).map(toNavigationLink);\n const staticFooter = (fallback.footerNavigation ?? []).map((group) =>\n group.map(toNavigationLink)\n );\n\n const main = await navigationCollection(mainCollection, 'main', staticMain);\n const footer = await navigationCollection(footerCollection, 'footer', staticFooter.flat());\n\n const social = await socialEntries();\n\n // Split footer links by membership role: legal → the legal strip; everything else → columns.\n const columnLinks = footer.links.filter((link) => link.role !== legalRole);\n const staticLegal = (fallback.legalLinks ?? []).map(toNavigationLink);\n\n return {\n site,\n mainNavigation: main.links,\n footerNavigation: footer.fromBackend ? groupFooterLinks(columnLinks) : staticFooter,\n legalLinks: footer.fromBackend\n ? footer.links.filter((link) => link.role === legalRole)\n : staticLegal,\n socialLinks: social ?? siteSocialLinks(site),\n inquiry:\n metadataLink(site.metadata.inquiry) ??\n (fallback.inquiry ? toNavigationLink(fallback.inquiry) : undefined),\n };\n }\n\n return {\n get() {\n promise ??= load();\n return promise;\n },\n reset() {\n promise = null;\n },\n };\n}\n","/**\n * Every call into Marvin, wrapped the same way: skip when there's no backend, catch, remember\n * network failures for the latch, warn once, and return an empty value the caller can fall\n * back from. Nothing here throws.\n */\n\nimport type {\n Collection,\n CollectionEntry,\n Entry,\n MarvinAsset,\n MarvinEntry,\n MarvinResource,\n MarvinSite,\n PublishedCollectionSummary,\n Workspace,\n} from '@inneropen/marvin-sdk';\nimport { errorMessage, type MarvinBackend } from './client.js';\nimport type { MarvinContentEntry } from './types.js';\n\n/** An entry that carries `data_json` — i.e. schema fields are readable. */\nexport type HydratedEntry = MarvinEntry | Entry;\n\nexport type MarvinFetcher = ReturnType<typeof createFetcher>;\n\n/**\n * `collections.entries` as SDK 4.2+ declares it: `{ expand: 'full' }` asks the server for full\n * entries (`?expand=full`) and returns them as SDK `Entry` objects. Older SDKs take only the slug\n * and ignore the option, so the same call works with any SDK in the peer range; their list items\n * are then hydrated one by one.\n */\ntype ExpandingCollectionEntries = (\n slug: string,\n options?: { expand?: 'full' }\n) => Promise<MarvinContentEntry[]>;\n\n/**\n * True for a full entry (`PublishedEntryRead`), false for a list item (`PublishedEntryListItem`).\n *\n * `data` doesn't tell them apart: list items carry it too now. What differs is attachments. A full\n * read has `assets[]` and `resources[]` (placement objects, always present, possibly empty); a list\n * item has only `assetSlugs` / `resourceSlugs` and a `featuredAsset`. The SDK's `Entry` wrapper\n * only ever comes from the single-entry read.\n */\nexport function isFullEntry(item: unknown): boolean {\n if (!item || typeof item !== 'object') return false;\n if (typeof (item as { field?: unknown }).field === 'function') return true;\n const raw = item as Record<string, unknown>;\n if ('assetSlugs' in raw || 'resourceSlugs' in raw) return false;\n return Array.isArray(raw.assets) || Array.isArray(raw.resources);\n}\n\n/** A failed hydration read is retried this many times in total, backing off from this base. */\nexport const HYDRATE_ATTEMPTS = 3;\nexport const HYDRATE_BACKOFF_MS = 500;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `Promise.all(items.map(run))` with at most `limit` calls in flight. Order is preserved. */\nasync function mapWithConcurrency<T, R>(\n items: T[],\n limit: number,\n run: (item: T) => Promise<R>\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n\n async function worker(): Promise<void> {\n while (next < items.length) {\n const index = next++;\n results[index] = await run(items[index]);\n }\n }\n\n const workers = Array.from({ length: Math.min(limit, items.length) }, worker);\n await Promise.all(workers);\n return results;\n}\n\nexport function createFetcher(backend: MarvinBackend) {\n async function guarded<T>(label: string, empty: T, run: () => Promise<T>): Promise<T> {\n if (!backend.hasBackend()) return empty;\n try {\n return await run();\n } catch (error) {\n backend.remember(error);\n backend.warn(`${label} unavailable: ${errorMessage(error)}`);\n return empty;\n }\n }\n\n async function entry(slug: string): Promise<Entry | null> {\n return guarded(`Entry \"${slug}\"`, null, () => backend.client().entry(slug));\n }\n\n /**\n * One entry read that survives a transient failure.\n *\n * `entry()` can't be used here: `guarded` swallows the error and trips the latch, and a\n * tripped latch turns every remaining item into a silent null. So retry with backoff,\n * re-opening the latch first in case a sibling read tripped it, and only remember the\n * failure — and warn — once the attempts are spent.\n */\n async function entryForHydration(slug: string): Promise<Entry | null> {\n if (!backend.hasBackend()) return null;\n\n let failure: unknown;\n for (let attempt = 1; attempt <= HYDRATE_ATTEMPTS; attempt++) {\n if (attempt > 1) {\n await sleep(HYDRATE_BACKOFF_MS * 2 ** (attempt - 2));\n if (backend.isLatched()) backend.clearLatch();\n }\n try {\n return await backend.client().entry(slug);\n } catch (error) {\n failure = error;\n }\n }\n\n backend.remember(failure);\n backend.warn(\n `Entry \"${slug}\" dropped after ${HYDRATE_ATTEMPTS} attempts: ${errorMessage(failure)}`\n );\n return null;\n }\n\n /**\n * Turn list items into full entries.\n *\n * The collection endpoint returns `PublishedEntryListItem`: core fields, `metadata_json` and\n * (on current servers) `data`, but only asset/resource *slugs* — no `assets[]` placements (so\n * no roles: no `hero-grade` image) and no `resources[]`. `PublishedEntryRead` (the single-entry\n * endpoint) has all of it. Only items that are already full entries ({@link isFullEntry}) are\n * passed through; everything else is re-read, even when it carries `data`. A list fetched with\n * `expand=full` ({@link expandedCollectionEntries}) is all full entries, so nothing is re-read.\n *\n * Reads run at most `config.hydrateConcurrency` at a time. Unbounded, a 280-entry collection\n * fired 280 requests at once, most of which timed out and were dropped — and the first\n * timeout latched the backend off for the rest of the build.\n */\n /**\n * A collection's entries as full entries in one request, where the server and SDK support\n * `expand=full` (Marvin core with expand, `@inneropen/marvin-sdk` 4.2+). Otherwise this is\n * `collectionEntries`: list items, which {@link hydrate} reads one at a time.\n */\n function expandedCollectionEntries(slug: string): Promise<MarvinContentEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () => {\n const collections = backend.client().collections;\n const entries = collections.entries as ExpandingCollectionEntries;\n return entries.call(collections, slug, { expand: 'full' });\n });\n }\n\n async function hydrate(entries: MarvinContentEntry[]): Promise<HydratedEntry[]> {\n const hydrated = await mapWithConcurrency(\n entries,\n backend.config.hydrateConcurrency,\n async (item) => {\n if (isFullEntry(item)) return item as HydratedEntry;\n return entryForHydration(item.slug ?? '');\n }\n );\n\n return hydrated.filter((item): item is HydratedEntry => Boolean(item));\n }\n\n return {\n backend,\n\n collections(): Promise<PublishedCollectionSummary[]> {\n return guarded('Collections', [], () => backend.client().collections.list());\n },\n\n async collection(slug: string): Promise<Collection | null> {\n return guarded(`Collection \"${slug}\"`, null, async () => {\n const found = await backend.client().collections.get(slug);\n return Array.isArray(found) ? null : found;\n });\n },\n\n collectionEntries(slug: string): Promise<CollectionEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () =>\n backend.client().collections.entries(slug)\n );\n },\n\n /** Try each collection slug in order; return the first that yields entries. */\n async collectionEntriesFallback(slugs: string[]): Promise<CollectionEntry[]> {\n for (const slug of slugs) {\n const entries = await this.collectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n expandedCollectionEntries,\n\n /** Full entries: one `expand=full` request, or per-item reads where that isn't supported. */\n async hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]> {\n return hydrate(await expandedCollectionEntries(slug));\n },\n\n async hydratedCollectionEntriesFallback(slugs: string[]): Promise<HydratedEntry[]> {\n for (const slug of slugs) {\n const entries = await this.hydratedCollectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n hydrate,\n entry,\n\n site(): Promise<MarvinSite | null> {\n return guarded('Site', null, () => backend.client().getSite());\n },\n\n workspace(): Promise<Workspace | null> {\n return guarded('Workspace', null, () => backend.client().getWorkspace());\n },\n\n assets(type?: string): Promise<MarvinAsset[]> {\n return guarded('Assets', [], () => backend.client().assets.list({ type }));\n },\n\n asset(slugOrId: string): Promise<MarvinAsset | null> {\n return guarded(`Asset \"${slugOrId}\"`, null, () => backend.client().assets.get(slugOrId));\n },\n\n resources(resourceType?: string): Promise<MarvinResource[]> {\n return guarded('Resources', [], () => backend.client().resources.list({ resourceType }));\n },\n };\n}\n","import { Marked, type MarkedOptions, type Tokens } from 'marked';\nimport { findEmbed, type MarvinEmbeds } from './embeds.js';\n\nexport type MarkdownOptions = MarkedOptions;\n\n/** Per-call options for a {@link MarkdownRenderer}. */\nexport type RenderMarkdownOptions = {\n /**\n * The entry's `embeds` map. A paragraph that is exactly a URL with an entry here renders as\n * that embed's Marvin-built HTML instead of a link.\n */\n embeds?: MarvinEmbeds | null;\n};\n\nexport type MarkdownRenderer = (\n markdown: string | string[] | null | undefined,\n options?: RenderMarkdownOptions\n) => Promise<string>;\n\n// Per-call data rides on marked's parse options: marked merges them over the instance defaults and\n// hands the merged object to the renderer as `this.options` for that parse only. A symbol key\n// keeps it clear of marked's own option names.\nconst EMBEDS = Symbol('marvin.embeds');\ntype ParseOptionsWithEmbeds = MarkedOptions & { [EMBEDS]?: MarvinEmbeds | null };\n\n// One run of non-space characters starting with a scheme. `<url>`, `[text](url)` and a URL inside\n// a sentence all fail this, so they stay links.\nconst BARE_URL = /^https?:\\/\\/\\S+$/i;\n\n/**\n * The embed for a paragraph that is exactly a URL with an `embeds` entry, else `undefined`.\n * Exported for tests; the renderer below is the only caller.\n */\nexport function paragraphEmbedHtml(\n token: Pick<Tokens.Paragraph, 'text'>,\n embeds: MarvinEmbeds | null | undefined\n): string | undefined {\n if (!embeds) return undefined;\n const text = token.text.trim();\n if (!BARE_URL.test(text)) return undefined;\n const html = findEmbed(embeds, text)?.html;\n return typeof html === 'string' && html.trim() ? html : undefined;\n}\n\n/**\n * A markdown renderer with its own `marked` instance — options are per-site, not global, so two\n * `createMarvinContent()` instances in one process can't clobber each other's settings.\n *\n * `breaks: false` is the default because CMS body copy is authored as prose: a soft-wrapped\n * paragraph should render as one paragraph. Where authored line breaks must survive (a landing\n * intro, an address block), pass `breaks: true` or pre-convert with a trailing double space.\n *\n * Pass the entry's `embeds` to turn a bare provider URL on its own line into its player:\n *\n * ```ts\n * await renderMarkdown(body, { embeds: entryEmbeds(entry) });\n * ```\n */\nexport function createMarkdownRenderer(options: MarkdownOptions = {}): MarkdownRenderer {\n const marked = new Marked();\n marked.setOptions({ gfm: true, breaks: false, ...options });\n marked.use({\n renderer: {\n paragraph(token) {\n const embeds = (this.options as ParseOptionsWithEmbeds)[EMBEDS];\n const html = paragraphEmbedHtml(token, embeds);\n // `false` hands the token back to marked's default paragraph renderer.\n return html === undefined ? false : `${html}\\n`;\n },\n },\n });\n\n return async (markdown, renderOptions = {}) => {\n const source = Array.isArray(markdown) ? markdown.join('\\n\\n') : (markdown ?? '');\n if (!source) return '';\n const parseOptions: ParseOptionsWithEmbeds = { [EMBEDS]: renderOptions.embeds ?? null };\n return await marked.parse(source, parseOptions);\n };\n}\n\n/** Turn single newlines into hard breaks, leaving paragraph breaks alone. */\nexport function preserveSoftBreaks(source: string): string {\n return source.replace(/(?<!\\n)\\n(?!\\n)/g, ' \\n');\n}\n","/**\n * Media embeds: the published shapes Marvin hands a site, read tolerantly.\n *\n * Marvin resolves a pasted provider link (YouTube, Vimeo, Spotify, …) on the server and publishes\n * the result next to the entry as `entry.embeds`, keyed by the URL exactly as the author wrote it.\n * Stored markdown is never rewritten: a site that knows about embeds swaps the link for the\n * player; an older site just shows the link.\n *\n * These types are declared here rather than imported from `@inneropen/marvin-sdk` so the package\n * works with SDKs that predate embeds. Kind and status are open-ended so a newer server value\n * doesn't break an older site's types.\n */\n\nimport { asRecord, asString } from './normalize.js';\n\nexport type MarvinEmbedKind = 'video' | 'audio' | 'podcast' | 'playlist';\nexport type MarvinEmbedStatus = 'ok' | 'link' | 'unavailable';\nexport type MarvinEmbedMode = 'direct' | 'click_to_load';\n\n/**\n * A Marvin-built iframe. Every value comes from Marvin's provider registry. Exactly one of\n * `aspectRatio` / `height` is set; the other is `null`.\n */\nexport type MarvinEmbedIframe = {\n src: string;\n title: string;\n allow?: string | null;\n sandbox?: string | null;\n referrerpolicy?: string | null;\n /** CSS aspect ratio for video players, e.g. `\"16/9\"` (`\"9/16\"` for Shorts); `null` otherwise. */\n aspectRatio?: string | null;\n /** Fixed pixel height for audio / podcast / playlist players; `null` for video. */\n height?: number | null;\n};\n\n/** The plain link every embed falls back to. */\nexport type MarvinEmbedLink = {\n href: string;\n title: string;\n providerName: string;\n};\n\n/** Optional fields arrive as `null` rather than omitted; every reader here treats both alike. */\nexport type MarvinEmbed = {\n /** The URL exactly as written in the content. */\n url: string;\n canonicalUrl?: string | null;\n /** Provider key, e.g. `youtube`. */\n provider: string;\n /** Display name, e.g. `YouTube`. */\n providerName: string;\n kind: MarvinEmbedKind | (string & {});\n /** `ok` has an iframe; `link` and `unavailable` render as a link card. */\n status: MarvinEmbedStatus | (string & {});\n title?: string | null;\n authorName?: string | null;\n thumbnailUrl?: string | null;\n iframe?: MarvinEmbedIframe | null;\n link: MarvinEmbedLink;\n /** Marvin-built HTML for the site's embed mode (an iframe, a click-to-load facade, or a link). */\n html: string;\n};\n\n/** `entry.embeds`: every resolved embed on the entry, keyed by the URL as written. */\nexport type MarvinEmbeds = Record<string, MarvinEmbed>;\n\n/**\n * `site.embeds` (next to `site.seo`): the site's embed privacy mode, consent text (`{provider}`\n * already substituted per embed in the published `html`) and the frame sources a CSP must allow.\n */\nexport type MarvinSiteEmbeds = {\n mode: MarvinEmbedMode;\n consentText: string;\n /** Origins/hosts the site's embeds may frame — feed these into a `frame-src` directive. */\n frameSources: string[];\n};\n\nconst EMBED_MODES = new Set<string>(['direct', 'click_to_load']);\n\nfunction readEmbedsField(entry: unknown): unknown {\n if (!entry || typeof entry !== 'object') return undefined;\n const direct = (entry as { embeds?: unknown }).embeds;\n if (direct !== undefined) return direct;\n // The SDK's `Entry` class keeps the payload private; its `toJSON()` hands it back.\n const toJSON = (entry as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === 'function') {\n return asRecord(toJSON.call(entry)).embeds;\n }\n return undefined;\n}\n\n/**\n * The entry's `embeds` map, or `undefined` when it has none (an older server, or no embeds).\n * Values that aren't objects carrying a URL are dropped.\n */\nexport function entryEmbeds(entry: unknown): MarvinEmbeds | undefined {\n const raw = readEmbedsField(entry);\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;\n\n const embeds: MarvinEmbeds = {};\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n embeds[key] = value as MarvinEmbed;\n }\n }\n return Object.keys(embeds).length > 0 ? embeds : undefined;\n}\n\n/** The embed for `url` as written, tolerating surrounding whitespace. */\nexport function findEmbed(\n embeds: MarvinEmbeds | null | undefined,\n url: string | null | undefined\n): MarvinEmbed | undefined {\n if (!embeds || !url) return undefined;\n const key = url.trim();\n if (!key) return undefined;\n return Object.prototype.hasOwnProperty.call(embeds, key) ? embeds[key] : undefined;\n}\n\n/** Normalize `site.embeds`; `undefined` when the server doesn't send it. */\nexport function siteEmbeds(raw: unknown): MarvinSiteEmbeds | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const value = asRecord(raw);\n const mode = asString(value.mode);\n const frameSources = Array.isArray(value.frameSources)\n ? value.frameSources.filter(\n (source): source is string => typeof source === 'string' && source.trim().length > 0\n )\n : [];\n\n return {\n // Unknown or missing mode → the privacy-preserving default.\n mode: mode && EMBED_MODES.has(mode) ? (mode as MarvinEmbedMode) : 'click_to_load',\n consentText: asString(value.consentText) ?? '',\n frameSources,\n };\n}\n","const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\n/**\n * Format a date value for display as \"Mon DD, YYYY\".\n *\n * ISO dates/timestamps (e.g. a raw `publishedAt` like \"2026-07-21T21:22:42.045708Z\") are read in\n * UTC — via the date part only, so there's no off-by-one day shift from the viewer's timezone.\n * Values that are already human-readable (e.g. a hand-entered \"Jul 02, 2024\") pass through\n * unchanged, and empty input returns `undefined` so callers can omit the field.\n */\nexport function formatDisplayDate(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(trimmed);\n if (iso) {\n const [, year, month, day] = iso;\n const name = MONTHS[Number(month) - 1];\n if (name) return `${name} ${day}, ${year}`;\n }\n\n return trimmed;\n}\n\n/**\n * Pick `count` items deterministically from `items`, seeded by `pageSlug`.\n *\n * Same page → same selection on every build, different pages → different selections. Use for\n * rotating pull-quotes, value bands, related links: variety across the site without the\n * hydration mismatch a random pick would cause.\n */\nexport function selectValuesForPage<T>(items: T[], pageSlug: string, count = 4): T[] {\n const seeded = [...items];\n let seed = 0;\n\n for (const character of pageSlug) {\n seed = (seed * 31 + character.charCodeAt(0)) >>> 0;\n }\n\n for (let index = seeded.length - 1; index > 0; index -= 1) {\n seed = (seed * 1664525 + 1013904223) >>> 0;\n\n const target = seed % (index + 1);\n\n [seeded[index], seeded[target]] = [seeded[target], seeded[index]];\n }\n\n return seeded.slice(0, count);\n}\n","/**\n * The field accessor handed to a repository's `transform`.\n *\n * Every reader defaults to the documented precedence — `data_json` first, `metadata_json`\n * second — so a transform reads like a field list instead of a pile of coalescing operators.\n */\n\nimport { entryEmbeds, findEmbed, type MarvinEmbed } from './embeds.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport { preserveSoftBreaks } from './markdown.js';\nimport {\n asNumber,\n asRecord,\n asString,\n asStringArray,\n assetAlt,\n assetField,\n assetUrl,\n collectionRole,\n collectionSlugs,\n entryAssets,\n entryData,\n entryMetadata,\n entryResources,\n field,\n focalPoint,\n resourceField,\n selectAssetByRole,\n selectFeaturedAsset,\n selectIconAsset,\n selectImageAsset,\n} from './normalize.js';\nimport { formatDisplayDate } from './format.js';\nimport type {\n ApiImage,\n ApiResourceLink,\n AssetSelectOptions,\n MarvinContentEntry,\n} from './types.js';\n\nexport type ImageFieldOptions = Omit<AssetSelectOptions, 'type' | 'types'> & {\n /**\n * Exact roles to prefer, in order, before the looser role/usage match. Use for derived\n * variants — e.g. `['hero-grade']` to prefer a colour-graded hero over the raw upload.\n */\n preferRoles?: string[];\n /** `metadata_json` key holding a hand-authored `{src, alt, focalPoint}`. */\n metadataKey?: string | false;\n /** Alt text when the asset carries none. Defaults to the entry title. */\n alt?: string;\n /** Fall back to the list item's `featuredAsset`. Default `true`. */\n fallbackToFeatured?: boolean;\n};\n\nexport type IconFieldOptions = {\n roles?: string[];\n /** Fall back to the list item's `featuredAsset` when no icon asset is attached. */\n fallbackToFeatured?: boolean;\n};\n\nexport type MarkdownFieldOptions = {\n /** Keep authored single newlines as hard breaks. Default `false`. */\n softBreaks?: boolean;\n /**\n * Render a bare provider URL on its own line as its player, using the entry's `embeds`.\n * Default `true`; `false` keeps every URL a plain link.\n */\n embeds?: boolean;\n};\n\nexport type ResourceFieldOptions = {\n /** Keep only these resource types, e.g. `['material', 'construction']`. */\n types?: string[];\n /** Keep only resources attached with this role. */\n role?: string;\n /** Build an href from the resource slug. */\n href?: (slug: string) => string;\n};\n\nconst DEFAULT_IMAGE_ROLES = ['hero', 'featured', 'card'];\n\nexport type FieldAccessor = ReturnType<typeof createFieldAccessor>;\n\nexport type FieldAccessorContext = {\n renderMarkdown: MarkdownRenderer;\n /** The route this entry resolves to, if the repository was given an `href` builder. */\n href?: string;\n /** The collection the entry was loaded from, when it came from one. */\n collection?: string;\n};\n\nexport function createFieldAccessor(\n entry: MarvinContentEntry,\n context: FieldAccessorContext\n) {\n const title = asString((entry as { title?: unknown }).title);\n const embeds = entryEmbeds(entry);\n\n const accessor = {\n /** The entry itself, for anything the accessor doesn't cover. */\n entry,\n /** The route this entry resolves to (from the repository's `href` option). */\n href: context.href,\n collection: context.collection,\n\n /** `data_json` → `metadata_json`, untyped. */\n raw(key: string): unknown {\n return field(entry, key);\n },\n\n string(key: string): string | undefined {\n return asString(field(entry, key));\n },\n\n number(key: string): number | undefined {\n return asNumber(field(entry, key));\n },\n\n /** Booleans authored as strings (`\"true\"`, `\"1\"`, `\"yes\"`) read as booleans. */\n bool(key: string): boolean {\n const value = field(entry, key);\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n return ['true', '1', 'yes', 'on'].includes(value.trim().toLowerCase());\n }\n if (typeof value === 'number') return value !== 0;\n return Boolean(value);\n },\n\n /** A string array, tolerating a single string authored in place of a list. */\n list(key: string): string[] | undefined {\n const value = field(entry, key);\n const array = asStringArray(value);\n if (array) return array;\n const single = asString(value);\n return single ? [single] : undefined;\n },\n\n /**\n * An enum-guarded read: the value if it's in `allowed`, else `fallback`. Replaces the\n * per-field `normalizeStatus`/`normalizeCategory`/`normalizeTone` guards that every site\n * ends up writing.\n */\n oneOf<T extends string>(key: string, allowed: readonly T[] | Set<T>, fallback: T): T {\n const value = asString(field(entry, key));\n const set = allowed instanceof Set ? allowed : new Set<string>(allowed);\n return value && set.has(value as T) ? (value as T) : fallback;\n },\n\n /** A field read as a display date (\"Mon DD, YYYY\"); already-formatted values pass through. */\n date(key: string): string | undefined {\n return formatDisplayDate(asString(field(entry, key)));\n },\n\n /** The raw ISO publish timestamp, if any. */\n publishedAt(): string | undefined {\n const value = (entry as { publishedAt?: unknown }).publishedAt;\n return value != null ? String(value) : undefined;\n },\n\n /**\n * Render a markdown field to HTML. Falls back to the entry's `contentMarkdown` when the\n * named field is absent. Returns `undefined` when there is nothing to render, so the\n * caller can omit the property rather than emit an empty string.\n */\n async markdown(\n key = 'body',\n options: MarkdownFieldOptions = {}\n ): Promise<string | undefined> {\n const raw =\n field<string | string[]>(entry, key) ??\n (entry as { contentMarkdown?: string | string[] }).contentMarkdown;\n const source = Array.isArray(raw) ? raw.join('\\n\\n') : asString(raw);\n if (!source) return undefined;\n return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source, {\n embeds: options.embeds === false ? undefined : embeds,\n });\n },\n\n /**\n * The resolved embed for an `embed` field (its value is the provider URL), or `undefined`\n * when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from\n * `@inneropen/marvin-astro/components`, or output `embed.html`.\n */\n embed(key: string): MarvinEmbed | undefined {\n return findEmbed(embeds, asString(field(entry, key)));\n },\n\n /**\n * The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,\n * the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the\n * list item's `featuredAsset`.\n */\n image(options: ImageFieldOptions = {}): ApiImage | undefined {\n const {\n preferRoles,\n metadataKey = 'featuredImage',\n alt: altFallback,\n fallbackToFeatured = true,\n roles = DEFAULT_IMAGE_ROLES,\n usages = DEFAULT_IMAGE_ROLES,\n allowUnroled = true,\n ...rest\n } = options;\n const fallbackAlt = altFallback ?? title ?? '';\n\n if (metadataKey) {\n const authored = asRecord(entryMetadata(entry)[metadataKey]);\n const src = asString(authored.src);\n if (src) {\n return {\n src,\n alt: asString(authored.alt) ?? fallbackAlt,\n focalPoint: focalPoint(authored.focalPoint),\n };\n }\n }\n\n const asset =\n (preferRoles?.length ? selectAssetByRole(entry, ...preferRoles) : undefined) ??\n selectImageAsset(entry, { ...rest, roles, usages, allowUnroled }) ??\n (fallbackToFeatured ? selectFeaturedAsset(entry) : undefined);\n\n const src = assetUrl(asset);\n if (!src) return undefined;\n\n return {\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n };\n },\n\n /** URL of the entry's icon asset (`role: icon`, SVG preferred). */\n icon(options: IconFieldOptions = {}): string | undefined {\n const { roles = ['icon'], fallbackToFeatured = false } = options;\n const asset = selectAssetByRole(entry, ...roles) ?? selectIconAsset(entry);\n return (\n assetUrl(asset) ?? (fallbackToFeatured ? assetUrl(selectFeaturedAsset(entry)) : undefined)\n );\n },\n\n /** The raw asset placement matching `options` — when you need more than src/alt/focal. */\n asset(options: AssetSelectOptions = {}): Record<string, unknown> | undefined {\n return selectImageAsset(entry, options) ?? selectFeaturedAsset(entry);\n },\n\n /** The asset whose role is EXACTLY one of `roles`, in preference order. */\n assetByRole(...roles: string[]): Record<string, unknown> | undefined {\n return selectAssetByRole(entry, ...roles);\n },\n\n /** All asset placements on the entry. */\n assets(): Record<string, unknown>[] {\n return entryAssets(entry);\n },\n\n /** Every image asset resolved, filtered by role/usage — support/detail galleries. */\n images(options: ImageFieldOptions = {}): (ApiImage & { usage?: string })[] {\n const wanted = new Set([...(options.roles ?? []), ...(options.usages ?? [])]);\n const fallbackAlt = options.alt ?? title ?? '';\n const images: (ApiImage & { usage?: string })[] = [];\n\n for (const asset of entryAssets(entry)) {\n const usage = asString(assetField(asset, 'usage')) ?? asString(assetField(asset, 'role'));\n const src = assetUrl(asset);\n if (!src) continue;\n if (wanted.size > 0 && (!usage || !wanted.has(usage))) continue;\n\n images.push({\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n usage,\n });\n }\n\n return images;\n },\n\n /** Attached resources, normalized to name/type/role/href. */\n resources(options: ResourceFieldOptions = {}): ApiResourceLink[] {\n const types = options.types?.length ? new Set(options.types) : undefined;\n\n return entryResources(entry)\n .map((relationship) => {\n const nested = asRecord(relationship.resource);\n const source = Object.keys(nested).length > 0 ? nested : relationship;\n const slug = asString(resourceField(source, 'slug'));\n return {\n name: asString(resourceField(source, 'name')) ?? '',\n type: asString(resourceField(source, 'resourceType')) ?? '',\n role: asString(assetField(relationship, 'role')),\n slug,\n href: slug && options.href ? options.href(slug) : undefined,\n };\n })\n .filter((resource) => Boolean(resource.name))\n .filter((resource) => !types || types.has(resource.type))\n .filter((resource) => !options.role || resource.role === options.role);\n },\n\n /** The first attached resource matching `options`. */\n resource(options: ResourceFieldOptions = {}): ApiResourceLink | undefined {\n return accessor.resources(options)[0];\n },\n\n /** The `metadata_json` blob. */\n metadata(): Record<string, unknown> {\n return entryMetadata(entry);\n },\n\n /** The `data_json` blob. Empty on a list item that was not hydrated. */\n data(): Record<string, unknown> {\n return entryData(entry);\n },\n\n /** Slugs of every collection the entry belongs to. */\n collections(): string[] {\n return collectionSlugs(entry);\n },\n\n /** The entry's membership role within `collectionSlug`. */\n role(collectionSlug: string): string | undefined {\n return collectionRole(entry, collectionSlug);\n },\n };\n\n return accessor;\n}\n","/**\n * The repository factory.\n *\n * `getAll` / `getBySlug` / `getFeatured` is the same forty lines of try / fallback / memoize\n * around every content type on every site. Only the transform differs. This holds the forty\n * lines; the site writes the transform.\n */\n\nimport { errorMessage } from './client.js';\nimport { createFieldAccessor, type FieldAccessor } from './fields.js';\nimport { isFullEntry, type MarvinFetcher } from './fetch.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport type { MarvinContentEntry } from './types.js';\n\nexport type RepositoryOptions<T> = {\n /** Collection slug to load from. */\n collection?: string;\n /**\n * Collection slugs tried in order; the first one that yields entries wins. Lets a site work\n * against workspaces that named the same thing `bench-notes`, `journal`, or `blog`.\n */\n collections?: string[];\n /**\n * Fetch each list item as a full entry before transforming.\n *\n * The collection endpoint returns `PublishedEntryListItem`, which has no `assets[]` or\n * `resources[]` (only their slugs) and, on older servers, no `data_json`; the single-entry\n * endpoint returns `PublishedEntryRead`, which has all of it. So if the transform reads asset\n * roles, resources, or (on an older server) any schema field, this must be `true`. The list is\n * requested with `expand=full`, which returns full entries in the one request; a server or SDK\n * without it (SDK < 4.2) costs one more request per entry. `bySlug` always works from full\n * entries either way.\n */\n hydrate?: boolean;\n /** Build the resolved item from an entry. May be async (e.g. to render markdown). */\n transform: (entry: MarvinContentEntry, fields: FieldAccessor) => T | Promise<T>;\n /** Static data to serve when Marvin is unreachable or the collection is empty. */\n fallback?: () => T[] | Promise<T[]>;\n /** Applied to Marvin and fallback results alike, so ordering doesn't depend on the source. */\n sort?: (a: T, b: T) => number;\n /** Applied to Marvin and fallback results alike. */\n filter?: (item: T) => boolean;\n /** The route an entry resolves to; exposed to the transform as `fields.href`. */\n href?: (slug: string, entry: MarvinContentEntry) => string;\n /** How to read an item's slug for `bySlug`. Defaults to `item.slug`. */\n slugOf?: (item: T) => string | undefined;\n /** How to tell whether an item is featured. Defaults to `item.featured`. */\n isFeatured?: (item: T) => boolean;\n};\n\nexport type Repository<T> = {\n /** Every item, resolved once per process. */\n all(): Promise<T[]>;\n /**\n * One item by slug. Served from `all()` once that has loaded; otherwise a direct entry\n * fetch, falling back to a scan of `all()`.\n */\n bySlug(slug: string): Promise<T | undefined>;\n /** The first featured item, or the first item when none is marked. */\n featured(): Promise<T | undefined>;\n /** Every featured item, in `all()` order. */\n allFeatured(): Promise<T[]>;\n /** Drop the memoized results. Next call re-fetches. */\n reset(): void;\n};\n\nexport type RepositoryContext = {\n fetcher: MarvinFetcher;\n renderMarkdown: MarkdownRenderer;\n};\n\nfunction defaultSlugOf<T>(item: T): string | undefined {\n const slug = (item as { slug?: unknown }).slug;\n return typeof slug === 'string' ? slug : undefined;\n}\n\nfunction defaultIsFeatured<T>(item: T): boolean {\n return Boolean((item as { featured?: unknown }).featured);\n}\n\nexport function createRepository<T>(\n context: RepositoryContext,\n options: RepositoryOptions<T>\n): Repository<T> {\n const { fetcher, renderMarkdown } = context;\n const collections = options.collections ?? (options.collection ? [options.collection] : []);\n const slugOf = options.slugOf ?? defaultSlugOf;\n const isFeatured = options.isFeatured ?? defaultIsFeatured;\n\n /** The loaded list, plus the slugs whose items were built from full entries. */\n type Loaded = { items: T[]; fromFullEntry: Set<string> };\n let loadedPromise: Promise<Loaded> | null = null;\n let allPromise: Promise<T[]> | null = null;\n const bySlugCache = new Map<string, Promise<T | undefined>>();\n\n function arrange(items: T[]): T[] {\n const filtered = options.filter ? items.filter(options.filter) : items;\n return options.sort ? [...filtered].sort(options.sort) : filtered;\n }\n\n function transform(entry: MarvinContentEntry, collection?: string): Promise<T> {\n const slug = (entry as { slug?: string }).slug ?? '';\n const fields = createFieldAccessor(entry, {\n renderMarkdown,\n href: options.href?.(slug, entry),\n collection,\n });\n return Promise.resolve(options.transform(entry, fields));\n }\n\n async function useFallback(): Promise<Loaded> {\n return { items: arrange((await options.fallback?.()) ?? []), fromFullEntry: new Set() };\n }\n\n async function load(): Promise<Loaded> {\n if (collections.length > 0 && fetcher.backend.hasBackend()) {\n try {\n const entries = options.hydrate\n ? await fetcher.hydratedCollectionEntriesFallback(collections)\n : await fetcher.collectionEntriesFallback(collections);\n\n if (entries.length > 0) {\n const items = await Promise.all(entries.map((entry) => transform(entry)));\n const fromFullEntry = new Set<string>();\n items.forEach((item, index) => {\n const slug = slugOf(item);\n if (slug !== undefined && isFullEntry(entries[index])) fromFullEntry.add(slug);\n });\n return { items: arrange(items), fromFullEntry };\n }\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(\n `Collection \"${collections[0]}\" failed, using fallback: ${errorMessage(error)}`\n );\n }\n }\n\n return useFallback();\n }\n\n function findInList(items: T[], slug: string): T | undefined {\n return items.find((item) => slugOf(item) === slug);\n }\n\n async function loadBySlug(slug: string): Promise<T | undefined> {\n // Once a hydrated list has loaded, every slug in it is already transformed from a full entry —\n // a per-entry fetch would be a second request for the same content (~1.4s per detail page on\n // a 280-entry site). An item built from a bare list item is NOT served: it has no assets[] or\n // resources[], so a detail page would lose its role-picked images and resource links.\n let listed: T | undefined;\n if (loadedPromise) {\n const loaded = await loadedPromise;\n listed = findInList(loaded.items, slug);\n if (listed && loaded.fromFullEntry.has(slug)) return listed;\n }\n\n if (fetcher.backend.hasBackend()) {\n try {\n const entry = await fetcher.entry(slug);\n if (entry) return await transform(entry);\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(`Entry \"${slug}\" failed, using fallback: ${errorMessage(error)}`);\n }\n }\n\n // No backend, no such entry, or a transform that blew up: fall back to the resolved list,\n // which is either Marvin's or the static data — the caller doesn't need to care which.\n return listed ?? findInList(await all(), slug);\n }\n\n function all(): Promise<T[]> {\n if (!allPromise) {\n loadedPromise = load();\n allPromise = loadedPromise.then((loaded) => loaded.items);\n }\n return allPromise;\n }\n\n return {\n all,\n\n bySlug(slug: string): Promise<T | undefined> {\n let cached = bySlugCache.get(slug);\n if (!cached) {\n cached = loadBySlug(slug);\n bySlugCache.set(slug, cached);\n }\n return cached;\n },\n\n async allFeatured(): Promise<T[]> {\n return (await all()).filter(isFeatured);\n },\n\n async featured(): Promise<T | undefined> {\n const items = await all();\n return items.find(isFeatured) ?? items[0];\n },\n\n reset() {\n allPromise = null;\n loadedPromise = null;\n bySlugCache.clear();\n },\n };\n}\n","import { entryEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { createMarkdownRenderer, preserveSoftBreaks, type MarkdownRenderer } from './markdown.js';\nimport { asString, assetUrl, entryField, selectAssetByRole } from './normalize.js';\nimport type { SectionLanding } from './types.js';\n\nexport type SectionLandingOptions = {\n /** Asset role to use as the hero image. Default `'hero'`. */\n heroRole?: string;\n /** Field holding the intro copy. Default `'body'`, falling back to the entry summary. */\n bodyField?: string;\n};\n\n/**\n * Resolve a section-landing header from a Marvin `page` entry with the given slug.\n *\n * The intro is rendered with authored line breaks preserved — landing copy is written as a few\n * deliberate lines, not a soft-wrapped paragraph, and losing those breaks changes the layout.\n */\nexport async function loadSectionLanding(\n fetcher: MarvinFetcher,\n slug: string,\n renderMarkdown: MarkdownRenderer = createMarkdownRenderer(),\n options: SectionLandingOptions = {}\n): Promise<SectionLanding> {\n const entry = await fetcher.entry(slug);\n if (!entry) return {};\n\n const source =\n asString(entryField(entry, options.bodyField ?? 'body')) ?? asString(entry.summary);\n\n return {\n title: asString(entry.title),\n introHtml: source\n ? await renderMarkdown(preserveSoftBreaks(source), { embeds: entryEmbeds(entry) })\n : undefined,\n hero: assetUrl(selectAssetByRole(entry, options.heroRole ?? 'hero')),\n };\n}\n","/**\n * Site identity + SEO.\n *\n * Marvin's site configuration is optional almost everywhere; a template that had to guard every\n * field would be unreadable. This resolves it once into an `ApiSite` where title, description,\n * siteName, locale and timezone are always present, and where brand asset slugs have already\n * become URLs.\n */\n\nimport type { MarvinSite, SiteConfiguration } from '@inneropen/marvin-sdk';\nimport { siteEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { asRecord, asString, assetUrl } from './normalize.js';\nimport type { ApiSeo, ApiSite } from './types.js';\n\nexport type SiteOptions = {\n /**\n * Static identity to fall back to, field by field, when Marvin is unreachable or leaves a\n * value unset. Anything omitted here falls through to a neutral default.\n */\n fallback?: Partial<ApiSite> | (() => Partial<ApiSite>);\n /** Default locale when neither Marvin nor the fallback sets one. Default `'en-US'`. */\n defaultLocale?: string;\n /** Default timezone when neither Marvin nor the fallback sets one. Default `'UTC'`. */\n defaultTimezone?: string;\n};\n\n/**\n * Build resolved SEO from Marvin's raw SiteSeo block, applying identity fallbacks so title,\n * description and siteName are never empty. Absent → sensible defaults (robots `index,follow`;\n * og type `website`; large-image Twitter card).\n */\nexport function buildSeo(\n raw: Record<string, unknown> | undefined,\n identity: { title: string; description: string; canonicalUrl?: string; siteName: string }\n): ApiSeo {\n const seo = raw ?? {};\n const verification = asRecord(seo.verification);\n\n return {\n title: asString(seo.title) ?? identity.title,\n titleTemplate: asString(seo.titleTemplate),\n description: asString(seo.description) ?? identity.description,\n keywords: Array.isArray(seo.keywords)\n ? seo.keywords.filter((keyword): keyword is string => typeof keyword === 'string')\n : [],\n robots: asString(seo.robots) ?? 'index,follow',\n image: asString(seo.image),\n imageAlt: asString(seo.imageAlt),\n ogType: asString(seo.ogType) ?? 'website',\n siteName: asString(seo.siteName) ?? identity.siteName,\n twitterCard: asString(seo.twitterCard) ?? 'summary_large_image',\n twitterHandle: asString(seo.twitterHandle),\n twitterCreator: asString(seo.twitterCreator),\n fbAppId: asString(seo.fbAppId),\n themeColor: asString(seo.themeColor),\n publisher: asString(seo.publisher),\n canonicalUrl: identity.canonicalUrl,\n verification: {\n google: asString(verification.google),\n bing: asString(verification.bing),\n pinterest: asString(verification.pinterest),\n yandex: asString(verification.yandex),\n },\n };\n}\n\nfunction normalizeSocial(social: unknown): Record<string, string> {\n return Object.fromEntries(\n Object.entries(asRecord(social)).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0\n )\n );\n}\n\n/** The offline site: the caller's fallback, with neutral defaults under it. */\nexport function buildFallbackSite(options: SiteOptions = {}): ApiSite {\n const provided = typeof options.fallback === 'function' ? options.fallback() : options.fallback;\n const fallback = provided ?? {};\n const title = fallback.title ?? fallback.name ?? 'Untitled site';\n const description = fallback.description ?? '';\n\n return {\n ...fallback,\n name: fallback.name ?? title,\n title,\n description,\n locale: fallback.locale ?? options.defaultLocale ?? 'en-US',\n timezone: fallback.timezone ?? options.defaultTimezone ?? 'UTC',\n social: fallback.social ?? {},\n metadata: fallback.metadata ?? {},\n seo:\n fallback.seo ??\n buildSeo(undefined, { title, description, siteName: fallback.name ?? title }),\n };\n}\n\nfunction transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite {\n const config: SiteConfiguration = marvinSite.site;\n const metadata = asRecord(config.metadataJson);\n const title =\n asString(config.title) ?? asString(marvinSite.workspace?.name) ?? fallback.title;\n const description = asString(config.description) ?? fallback.description;\n\n return {\n workspaceSlug: asString(marvinSite.workspace?.slug),\n workspaceName: asString(marvinSite.workspace?.name),\n name: title,\n title,\n tagline: asString(config.tagline) ?? fallback.tagline,\n description,\n canonicalUrl: asString(config.canonicalUrl) ?? fallback.canonicalUrl,\n logo: asString(config.logo) ?? fallback.logo,\n favicon: asString(config.favicon) ?? fallback.favicon,\n locale: asString(config.locale) ?? fallback.locale,\n timezone: asString(config.timezone) ?? fallback.timezone,\n email: asString(config.contactEmail) ?? fallback.email,\n imprint: asString(metadata.imprint) ?? fallback.imprint,\n social: { ...fallback.social, ...normalizeSocial(config.social) },\n // Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older\n // SDKs drop the typed field but pass the blob through untouched, and workspaces configured\n // before the typed field existed still only have the blob.\n seo: buildSeo(asRecord((config as { seo?: unknown }).seo ?? metadata.seo), {\n title,\n description,\n canonicalUrl: asString(config.canonicalUrl),\n siteName: title,\n }),\n // Older servers and SDKs don't send it; `siteEmbeds` returns undefined then.\n embeds: siteEmbeds(\n (config as { embeds?: unknown }).embeds ?? (marvinSite as { embeds?: unknown }).embeds\n ),\n metadata,\n };\n}\n\nfunction looksLikeAssetSlug(value: string): boolean {\n return !/^https?:\\/\\//.test(value) && !value.startsWith('/') && !value.startsWith('data:');\n}\n\nexport type SiteLoader = {\n get(): Promise<ApiSite>;\n reset(): void;\n};\n\nexport function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions = {}): SiteLoader {\n const fallback = buildFallbackSite(options);\n let promise: Promise<ApiSite> | null = null;\n\n async function resolveAssetSlug(slug: string | undefined): Promise<string | undefined> {\n if (!slug) return undefined;\n return assetUrl(await fetcher.asset(slug));\n }\n\n // Resolve a value only when it looks like a bare asset slug; a URL/path/data URI (or a slug with\n // no matching asset) is returned unchanged.\n async function resolveMaybeSlug(value: string | undefined): Promise<string | undefined> {\n if (!value || !looksLikeAssetSlug(value)) return value;\n return (await resolveAssetSlug(value)) ?? value;\n }\n\n async function load(): Promise<ApiSite> {\n const marvinSite = await fetcher.site();\n if (!marvinSite) return fallback;\n\n const site = transformMarvinSite(marvinSite, fallback);\n\n // Brand-asset registry: resolve EVERY `brand.<name>` slug → URL in one pass. A site adds a\n // shared asset with one config line (`brand.paperFold = \"paper-fold\"`) plus an upload;\n // components then read `site.brand.paperFold`. No code change per asset.\n const brandPairs = Object.entries(asRecord(site.metadata.brand)).filter(\n (entry): entry is [string, string] =>\n typeof entry[1] === 'string' && entry[1].trim().length > 0\n );\n const resolved = await Promise.all(\n brandPairs.map(async ([name, slug]) => [name, await resolveAssetSlug(slug)] as const)\n );\n const brand: Record<string, string> = {};\n const brandBySlug = new Map<string, string>();\n for (const [index, [name, url]] of resolved.entries()) {\n if (!url) continue;\n brand[name] = url;\n brandBySlug.set(brandPairs[index][1], url);\n }\n\n // The og:image may be given as a bare asset slug too. It is usually one of the brand slugs\n // (a monogram, a logo), so check what was just resolved before fetching it again.\n const rawImage = site.seo.image;\n const ogImage =\n rawImage && looksLikeAssetSlug(rawImage)\n ? (brandBySlug.get(rawImage) ?? (await resolveAssetSlug(rawImage)))\n : undefined;\n\n // The top-level `site_logo` / `site_favicon` fields may be bare asset slugs too (like brand.*\n // and og:image) — resolve them so they can reference an uploaded asset by slug. The brand-map\n // aliases still win when present.\n const logo = brand.logo ?? (await resolveMaybeSlug(site.logo));\n const favicon = brand.favicon ?? (await resolveMaybeSlug(site.favicon));\n\n return {\n ...site,\n brand,\n // Convenience aliases for common callers; all of these also live in `site.brand`.\n logo,\n favicon,\n seal: brand.seal ?? site.seal,\n seo: { ...site.seo, image: ogImage ?? site.seo.image },\n };\n }\n\n return {\n get() {\n // Memoized: components like a per-card brand stamp render many instances per page, so\n // this keeps brand-slug resolution to once per process rather than once per render.\n promise ??= load();\n return promise;\n },\n reset() {\n promise = null;\n },\n };\n}\n","/**\n * `@inneropen/marvin-astro` — site integration for Marvin CMS.\n *\n * Sits above `@inneropen/marvin-sdk` (transport). This layer is the part every Marvin-backed site\n * rewrites: repositories with a static fallback, site chrome from collections, and payload\n * normalization. Astro components (forms, media embeds, entry-type renderers) live under\n * `@inneropen/marvin-astro/components`.\n *\n * ```ts\n * // src/lib/content.ts\n * export const marvin = createMarvinContent({ site: { fallback: staticSite } });\n *\n * export const posts = marvin.repository({\n * collections: ['bench-notes', 'journal', 'blog'],\n * hydrate: true,\n * href: (slug) => `/bench-notes/${slug}`,\n * fallback: () => staticPosts,\n * transform: async (entry, f) => ({\n * slug: entry.slug,\n * title: entry.title ?? 'Untitled',\n * noteNumber: f.string('noteNumber'),\n * bodyHtml: await f.markdown('body'),\n * image: f.image({ roles: ['hero', 'featured', 'card'] }),\n * }),\n * });\n * ```\n */\n\nimport { createBackend } from './client.js';\nimport { createChromeLoader, type ChromeOptions } from './chrome.js';\nimport { createFetcher } from './fetch.js';\nimport { createMarkdownRenderer } from './markdown.js';\nimport { createRepository, type Repository, type RepositoryOptions } from './repository.js';\nimport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nimport { createSiteLoader, type SiteOptions } from './site.js';\nimport type { MarvinAstroConfig } from './config.js';\nimport type { ApiSite, ApiSiteChrome, SectionLanding } from './types.js';\n\nexport type MarvinContentOptions = MarvinAstroConfig & {\n site?: SiteOptions;\n chrome?: ChromeOptions;\n};\n\nexport type MarvinContent = ReturnType<typeof createMarvinContent>;\n\n/**\n * Build the content layer for one Marvin workspace.\n *\n * Connection settings come from `MARVIN_API_URL`, `MARVIN_SITE_CLIENT_TOKEN` and\n * `MARVIN_WORKSPACE_SLUG` unless passed explicitly. Everything returned degrades to its static\n * fallback when the backend is unreachable, so a site still builds offline.\n */\nexport function createMarvinContent(options: MarvinContentOptions = {}) {\n const backend = createBackend(options);\n const fetcher = createFetcher(backend);\n const renderMarkdown = createMarkdownRenderer(backend.config.markdown);\n const siteLoader = createSiteLoader(fetcher, options.site);\n const chromeLoader = createChromeLoader(fetcher, siteLoader, options.chrome);\n const repositories: Repository<unknown>[] = [];\n\n return {\n /** The resolved configuration, with the token still in place — don't log it. */\n config: backend.config,\n /** Latch control and the raw SDK client. */\n backend,\n /** Guarded, never-throwing wrappers around every published endpoint. */\n fetch: fetcher,\n renderMarkdown,\n\n /** True when Marvin is configured and not currently latched out as unreachable. */\n hasBackend: () => backend.hasBackend(),\n\n /** Resolved site identity, SEO and brand assets. Memoized per process. */\n getSite(): Promise<ApiSite> {\n return siteLoader.get();\n },\n\n /** Navigation, footer, legal, social and inquiry links. Memoized per process. */\n getSiteChrome(): Promise<ApiSiteChrome> {\n return chromeLoader.get();\n },\n\n /** Title/intro/hero for an index page, driven by a `page` entry of the same slug. */\n getSectionLanding(slug: string, sectionOptions?: SectionLandingOptions): Promise<SectionLanding> {\n return loadSectionLanding(fetcher, slug, renderMarkdown, sectionOptions);\n },\n\n /** A collection-backed content repository: `all()` / `bySlug()` / `featured()`. */\n repository<T>(repositoryOptions: RepositoryOptions<T>): Repository<T> {\n const repository = createRepository<T>({ fetcher, renderMarkdown }, repositoryOptions);\n repositories.push(repository as Repository<unknown>);\n return repository;\n },\n\n /** Drop every memoized result and re-open the failure latch. */\n reset() {\n backend.clearLatch();\n siteLoader.reset();\n chromeLoader.reset();\n for (const repository of repositories) repository.reset();\n },\n };\n}\n\nexport {\n createBackend,\n errorMessage,\n isNetworkFailure,\n type MarvinBackend,\n} from './client.js';\nexport {\n DEFAULT_DEV_RETRY_MS,\n DEFAULT_HYDRATE_CONCURRENCY,\n ENV_KEYS,\n describeConfig,\n readEnv,\n resolveConfig,\n type MarvinAstroConfig,\n type MarvinLogger,\n type ResolvedConfig,\n} from './config.js';\nexport { createFetcher, isFullEntry, type HydratedEntry, type MarvinFetcher } from './fetch.js';\nexport {\n createFieldAccessor,\n type FieldAccessor,\n type IconFieldOptions,\n type ImageFieldOptions,\n type MarkdownFieldOptions,\n type ResourceFieldOptions,\n} from './fields.js';\nexport { formatDisplayDate, selectValuesForPage } from './format.js';\nexport {\n createMarkdownRenderer,\n preserveSoftBreaks,\n type MarkdownOptions,\n type MarkdownRenderer,\n type RenderMarkdownOptions,\n} from './markdown.js';\nexport { entryEmbeds, findEmbed, siteEmbeds } from './embeds.js';\nexport * from './normalize.js';\nexport {\n createRepository,\n type Repository,\n type RepositoryContext,\n type RepositoryOptions,\n} from './repository.js';\nexport {\n buildFallbackSite,\n buildSeo,\n createSiteLoader,\n type SiteLoader,\n type SiteOptions,\n} from './site.js';\nexport {\n createChromeLoader,\n defaultResolveHref,\n metadataLink,\n socialLinkFromKey,\n toNavigationLink,\n type ChromeLoader,\n type ChromeOptions,\n type HrefContext,\n type NavigationContext,\n type NavigationLinkInput,\n type ResolveHref,\n type SocialLinkInput,\n} from './chrome.js';\nexport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nexport type * from './types.js';\n"],"mappings":"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","names":["src"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/config.ts","../src/normalize.ts","../src/chrome.ts","../src/fetch.ts","../src/markdown.ts","../src/embeds.ts","../src/format.ts","../src/fields.ts","../src/repository.ts","../src/sections.ts","../src/site.ts","../src/index.ts"],"sourcesContent":["/**\n * SDK client construction and the network-failure latch.\n *\n * The latch exists because a static build asks for content once per path. When the backend is\n * down, that is N failed requests with N timeouts before the build gives up. The latch trips on\n * the first network failure and short-circuits the rest.\n *\n * Unlike the hand-rolled original it is *retryable*: it expires after `retryAfterMs` so a dev\n * server recovers on its own when the backend comes back, instead of serving stale static data\n * until someone restarts it. Production keeps the permanent latch — a build should fail fast and\n * consistently rather than half-succeed.\n */\n\nimport { createMarvinClient } from '@inneropen/marvin-sdk';\nimport type { MarvinClient } from '@inneropen/marvin-sdk';\nimport {\n describeConfig,\n resolveConfig,\n type MarvinAstroConfig,\n type ResolvedConfig,\n} from './config.js';\n\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\n/**\n * A network failure means \"the backend is unreachable\", not \"this request was bad\". Only the\n * former should trip the latch — a 404 on one entry says nothing about the next one.\n */\nexport function isNetworkFailure(error: unknown): boolean {\n const message = errorMessage(error).toLowerCase();\n return (\n message.includes('network error') ||\n message.includes('fetch failed') ||\n message.includes('econnrefused') ||\n message.includes('enotfound') ||\n message.includes('etimedout')\n );\n}\n\nexport type MarvinBackend = {\n readonly config: ResolvedConfig;\n /** True when Marvin is configured AND not currently latched out. */\n hasBackend(): boolean;\n /** The SDK client. Throws when Marvin is not configured. */\n client(): MarvinClient;\n /** Trip the latch if this error indicates the backend is unreachable. */\n remember(error: unknown): void;\n /** True while the latch is closed (backend considered unreachable). */\n isLatched(): boolean;\n /** Manually re-open the latch, e.g. after a deploy. */\n clearLatch(): void;\n warn(message: string): void;\n};\n\nexport function createBackend(options: MarvinAstroConfig = {}): MarvinBackend {\n const config = resolveConfig(options);\n let client: MarvinClient | null = null;\n let latchedAt: number | null = null;\n let envLogged = false;\n\n function isLatched(): boolean {\n if (latchedAt === null) return false;\n if (config.retryAfterMs === Number.POSITIVE_INFINITY) return true;\n if (config.now() - latchedAt < config.retryAfterMs) return true;\n\n // Expired — re-open and let the next call find out whether the backend is back.\n latchedAt = null;\n return false;\n }\n\n function hasBackend(): boolean {\n if (config.debug && !envLogged) {\n envLogged = true;\n config.logger.log('[marvin-astro] env check:', describeConfig(config));\n }\n if (!config.configured) return false;\n return !isLatched();\n }\n\n return {\n config,\n hasBackend,\n isLatched,\n clearLatch() {\n latchedAt = null;\n },\n remember(error: unknown) {\n if (isNetworkFailure(error)) latchedAt = config.now();\n },\n warn(message: string) {\n config.logger.warn(`[marvin-astro] ${message}`);\n },\n client(): MarvinClient {\n if (!config.configured) {\n throw new Error(\n '[marvin-astro] Marvin is not configured. Set MARVIN_API_URL, ' +\n 'MARVIN_SITE_CLIENT_TOKEN and MARVIN_WORKSPACE_SLUG, or pass them to createMarvinContent().'\n );\n }\n client ??= (config.createClient\n ? config.createClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })\n : createMarvinClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })) as MarvinClient;\n return client;\n },\n };\n}\n","/**\n * Configuration + environment resolution.\n *\n * Precedence for every value: explicit option → `import.meta.env` → `process.env`.\n * Astro exposes server-side env through `import.meta.env`; a plain Node script\n * (a smoke test, a build hook) only has `process.env`. Both work, unchanged.\n */\n\nimport type { MarkdownOptions } from './markdown.js';\n\nexport type MarvinLogger = {\n log: (message: string, ...args: unknown[]) => void;\n warn: (message: string, ...args: unknown[]) => void;\n error: (message: string, ...args: unknown[]) => void;\n};\n\nexport const ENV_KEYS = {\n apiUrl: 'MARVIN_API_URL',\n siteClientToken: 'MARVIN_SITE_CLIENT_TOKEN',\n workspaceSlug: 'MARVIN_WORKSPACE_SLUG',\n debug: 'MARVIN_DEBUG',\n} as const;\n\n/** Dev default for the network-failure latch: retry a downed backend after 30s. */\nexport const DEFAULT_DEV_RETRY_MS = 30_000;\n\n/** How many entry reads `hydrate` keeps in flight at once. */\nexport const DEFAULT_HYDRATE_CONCURRENCY = 6;\n\nexport type MarvinAstroConfig = {\n apiUrl?: string;\n siteClientToken?: string;\n workspaceSlug?: string;\n /** Log the resolved env once at startup, and enable SDK request logging. */\n debug?: boolean;\n /**\n * How long the network-failure latch stays closed before the backend is retried.\n * Defaults to 30s in dev and `Infinity` in production — a build should fail fast and\n * consistently rather than half-succeed with some pages live and some pages static.\n */\n retryAfterMs?: number;\n /**\n * Maximum entry reads in flight at once while hydrating a collection. Firing every read at\n * once makes a large collection time out on itself; the default of 6 keeps a 280-entry\n * hydration well inside the SDK's per-request timeout.\n */\n hydrateConcurrency?: number;\n logger?: MarvinLogger;\n /** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */\n markdown?: MarkdownOptions;\n /** Explicit env source. Highest priority after direct options — mainly a test seam. */\n env?: Record<string, string | undefined>;\n /** Injectable clock, so latch expiry is testable without waiting. */\n now?: () => number;\n /** Injectable SDK client factory — a test seam; defaults to the SDK's `createMarvinClient`. */\n createClient?: (config: MarvinClientOptions) => unknown;\n};\n\nexport type MarvinClientOptions = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n};\n\nexport type ResolvedConfig = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n retryAfterMs: number;\n hydrateConcurrency: number;\n logger: MarvinLogger;\n markdown?: MarkdownOptions;\n now: () => number;\n createClient?: (config: MarvinClientOptions) => unknown;\n /** True when all three connection values are present. */\n configured: boolean;\n};\n\nfunction metaEnv(): Record<string, unknown> | undefined {\n try {\n return (import.meta as unknown as { env?: Record<string, unknown> }).env;\n } catch {\n return undefined;\n }\n}\n\nfunction processEnv(): Record<string, string | undefined> | undefined {\n try {\n return typeof process !== 'undefined' ? process.env : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction nonEmpty(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\n/** Read one env var, honouring the documented precedence. */\nexport function readEnv(\n key: string,\n override?: Record<string, string | undefined>\n): string | undefined {\n return nonEmpty(override?.[key]) ?? nonEmpty(metaEnv()?.[key]) ?? nonEmpty(processEnv()?.[key]);\n}\n\nfunction isProduction(override?: Record<string, string | undefined>): boolean {\n const prodFlag = metaEnv()?.PROD;\n if (typeof prodFlag === 'boolean') return prodFlag;\n const mode = readEnv('NODE_ENV', override) ?? readEnv('MODE', override);\n return mode === 'production';\n}\n\nexport function resolveConfig(options: MarvinAstroConfig = {}): ResolvedConfig {\n const env = options.env;\n const apiUrl = options.apiUrl ?? readEnv(ENV_KEYS.apiUrl, env) ?? '';\n const siteClientToken = options.siteClientToken ?? readEnv(ENV_KEYS.siteClientToken, env) ?? '';\n const workspaceSlug = options.workspaceSlug ?? readEnv(ENV_KEYS.workspaceSlug, env) ?? '';\n const debug = options.debug ?? readEnv(ENV_KEYS.debug, env) === 'true';\n\n return {\n apiUrl,\n siteClientToken,\n workspaceSlug,\n debug,\n retryAfterMs:\n options.retryAfterMs ??\n (isProduction(env) ? Number.POSITIVE_INFINITY : DEFAULT_DEV_RETRY_MS),\n hydrateConcurrency: Math.max(1, Math.floor(options.hydrateConcurrency ?? DEFAULT_HYDRATE_CONCURRENCY)),\n logger: options.logger ?? console,\n markdown: options.markdown,\n now: options.now ?? (() => Date.now()),\n createClient: options.createClient,\n configured: Boolean(apiUrl && siteClientToken && workspaceSlug),\n };\n}\n\n/** One-line summary of the resolved env, with the token masked. For `debug` logging. */\nexport function describeConfig(config: ResolvedConfig): Record<string, unknown> {\n const token = config.siteClientToken;\n return {\n [ENV_KEYS.apiUrl]: config.apiUrl || 'MISSING',\n [ENV_KEYS.siteClientToken]: token ? `${token.slice(0, 12)}${'*'.repeat(20)}` : 'MISSING',\n [ENV_KEYS.workspaceSlug]: config.workspaceSlug || 'MISSING',\n hasAll: config.configured,\n };\n}\n","/**\n * Reading Marvin payloads.\n *\n * The published API returns the same logical value in several shapes depending on which\n * endpoint produced it (list item vs full read, SDK `Entry` wrapper vs raw JSON, placement\n * metadata vs asset metadata). Every reader here is total: it takes `unknown`, checks, and\n * returns `undefined` rather than throwing.\n */\n\nimport type { AssetSelectOptions, MarvinContentEntry } from './types.js';\n\n/** A non-empty string, or `undefined`. Empty/whitespace strings are treated as absent. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};\n}\n\nexport function asNumber(value: unknown): number | undefined {\n if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\nexport function asStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n\n const items = value.filter(\n (item): item is string => typeof item === 'string' && item.trim().length > 0\n );\n return items.length > 0 ? items : undefined;\n}\n\n/** `metadata_json` — the free-form blob every entry carries, whatever its schema. */\nexport function entryMetadata(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { metadata?: unknown; metadataJson?: unknown };\n return asRecord(value.metadata ?? value.metadataJson);\n}\n\n/** `data_json` — the schema-defined fields. Absent on list items until hydrated. */\nexport function entryData(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { data?: unknown; dataJson?: unknown };\n return asRecord(value.data ?? value.dataJson);\n}\n\n/**\n * Read a schema field (`data_json`) from an entry, through the SDK's `Entry` accessor when\n * present and off the raw payload otherwise.\n */\nexport function entryField<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const accessor = (entry as { field?: (key: string) => unknown }).field;\n if (typeof accessor === 'function') {\n return accessor.call(entry, key) as T | undefined;\n }\n return entryData(entry)[key] as T | undefined;\n}\n\n/**\n * The precedence rule: structured schema field first (`data_json`), legacy blob second\n * (`metadata_json`).\n *\n * An empty string counts as absent, which matters more than it sounds — an entry type that\n * declares a field the author left blank stores `\"\"`, and without this the legacy value that\n * *is* set would never surface.\n */\nexport function field<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const value = entryField(entry, key);\n if (value !== undefined && value !== null && value !== '') return value as T;\n const fallback = entryMetadata(entry)[key];\n return (fallback === '' ? undefined : fallback) as T | undefined;\n}\n\n/**\n * Read a value off a published asset placement, checking every level the API might have put it\n * at: the placement itself, its metadata blobs, then the underlying asset and its blobs.\n */\nexport function assetField<T = unknown>(asset: unknown, key: string): T | undefined {\n if (!asset || typeof asset !== 'object') return undefined;\n\n const value = asset as Record<string, unknown>;\n const nested = asRecord(value.asset);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.placementMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/** Same layered read as {@link assetField}, for resource relationships. */\nexport function resourceField<T = unknown>(resource: unknown, key: string): T | undefined {\n if (!resource || typeof resource !== 'object') return undefined;\n\n const value = resource as Record<string, unknown>;\n const nested = asRecord(value.resource);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/**\n * The entry's collection memberships as slugs. Collections arrive as bare slugs on list items,\n * flat `{slug}` objects, or the SDK's nested `{collection: {slug}}` — all three are read here.\n */\nexport function collectionSlugs(entry: MarvinContentEntry): string[] {\n const direct = (entry as { collectionSlugs?: unknown }).collectionSlugs;\n if (Array.isArray(direct)) {\n return direct.filter((slug): slug is string => typeof slug === 'string');\n }\n\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return [];\n\n return raw\n .map((item) =>\n typeof item === 'string' ? item : (asRecord(asRecord(item).collection).slug ?? asRecord(item).slug)\n )\n .filter((slug): slug is string => typeof slug === 'string');\n}\n\n/**\n * The entry's membership role within one specific collection (e.g. `'legal'` in\n * `footer-navigation`). A hydrated entry carries all of its memberships; this finds the one for\n * the given collection.\n */\nexport function collectionRole(\n entry: MarvinContentEntry,\n collectionSlug: string\n): string | undefined {\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return undefined;\n\n for (const item of raw) {\n if (typeof item === 'string') continue;\n const record = asRecord(item);\n const slug = asString(record.slug) ?? asString(asRecord(record.collection).slug);\n if (slug === collectionSlug) {\n return asString(asRecord(record.entryMetadata).role) ?? asString(record.role);\n }\n }\n return undefined;\n}\n\n/**\n * A CSS `object-position` value from a focal point. Marvin stores these either pre-formatted\n * (`\"40% 60%\"`) or as `{x, y}` in 0–1 or 0–100 units.\n */\nexport function focalPoint(value: unknown): string | undefined {\n const direct = asString(value);\n if (direct) return direct;\n\n const point = asRecord(value);\n const x = typeof point.x === 'number' ? point.x : undefined;\n const y = typeof point.y === 'number' ? point.y : undefined;\n if (x == null || y == null) return undefined;\n\n return `${x > 1 ? x : x * 100}% ${y > 1 ? y : y * 100}%`;\n}\n\nexport function entryAssets(entry: MarvinContentEntry): Record<string, unknown>[] {\n const assets = (entry as { assets?: unknown }).assets;\n return Array.isArray(assets) ? (assets as Record<string, unknown>[]) : [];\n}\n\nexport function entryResources(entry: MarvinContentEntry): Record<string, unknown>[] {\n const resources = (entry as { resources?: unknown }).resources;\n return Array.isArray(resources) ? (resources as Record<string, unknown>[]) : [];\n}\n\nfunction includesPreferred(actual: string | undefined, exact?: string, any?: string[]): boolean {\n if (!exact && !any?.length) return true;\n if (!actual) return false;\n return actual === exact || Boolean(any?.includes(actual));\n}\n\nfunction assetMatches(asset: Record<string, unknown>, options: AssetSelectOptions): boolean {\n const role = asString(assetField(asset, 'role'));\n const usage = asString(assetField(asset, 'usage'));\n const type = asString(assetField(asset, 'assetType'));\n const mimeType = asString(assetField(asset, 'mimeType'));\n\n const hasRoleCriteria = Boolean(options.role || options.roles?.length);\n const hasUsageCriteria = Boolean(options.usage || options.usages?.length);\n const roleMatches = includesPreferred(role, options.role, options.roles);\n const usageMatches = includesPreferred(usage, options.usage, options.usages);\n const typeMatches = includesPreferred(type, options.type, options.types);\n const mimeMatches = options.mimeType ? mimeType === options.mimeType : true;\n const unroledAllowed = Boolean(options.allowUnroled) && !role && !usage;\n\n // Role and usage are alternatives, not a conjunction: an asset placed with `role: hero` and\n // one placed with `usage: hero` both satisfy a hero request.\n const relationshipMatches =\n hasRoleCriteria || hasUsageCriteria ? roleMatches || usageMatches || unroledAllowed : true;\n\n return relationshipMatches && typeMatches && mimeMatches;\n}\n\nexport function selectEntryAsset(\n entry: MarvinContentEntry,\n options: AssetSelectOptions = {}\n): Record<string, unknown> | undefined {\n return entryAssets(entry).find((asset) => assetMatches(asset, options));\n}\n\n/**\n * Match on role ALONE, exactly. `selectEntryAsset` ORs role against a vacuously-true usage\n * check, so `{roles: ['hero-grade']}` there matches the entry's first asset whatever its role.\n * When you mean \"the asset whose role is exactly this\", use this.\n */\nexport function selectAssetByRole(\n entry: MarvinContentEntry,\n ...roles: string[]\n): Record<string, unknown> | undefined {\n for (const role of roles) {\n const found = entryAssets(entry).find((asset) => asString(assetField(asset, 'role')) === role);\n if (found) return found;\n }\n return undefined;\n}\n\n/** The list-item shorthand: `featuredAsset` is present on collection entries. */\nexport function selectFeaturedAsset(\n entry: MarvinContentEntry\n): Record<string, unknown> | undefined {\n const featured = asRecord((entry as { featuredAsset?: unknown }).featuredAsset);\n return Object.keys(featured).length > 0 ? featured : undefined;\n}\n\nexport function selectImageAsset(\n entry: MarvinContentEntry,\n options: Omit<AssetSelectOptions, 'type' | 'types'> = {}\n): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, { ...options, type: 'image' }) ??\n selectEntryAsset(entry, { ...options, types: ['image'], allowUnroled: true })\n );\n}\n\nexport function selectIconAsset(entry: MarvinContentEntry): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n mimeType: 'image/svg+xml',\n allowUnroled: true,\n }) ??\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n allowUnroled: true,\n })\n );\n}\n\nexport function assetUrl(asset: unknown): string | undefined {\n return asString(assetField(asset, 'publicUrl'));\n}\n\nexport function assetAlt(asset: unknown, fallback?: string): string | undefined {\n return asString(assetField(asset, 'altText')) ?? fallback;\n}\n\nexport function resourceRole(resource: unknown): string | undefined {\n return asString(resourceField(resource, 'role'));\n}\n\nexport function isExternalHref(href: string): boolean {\n return /^(https?:|mailto:|tel:)/.test(href);\n}\n","/**\n * Site chrome: main nav, footer columns, legal strip, social links, inquiry link.\n *\n * All of it comes from ordinary Marvin collections, so a workspace can reorder its own\n * navigation without a deploy. Every piece has a static fallback for when the backend is down.\n */\n\nimport type { MarvinFetcher, HydratedEntry } from './fetch.js';\nimport {\n asRecord,\n asString,\n collectionRole,\n collectionSlugs,\n entryField,\n isExternalHref,\n} from './normalize.js';\nimport type { SiteLoader } from './site.js';\nimport type {\n ApiNavigationLink,\n ApiSite,\n ApiSiteChrome,\n ApiSocialLink,\n MarvinContentEntry,\n} from './types.js';\n\nexport type NavigationContext = 'main' | 'footer';\n\nexport type HrefContext = {\n /** Where this link is being rendered. */\n context: NavigationContext;\n /** The nav collection the entry was read from. */\n collectionSlug: string;\n slug: string;\n /** Every collection the entry belongs to, nav collections included. */\n collections: string[];\n /** The collection slugs treated as navigation, i.e. not route prefixes. */\n navCollections: Set<string>;\n};\n\nexport type ResolveHref = (entry: MarvinContentEntry, context: HrefContext) => string;\n\n/** A link authored in site code rather than in the CMS. */\nexport type NavigationLinkInput = {\n label: string;\n href: string;\n description?: string;\n role?: string;\n};\n\nexport type SocialLinkInput = NavigationLinkInput & { icon: string };\n\nexport type ChromeOptions = {\n /** Collection holding the main nav. Default `'main-navigation'`. */\n mainCollection?: string;\n /** Collection holding the footer nav. Default `'footer-navigation'`. */\n footerCollection?: string;\n /**\n * Collection holding the social links. Default `'social-links'`. When it has entries, they\n * (label, href, optional `icon` field) are exactly the social links, in order; otherwise the\n * site `social` map is used.\n */\n socialCollection?: string;\n /**\n * Route for a nav entry that carries no explicit `href`/`url`/`path` field.\n *\n * The default prefixes the entry's own (non-navigation) collection: an entry in\n * `workshop-reference` becomes `/workshop-reference/<slug>`, and an entry in no other\n * collection becomes `/<slug>`. Override when routes don't mirror collections.\n */\n resolveHref?: ResolveHref;\n /** Membership role that marks a footer link as legal (Terms, Privacy…). Default `'legal'`. */\n legalRole?: string;\n /** Footer links per column once split. Columns are only created above this count. */\n footerColumnThreshold?: number;\n fallback?: {\n mainNavigation?: NavigationLinkInput[];\n footerNavigation?: NavigationLinkInput[][];\n legalLinks?: NavigationLinkInput[];\n socialLinks?: SocialLinkInput[];\n inquiry?: NavigationLinkInput;\n };\n};\n\nexport function toNavigationLink(link: NavigationLinkInput): ApiNavigationLink {\n return {\n label: link.label,\n href: link.href,\n description: link.description,\n external: isExternalHref(link.href),\n role: link.role,\n };\n}\n\n/** Collection-prefixed routing: `/<owning-collection>/<slug>`, else `/<slug>`. */\nexport const defaultResolveHref: ResolveHref = (_entry, context) => {\n const owning = context.collections.find((slug) => !context.navCollections.has(slug));\n return owning ? `/${owning}/${context.slug}` : `/${context.slug}`;\n};\n\n/** A `{label, href}` object stored loose in site metadata (e.g. `metadata.inquiry`). */\nexport function metadataLink(value: unknown): ApiNavigationLink | undefined {\n const link = asRecord(value);\n const label = asString(link.label);\n const href = asString(link.href);\n if (!label || !href) return undefined;\n\n return {\n label,\n href,\n description: asString(link.description) ?? asString(link.subject),\n external: isExternalHref(href),\n };\n}\n\n/** Title-case a social key: `x_handle` → `X Handle`, `email` → `Email`. */\nfunction socialLabel(key: string): string {\n return key.replace(/[-_]/g, ' ').replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nexport function socialLinkFromKey(key: string, href: string): ApiSocialLink {\n return {\n label: socialLabel(key),\n href,\n icon: key,\n external: isExternalHref(href),\n };\n}\n\n/**\n * Icon key for a social link with no `icon` field: `mailto:` → `email`, else the network's\n * name from the host (`www.instagram.com` → `instagram`, `x.com` → `x`), else the label.\n */\nexport function socialIconFor(href: string, label: string): string {\n if (href.startsWith('mailto:')) return 'email';\n try {\n const host = new URL(href).hostname.replace(/^(www|m)\\./, '');\n return host.split('.')[0] ?? label.toLowerCase();\n } catch {\n return label.toLowerCase();\n }\n}\n\nexport type ChromeLoader = {\n get(): Promise<ApiSiteChrome>;\n reset(): void;\n};\n\nexport function createChromeLoader(\n fetcher: MarvinFetcher,\n siteLoader: SiteLoader,\n options: ChromeOptions = {}\n): ChromeLoader {\n const mainCollection = options.mainCollection ?? 'main-navigation';\n const footerCollection = options.footerCollection ?? 'footer-navigation';\n const socialCollection = options.socialCollection ?? 'social-links';\n const legalRole = options.legalRole ?? 'legal';\n const footerColumnThreshold = options.footerColumnThreshold ?? 4;\n const resolveHref = options.resolveHref ?? defaultResolveHref;\n const navCollections = new Set([mainCollection, footerCollection, socialCollection]);\n const fallback = options.fallback ?? {};\n\n let promise: Promise<ApiSiteChrome> | null = null;\n\n function entryToLink(\n entry: HydratedEntry,\n collectionSlug: string,\n context: NavigationContext\n ): ApiNavigationLink {\n const slug = entry.slug ?? '';\n const href =\n asString(entryField(entry, 'href')) ??\n asString(entryField(entry, 'url')) ??\n asString(entryField(entry, 'path')) ??\n resolveHref(entry, {\n context,\n collectionSlug,\n slug,\n collections: collectionSlugs(entry),\n navCollections,\n });\n const label =\n asString(entryField(entry, 'label')) ??\n asString(asRecord((entry as { metadataJson?: unknown }).metadataJson).label) ??\n asString(entry.title) ??\n href;\n\n return {\n label,\n href,\n description:\n asString(entry.summary) ?? asString((entry as { description?: unknown }).description),\n external: isExternalHref(href),\n role: collectionRole(entry, collectionSlug),\n };\n }\n\n async function navigationCollection(\n slug: string,\n context: NavigationContext,\n online: boolean\n ): Promise<{ links: ApiNavigationLink[]; entries: HydratedEntry[] }> {\n if (!online) return { links: [], entries: [] };\n\n // Hydrated: a nav entry's `href`/`label` overrides are schema fields, absent from list items.\n // Empty, hidden or missing all read as empty: Marvin is up, so its answer is final.\n const entries = await fetcher.hydratedCollectionEntries(slug);\n return { links: entries.map((entry) => entryToLink(entry, slug, context)), entries };\n }\n\n function groupFooterLinks(links: ApiNavigationLink[]): ApiNavigationLink[][] {\n if (links.length <= footerColumnThreshold) return [links];\n\n const midpoint = Math.ceil(links.length / 2);\n return [links.slice(0, midpoint), links.slice(midpoint)];\n }\n\n /** The social-links collection as links, or null when it has no entries. */\n async function socialEntries(online: boolean): Promise<ApiSocialLink[] | null> {\n const social = await navigationCollection(socialCollection, 'footer', online);\n if (social.links.length === 0) return null;\n\n return social.links.map((link, index) => ({\n ...link,\n icon:\n asString(entryField(social.entries[index], 'icon')) ?? socialIconFor(link.href, link.label),\n }));\n }\n\n /** The site `social` map plus the contact email; offline, the static social links if both are empty. */\n function siteSocialLinks(site: ApiSite, online: boolean): ApiSocialLink[] {\n const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));\n if (site.email && !links.some((link) => link.icon === 'email')) {\n links.push(socialLinkFromKey('email', `mailto:${site.email}`));\n }\n if (links.length > 0 || online) return links;\n\n return (fallback.socialLinks ?? []).map((link) => ({\n ...toNavigationLink(link),\n icon: link.icon,\n }));\n }\n\n async function load(): Promise<ApiSiteChrome> {\n const site = await siteLoader.get();\n const staticMain = (fallback.mainNavigation ?? []).map(toNavigationLink);\n const staticFooter = (fallback.footerNavigation ?? []).map((group) =>\n group.map(toNavigationLink)\n );\n\n // The build's one \"is Marvin up?\" check: online, Marvin's content is final everywhere below;\n // offline, every part of the chrome is the static fallback.\n const online = await siteLoader.online();\n const main = await navigationCollection(mainCollection, 'main', online);\n const footer = await navigationCollection(footerCollection, 'footer', online);\n\n const social = await socialEntries(online);\n\n // Split footer links by membership role: legal → the legal strip; everything else → columns.\n const columnLinks = footer.links.filter((link) => link.role !== legalRole);\n const staticLegal = (fallback.legalLinks ?? []).map(toNavigationLink);\n\n return {\n site,\n mainNavigation: online ? main.links : staticMain,\n footerNavigation: online ? groupFooterLinks(columnLinks) : staticFooter,\n legalLinks: online ? footer.links.filter((link) => link.role === legalRole) : staticLegal,\n socialLinks: social ?? siteSocialLinks(site, online),\n inquiry: online\n ? metadataLink(site.metadata.inquiry)\n : fallback.inquiry && toNavigationLink(fallback.inquiry),\n };\n }\n\n return {\n get() {\n promise ??= load();\n return promise;\n },\n reset() {\n promise = null;\n },\n };\n}\n","/**\n * Every call into Marvin, wrapped the same way: skip when there's no backend, catch, remember\n * network failures for the latch, warn once, and return an empty value the caller can fall\n * back from. Nothing here throws.\n */\n\nimport type {\n Collection,\n CollectionEntry,\n Entry,\n MarvinEntry,\n MarvinSite,\n PublishedCollectionSummary,\n Workspace,\n} from '@inneropen/marvin-sdk';\nimport { errorMessage, type MarvinBackend } from './client.js';\nimport type { MarvinContentEntry } from './types.js';\n\n/** An entry that carries `data_json` — i.e. schema fields are readable. */\nexport type HydratedEntry = MarvinEntry | Entry;\n\nexport type MarvinFetcher = ReturnType<typeof createFetcher>;\n\n/**\n * `collections.entries` as SDK 4.2+ declares it: `{ expand: 'full' }` asks the server for full\n * entries (`?expand=full`) and returns them as SDK `Entry` objects. Older SDKs take only the slug\n * and ignore the option, so the same call works with any SDK in the peer range; their list items\n * are then hydrated one by one.\n */\ntype ExpandingCollectionEntries = (\n slug: string,\n options?: { expand?: 'full' }\n) => Promise<MarvinContentEntry[]>;\n\n/**\n * True for a full entry (`PublishedEntryRead`), false for a list item (`PublishedEntryListItem`).\n *\n * `data` doesn't tell them apart: list items carry it too now. What differs is attachments. A full\n * read has `assets[]` and `resources[]` (placement objects, always present, possibly empty); a list\n * item has only `assetSlugs` / `resourceSlugs` and a `featuredAsset`. The SDK's `Entry` wrapper\n * only ever comes from the single-entry read.\n */\nexport function isFullEntry(item: unknown): boolean {\n if (!item || typeof item !== 'object') return false;\n if (typeof (item as { field?: unknown }).field === 'function') return true;\n const raw = item as Record<string, unknown>;\n if ('assetSlugs' in raw || 'resourceSlugs' in raw) return false;\n return Array.isArray(raw.assets) || Array.isArray(raw.resources);\n}\n\n/** A failed hydration read is retried this many times in total, backing off from this base. */\nexport const HYDRATE_ATTEMPTS = 3;\nexport const HYDRATE_BACKOFF_MS = 500;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `Promise.all(items.map(run))` with at most `limit` calls in flight. Order is preserved. */\nasync function mapWithConcurrency<T, R>(\n items: T[],\n limit: number,\n run: (item: T) => Promise<R>\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n\n async function worker(): Promise<void> {\n while (next < items.length) {\n const index = next++;\n results[index] = await run(items[index]);\n }\n }\n\n const workers = Array.from({ length: Math.min(limit, items.length) }, worker);\n await Promise.all(workers);\n return results;\n}\n\nexport function createFetcher(backend: MarvinBackend) {\n async function guarded<T>(label: string, empty: T, run: () => Promise<T>): Promise<T> {\n if (!backend.hasBackend()) return empty;\n try {\n return await run();\n } catch (error) {\n backend.remember(error);\n backend.warn(`${label} unavailable: ${errorMessage(error)}`);\n return empty;\n }\n }\n\n async function entry(slug: string): Promise<Entry | null> {\n return guarded(`Entry \"${slug}\"`, null, () => backend.client().entry(slug));\n }\n\n /**\n * One entry read that survives a transient failure.\n *\n * `entry()` can't be used here: `guarded` swallows the error and trips the latch, and a\n * tripped latch turns every remaining item into a silent null. So retry with backoff,\n * re-opening the latch first in case a sibling read tripped it, and only remember the\n * failure — and warn — once the attempts are spent.\n */\n async function entryForHydration(slug: string): Promise<Entry | null> {\n if (!backend.hasBackend()) return null;\n\n let failure: unknown;\n for (let attempt = 1; attempt <= HYDRATE_ATTEMPTS; attempt++) {\n if (attempt > 1) {\n await sleep(HYDRATE_BACKOFF_MS * 2 ** (attempt - 2));\n if (backend.isLatched()) backend.clearLatch();\n }\n try {\n return await backend.client().entry(slug);\n } catch (error) {\n failure = error;\n }\n }\n\n backend.remember(failure);\n backend.warn(\n `Entry \"${slug}\" dropped after ${HYDRATE_ATTEMPTS} attempts: ${errorMessage(failure)}`\n );\n return null;\n }\n\n /**\n * Turn list items into full entries.\n *\n * The collection endpoint returns `PublishedEntryListItem`: core fields, `metadata_json` and\n * (on current servers) `data`, but only asset/resource *slugs* — no `assets[]` placements (so\n * no roles: no `hero-grade` image) and no `resources[]`. `PublishedEntryRead` (the single-entry\n * endpoint) has all of it. Only items that are already full entries ({@link isFullEntry}) are\n * passed through; everything else is re-read, even when it carries `data`. A list fetched with\n * `expand=full` ({@link expandedCollectionEntries}) is all full entries, so nothing is re-read.\n *\n * Reads run at most `config.hydrateConcurrency` at a time. Unbounded, a 280-entry collection\n * fired 280 requests at once, most of which timed out and were dropped — and the first\n * timeout latched the backend off for the rest of the build.\n */\n /**\n * A collection's entries as full entries in one request, where the server and SDK support\n * `expand=full` (Marvin core with expand, `@inneropen/marvin-sdk` 4.2+). Otherwise this is\n * `collectionEntries`: list items, which {@link hydrate} reads one at a time.\n */\n function expandedCollectionEntries(slug: string): Promise<MarvinContentEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () => {\n const collections = backend.client().collections;\n const entries = collections.entries as ExpandingCollectionEntries;\n return entries.call(collections, slug, { expand: 'full' });\n });\n }\n\n async function hydrate(entries: MarvinContentEntry[]): Promise<HydratedEntry[]> {\n const hydrated = await mapWithConcurrency(\n entries,\n backend.config.hydrateConcurrency,\n async (item) => {\n if (isFullEntry(item)) return item as HydratedEntry;\n return entryForHydration(item.slug ?? '');\n }\n );\n\n return hydrated.filter((item): item is HydratedEntry => Boolean(item));\n }\n\n return {\n backend,\n\n collections(): Promise<PublishedCollectionSummary[]> {\n return guarded('Collections', [], () => backend.client().collections.list());\n },\n\n async collection(slug: string): Promise<Collection | null> {\n return guarded(`Collection \"${slug}\"`, null, async () => {\n const found = await backend.client().collections.get(slug);\n return Array.isArray(found) ? null : found;\n });\n },\n\n collectionEntries(slug: string): Promise<CollectionEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () =>\n backend.client().collections.entries(slug)\n );\n },\n\n /** Try each collection slug in order; return the first that yields entries. */\n async collectionEntriesFallback(slugs: string[]): Promise<CollectionEntry[]> {\n for (const slug of slugs) {\n const entries = await this.collectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n expandedCollectionEntries,\n\n /** Full entries: one `expand=full` request, or per-item reads where that isn't supported. */\n async hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]> {\n return hydrate(await expandedCollectionEntries(slug));\n },\n\n async hydratedCollectionEntriesFallback(slugs: string[]): Promise<HydratedEntry[]> {\n for (const slug of slugs) {\n const entries = await this.hydratedCollectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n hydrate,\n entry,\n\n site(): Promise<MarvinSite | null> {\n return guarded('Site', null, () => backend.client().getSite());\n },\n\n workspace(): Promise<Workspace | null> {\n return guarded('Workspace', null, () => backend.client().getWorkspace());\n },\n };\n}\n","import { Marked, type MarkedOptions, type Tokens } from 'marked';\nimport { findEmbed, type MarvinEmbeds } from './embeds.js';\n\nexport type MarkdownOptions = MarkedOptions;\n\n/** Per-call options for a {@link MarkdownRenderer}. */\nexport type RenderMarkdownOptions = {\n /**\n * The entry's `embeds` map. A paragraph that is exactly a URL with an entry here renders as\n * that embed's Marvin-built HTML instead of a link.\n */\n embeds?: MarvinEmbeds | null;\n};\n\nexport type MarkdownRenderer = (\n markdown: string | string[] | null | undefined,\n options?: RenderMarkdownOptions\n) => Promise<string>;\n\n// Per-call data rides on marked's parse options: marked merges them over the instance defaults and\n// hands the merged object to the renderer as `this.options` for that parse only. A symbol key\n// keeps it clear of marked's own option names.\nconst EMBEDS = Symbol('marvin.embeds');\ntype ParseOptionsWithEmbeds = MarkedOptions & { [EMBEDS]?: MarvinEmbeds | null };\n\n// One run of non-space characters starting with a scheme. `<url>`, `[text](url)` and a URL inside\n// a sentence all fail this, so they stay links.\nconst BARE_URL = /^https?:\\/\\/\\S+$/i;\n\n/**\n * The embed for a paragraph that is exactly a URL with an `embeds` entry, else `undefined`.\n * Exported for tests; the renderer below is the only caller.\n */\nexport function paragraphEmbedHtml(\n token: Pick<Tokens.Paragraph, 'text'>,\n embeds: MarvinEmbeds | null | undefined\n): string | undefined {\n if (!embeds) return undefined;\n const text = token.text.trim();\n if (!BARE_URL.test(text)) return undefined;\n const html = findEmbed(embeds, text)?.html;\n return typeof html === 'string' && html.trim() ? html : undefined;\n}\n\n/**\n * A markdown renderer with its own `marked` instance — options are per-site, not global, so two\n * `createMarvinContent()` instances in one process can't clobber each other's settings.\n *\n * `breaks: false` is the default because CMS body copy is authored as prose: a soft-wrapped\n * paragraph should render as one paragraph. Where authored line breaks must survive (a landing\n * intro, an address block), pass `breaks: true` or pre-convert with a trailing double space.\n *\n * Pass the entry's `embeds` to turn a bare provider URL on its own line into its player:\n *\n * ```ts\n * await renderMarkdown(body, { embeds: entryEmbeds(entry) });\n * ```\n */\nexport function createMarkdownRenderer(options: MarkdownOptions = {}): MarkdownRenderer {\n const marked = new Marked();\n marked.setOptions({ gfm: true, breaks: false, ...options });\n marked.use({\n renderer: {\n paragraph(token) {\n const embeds = (this.options as ParseOptionsWithEmbeds)[EMBEDS];\n const html = paragraphEmbedHtml(token, embeds);\n // `false` hands the token back to marked's default paragraph renderer.\n return html === undefined ? false : `${html}\\n`;\n },\n },\n });\n\n return async (markdown, renderOptions = {}) => {\n const source = Array.isArray(markdown) ? markdown.join('\\n\\n') : (markdown ?? '');\n if (!source) return '';\n const parseOptions: ParseOptionsWithEmbeds = { [EMBEDS]: renderOptions.embeds ?? null };\n return await marked.parse(source, parseOptions);\n };\n}\n\n/** Turn single newlines into hard breaks, leaving paragraph breaks alone. */\nexport function preserveSoftBreaks(source: string): string {\n return source.replace(/(?<!\\n)\\n(?!\\n)/g, ' \\n');\n}\n","/**\n * Media embeds: the published shapes Marvin hands a site, read tolerantly.\n *\n * Marvin resolves a pasted provider link (YouTube, Vimeo, Spotify, …) on the server and publishes\n * the result next to the entry as `entry.embeds`, keyed by the URL exactly as the author wrote it.\n * Stored markdown is never rewritten: a site that knows about embeds swaps the link for the\n * player; an older site just shows the link.\n *\n * These types are declared here rather than imported from `@inneropen/marvin-sdk` so the package\n * works with SDKs that predate embeds. Kind and status are open-ended so a newer server value\n * doesn't break an older site's types.\n */\n\nimport { asRecord, asString } from './normalize.js';\n\nexport type MarvinEmbedKind = 'video' | 'audio' | 'podcast' | 'playlist';\nexport type MarvinEmbedStatus = 'ok' | 'link' | 'unavailable';\nexport type MarvinEmbedMode = 'direct' | 'click_to_load';\n\n/**\n * A Marvin-built iframe. Every value comes from Marvin's provider registry. Exactly one of\n * `aspectRatio` / `height` is set; the other is `null`.\n */\nexport type MarvinEmbedIframe = {\n src: string;\n title: string;\n allow?: string | null;\n sandbox?: string | null;\n referrerpolicy?: string | null;\n /** CSS aspect ratio for video players, e.g. `\"16/9\"` (`\"9/16\"` for Shorts); `null` otherwise. */\n aspectRatio?: string | null;\n /** Fixed pixel height for audio / podcast / playlist players; `null` for video. */\n height?: number | null;\n};\n\n/** The plain link every embed falls back to. */\nexport type MarvinEmbedLink = {\n href: string;\n title: string;\n providerName: string;\n};\n\n/** Optional fields arrive as `null` rather than omitted; every reader here treats both alike. */\nexport type MarvinEmbed = {\n /** The URL exactly as written in the content. */\n url: string;\n canonicalUrl?: string | null;\n /** Provider key, e.g. `youtube`. */\n provider: string;\n /** Display name, e.g. `YouTube`. */\n providerName: string;\n kind: MarvinEmbedKind | (string & {});\n /** `ok` has an iframe; `link` and `unavailable` render as a link card. */\n status: MarvinEmbedStatus | (string & {});\n title?: string | null;\n authorName?: string | null;\n thumbnailUrl?: string | null;\n iframe?: MarvinEmbedIframe | null;\n link: MarvinEmbedLink;\n /** Marvin-built HTML for the site's embed mode (an iframe, a click-to-load facade, or a link). */\n html: string;\n};\n\n/** `entry.embeds`: every resolved embed on the entry, keyed by the URL as written. */\nexport type MarvinEmbeds = Record<string, MarvinEmbed>;\n\n/**\n * `site.embeds` (next to `site.seo`): the site's embed privacy mode, consent text (`{provider}`\n * already substituted per embed in the published `html`) and the frame sources a CSP must allow.\n */\nexport type MarvinSiteEmbeds = {\n mode: MarvinEmbedMode;\n consentText: string;\n /** Origins/hosts the site's embeds may frame — feed these into a `frame-src` directive. */\n frameSources: string[];\n};\n\nconst EMBED_MODES = new Set<string>(['direct', 'click_to_load']);\n\nfunction readEmbedsField(entry: unknown): unknown {\n if (!entry || typeof entry !== 'object') return undefined;\n const direct = (entry as { embeds?: unknown }).embeds;\n if (direct !== undefined) return direct;\n // The SDK's `Entry` class keeps the payload private; its `toJSON()` hands it back.\n const toJSON = (entry as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === 'function') {\n return asRecord(toJSON.call(entry)).embeds;\n }\n return undefined;\n}\n\n/**\n * The entry's `embeds` map, or `undefined` when it has none (an older server, or no embeds).\n * Values that aren't objects carrying a URL are dropped.\n */\nexport function entryEmbeds(entry: unknown): MarvinEmbeds | undefined {\n const raw = readEmbedsField(entry);\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;\n\n const embeds: MarvinEmbeds = {};\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n embeds[key] = value as MarvinEmbed;\n }\n }\n return Object.keys(embeds).length > 0 ? embeds : undefined;\n}\n\n/** The embed for `url` as written, tolerating surrounding whitespace. */\nexport function findEmbed(\n embeds: MarvinEmbeds | null | undefined,\n url: string | null | undefined\n): MarvinEmbed | undefined {\n if (!embeds || !url) return undefined;\n const key = url.trim();\n if (!key) return undefined;\n return Object.prototype.hasOwnProperty.call(embeds, key) ? embeds[key] : undefined;\n}\n\n/** Normalize `site.embeds`; `undefined` when the server doesn't send it. */\nexport function siteEmbeds(raw: unknown): MarvinSiteEmbeds | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const value = asRecord(raw);\n const mode = asString(value.mode);\n const frameSources = Array.isArray(value.frameSources)\n ? value.frameSources.filter(\n (source): source is string => typeof source === 'string' && source.trim().length > 0\n )\n : [];\n\n return {\n // Unknown or missing mode → the privacy-preserving default.\n mode: mode && EMBED_MODES.has(mode) ? (mode as MarvinEmbedMode) : 'click_to_load',\n consentText: asString(value.consentText) ?? '',\n frameSources,\n };\n}\n","const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\n/**\n * Format a date value for display as \"Mon DD, YYYY\".\n *\n * ISO dates/timestamps (e.g. a raw `publishedAt` like \"2026-07-21T21:22:42.045708Z\") are read in\n * UTC — via the date part only, so there's no off-by-one day shift from the viewer's timezone.\n * Values that are already human-readable (e.g. a hand-entered \"Jul 02, 2024\") pass through\n * unchanged, and empty input returns `undefined` so callers can omit the field.\n */\nexport function formatDisplayDate(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(trimmed);\n if (iso) {\n const [, year, month, day] = iso;\n const name = MONTHS[Number(month) - 1];\n if (name) return `${name} ${day}, ${year}`;\n }\n\n return trimmed;\n}\n\n/**\n * Pick `count` items deterministically from `items`, seeded by `pageSlug`.\n *\n * Same page → same selection on every build, different pages → different selections. Use for\n * rotating pull-quotes, value bands, related links: variety across the site without the\n * hydration mismatch a random pick would cause.\n */\nexport function selectValuesForPage<T>(items: T[], pageSlug: string, count = 4): T[] {\n const seeded = [...items];\n let seed = 0;\n\n for (const character of pageSlug) {\n seed = (seed * 31 + character.charCodeAt(0)) >>> 0;\n }\n\n for (let index = seeded.length - 1; index > 0; index -= 1) {\n seed = (seed * 1664525 + 1013904223) >>> 0;\n\n const target = seed % (index + 1);\n\n [seeded[index], seeded[target]] = [seeded[target], seeded[index]];\n }\n\n return seeded.slice(0, count);\n}\n","/**\n * The field accessor handed to a repository's `transform`.\n *\n * Every reader defaults to the documented precedence — `data_json` first, `metadata_json`\n * second — so a transform reads like a field list instead of a pile of coalescing operators.\n */\n\nimport { entryEmbeds, findEmbed, type MarvinEmbed } from './embeds.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport { preserveSoftBreaks } from './markdown.js';\nimport {\n asNumber,\n asRecord,\n asString,\n asStringArray,\n assetAlt,\n assetField,\n assetUrl,\n collectionRole,\n collectionSlugs,\n entryAssets,\n entryData,\n entryMetadata,\n entryResources,\n field,\n focalPoint,\n resourceField,\n selectAssetByRole,\n selectFeaturedAsset,\n selectIconAsset,\n selectImageAsset,\n} from './normalize.js';\nimport { formatDisplayDate } from './format.js';\nimport type {\n ApiImage,\n ApiResourceLink,\n AssetSelectOptions,\n MarvinContentEntry,\n} from './types.js';\n\nexport type ImageFieldOptions = Omit<AssetSelectOptions, 'type' | 'types'> & {\n /**\n * Exact roles to prefer, in order, before the looser role/usage match. Use for derived\n * variants — e.g. `['hero-grade']` to prefer a colour-graded hero over the raw upload.\n */\n preferRoles?: string[];\n /** `metadata_json` key holding a hand-authored `{src, alt, focalPoint}`. */\n metadataKey?: string | false;\n /** Alt text when the asset carries none. Defaults to the entry title. */\n alt?: string;\n /** Fall back to the list item's `featuredAsset`. Default `true`. */\n fallbackToFeatured?: boolean;\n};\n\nexport type IconFieldOptions = {\n roles?: string[];\n /** Fall back to the list item's `featuredAsset` when no icon asset is attached. */\n fallbackToFeatured?: boolean;\n};\n\nexport type MarkdownFieldOptions = {\n /** Keep authored single newlines as hard breaks. Default `false`. */\n softBreaks?: boolean;\n /**\n * Render a bare provider URL on its own line as its player, using the entry's `embeds`.\n * Default `true`; `false` keeps every URL a plain link.\n */\n embeds?: boolean;\n};\n\nexport type ResourceFieldOptions = {\n /** Keep only these resource types, e.g. `['material', 'construction']`. */\n types?: string[];\n /** Keep only resources attached with this role. */\n role?: string;\n /** Build an href from the resource slug. */\n href?: (slug: string) => string;\n};\n\nconst DEFAULT_IMAGE_ROLES = ['hero', 'featured', 'card'];\n\nexport type FieldAccessor = ReturnType<typeof createFieldAccessor>;\n\nexport type FieldAccessorContext = {\n renderMarkdown: MarkdownRenderer;\n /** The route this entry resolves to, if the repository was given an `href` builder. */\n href?: string;\n /** The collection the entry was loaded from, when it came from one. */\n collection?: string;\n};\n\nexport function createFieldAccessor(\n entry: MarvinContentEntry,\n context: FieldAccessorContext\n) {\n const title = asString((entry as { title?: unknown }).title);\n const embeds = entryEmbeds(entry);\n\n const accessor = {\n /** The entry itself, for anything the accessor doesn't cover. */\n entry,\n /** The route this entry resolves to (from the repository's `href` option). */\n href: context.href,\n collection: context.collection,\n\n /** `data_json` → `metadata_json`, untyped. */\n raw(key: string): unknown {\n return field(entry, key);\n },\n\n string(key: string): string | undefined {\n return asString(field(entry, key));\n },\n\n number(key: string): number | undefined {\n return asNumber(field(entry, key));\n },\n\n /** Booleans authored as strings (`\"true\"`, `\"1\"`, `\"yes\"`) read as booleans. */\n bool(key: string): boolean {\n const value = field(entry, key);\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n return ['true', '1', 'yes', 'on'].includes(value.trim().toLowerCase());\n }\n if (typeof value === 'number') return value !== 0;\n return Boolean(value);\n },\n\n /** A string array, tolerating a single string authored in place of a list. */\n list(key: string): string[] | undefined {\n const value = field(entry, key);\n const array = asStringArray(value);\n if (array) return array;\n const single = asString(value);\n return single ? [single] : undefined;\n },\n\n /**\n * An enum-guarded read: the value if it's in `allowed`, else `fallback`. Replaces the\n * per-field `normalizeStatus`/`normalizeCategory`/`normalizeTone` guards that every site\n * ends up writing.\n */\n oneOf<T extends string>(key: string, allowed: readonly T[] | Set<T>, fallback: T): T {\n const value = asString(field(entry, key));\n const set = allowed instanceof Set ? allowed : new Set<string>(allowed);\n return value && set.has(value as T) ? (value as T) : fallback;\n },\n\n /** A field read as a display date (\"Mon DD, YYYY\"); already-formatted values pass through. */\n date(key: string): string | undefined {\n return formatDisplayDate(asString(field(entry, key)));\n },\n\n /** The raw ISO publish timestamp, if any. */\n publishedAt(): string | undefined {\n const value = (entry as { publishedAt?: unknown }).publishedAt;\n return value != null ? String(value) : undefined;\n },\n\n /**\n * Render a markdown field to HTML. Falls back to the entry's `contentMarkdown` when the\n * named field is absent. Returns `undefined` when there is nothing to render, so the\n * caller can omit the property rather than emit an empty string.\n */\n async markdown(\n key = 'body',\n options: MarkdownFieldOptions = {}\n ): Promise<string | undefined> {\n const raw =\n field<string | string[]>(entry, key) ??\n (entry as { contentMarkdown?: string | string[] }).contentMarkdown;\n const source = Array.isArray(raw) ? raw.join('\\n\\n') : asString(raw);\n if (!source) return undefined;\n return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source, {\n embeds: options.embeds === false ? undefined : embeds,\n });\n },\n\n /**\n * The resolved embed for an `embed` field (its value is the provider URL), or `undefined`\n * when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from\n * `@inneropen/marvin-astro/components`, or output `embed.html`.\n */\n embed(key: string): MarvinEmbed | undefined {\n return findEmbed(embeds, asString(field(entry, key)));\n },\n\n /**\n * The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,\n * the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the\n * list item's `featuredAsset`.\n */\n image(options: ImageFieldOptions = {}): ApiImage | undefined {\n const {\n preferRoles,\n metadataKey = 'featuredImage',\n alt: altFallback,\n fallbackToFeatured = true,\n roles = DEFAULT_IMAGE_ROLES,\n usages = DEFAULT_IMAGE_ROLES,\n allowUnroled = true,\n ...rest\n } = options;\n const fallbackAlt = altFallback ?? title ?? '';\n\n if (metadataKey) {\n const authored = asRecord(entryMetadata(entry)[metadataKey]);\n const src = asString(authored.src);\n if (src) {\n return {\n src,\n alt: asString(authored.alt) ?? fallbackAlt,\n focalPoint: focalPoint(authored.focalPoint),\n };\n }\n }\n\n const asset =\n (preferRoles?.length ? selectAssetByRole(entry, ...preferRoles) : undefined) ??\n selectImageAsset(entry, { ...rest, roles, usages, allowUnroled }) ??\n (fallbackToFeatured ? selectFeaturedAsset(entry) : undefined);\n\n const src = assetUrl(asset);\n if (!src) return undefined;\n\n return {\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n };\n },\n\n /** URL of the entry's icon asset (`role: icon`, SVG preferred). */\n icon(options: IconFieldOptions = {}): string | undefined {\n const { roles = ['icon'], fallbackToFeatured = false } = options;\n const asset = selectAssetByRole(entry, ...roles) ?? selectIconAsset(entry);\n return (\n assetUrl(asset) ?? (fallbackToFeatured ? assetUrl(selectFeaturedAsset(entry)) : undefined)\n );\n },\n\n /** The raw asset placement matching `options` — when you need more than src/alt/focal. */\n asset(options: AssetSelectOptions = {}): Record<string, unknown> | undefined {\n return selectImageAsset(entry, options) ?? selectFeaturedAsset(entry);\n },\n\n /** The asset whose role is EXACTLY one of `roles`, in preference order. */\n assetByRole(...roles: string[]): Record<string, unknown> | undefined {\n return selectAssetByRole(entry, ...roles);\n },\n\n /** All asset placements on the entry. */\n assets(): Record<string, unknown>[] {\n return entryAssets(entry);\n },\n\n /** Every image asset resolved, filtered by role/usage — support/detail galleries. */\n images(options: ImageFieldOptions = {}): (ApiImage & { usage?: string })[] {\n const wanted = new Set([...(options.roles ?? []), ...(options.usages ?? [])]);\n const fallbackAlt = options.alt ?? title ?? '';\n const images: (ApiImage & { usage?: string })[] = [];\n\n for (const asset of entryAssets(entry)) {\n const usage = asString(assetField(asset, 'usage')) ?? asString(assetField(asset, 'role'));\n const src = assetUrl(asset);\n if (!src) continue;\n if (wanted.size > 0 && (!usage || !wanted.has(usage))) continue;\n\n images.push({\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n usage,\n });\n }\n\n return images;\n },\n\n /** Attached resources, normalized to name/type/role/href. */\n resources(options: ResourceFieldOptions = {}): ApiResourceLink[] {\n const types = options.types?.length ? new Set(options.types) : undefined;\n\n return entryResources(entry)\n .map((relationship) => {\n const nested = asRecord(relationship.resource);\n const source = Object.keys(nested).length > 0 ? nested : relationship;\n const slug = asString(resourceField(source, 'slug'));\n return {\n name: asString(resourceField(source, 'name')) ?? '',\n type: asString(resourceField(source, 'resourceType')) ?? '',\n role: asString(assetField(relationship, 'role')),\n slug,\n href: slug && options.href ? options.href(slug) : undefined,\n };\n })\n .filter((resource) => Boolean(resource.name))\n .filter((resource) => !types || types.has(resource.type))\n .filter((resource) => !options.role || resource.role === options.role);\n },\n\n /** The first attached resource matching `options`. */\n resource(options: ResourceFieldOptions = {}): ApiResourceLink | undefined {\n return accessor.resources(options)[0];\n },\n\n /** The `metadata_json` blob. */\n metadata(): Record<string, unknown> {\n return entryMetadata(entry);\n },\n\n /** The `data_json` blob. Empty on a list item that was not hydrated. */\n data(): Record<string, unknown> {\n return entryData(entry);\n },\n\n /** Slugs of every collection the entry belongs to. */\n collections(): string[] {\n return collectionSlugs(entry);\n },\n\n /** The entry's membership role within `collectionSlug`. */\n role(collectionSlug: string): string | undefined {\n return collectionRole(entry, collectionSlug);\n },\n };\n\n return accessor;\n}\n","/**\n * The repository factory.\n *\n * `getAll` / `getBySlug` / `getFeatured` is the same forty lines of try / fallback / memoize\n * around every content type on every site. Only the transform differs. This holds the forty\n * lines; the site writes the transform.\n */\n\nimport { errorMessage } from './client.js';\nimport { createFieldAccessor, type FieldAccessor } from './fields.js';\nimport { isFullEntry, type MarvinFetcher } from './fetch.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport type { MarvinContentEntry } from './types.js';\n\nexport type RepositoryOptions<T> = {\n /** Collection slug to load from. */\n collection?: string;\n /**\n * Collection slugs tried in order; the first one that yields entries wins. Lets a site work\n * against workspaces that named the same thing `bench-notes`, `journal`, or `blog`.\n */\n collections?: string[];\n /**\n * Fetch each list item as a full entry before transforming.\n *\n * The collection endpoint returns `PublishedEntryListItem`, which has no `assets[]` or\n * `resources[]` (only their slugs) and, on older servers, no `data_json`; the single-entry\n * endpoint returns `PublishedEntryRead`, which has all of it. So if the transform reads asset\n * roles, resources, or (on an older server) any schema field, this must be `true`. The list is\n * requested with `expand=full`, which returns full entries in the one request; a server or SDK\n * without it (SDK < 4.2) costs one more request per entry. `bySlug` always works from full\n * entries either way.\n */\n hydrate?: boolean;\n /** Build the resolved item from an entry. May be async (e.g. to render markdown). */\n transform: (entry: MarvinContentEntry, fields: FieldAccessor) => T | Promise<T>;\n /** Static data to serve when Marvin is unreachable or the collection is empty. */\n fallback?: () => T[] | Promise<T[]>;\n /** Applied to Marvin and fallback results alike, so ordering doesn't depend on the source. */\n sort?: (a: T, b: T) => number;\n /** Applied to Marvin and fallback results alike. */\n filter?: (item: T) => boolean;\n /** The route an entry resolves to; exposed to the transform as `fields.href`. */\n href?: (slug: string, entry: MarvinContentEntry) => string;\n /** How to read an item's slug for `bySlug`. Defaults to `item.slug`. */\n slugOf?: (item: T) => string | undefined;\n /** How to tell whether an item is featured. Defaults to `item.featured`. */\n isFeatured?: (item: T) => boolean;\n};\n\nexport type Repository<T> = {\n /** Every item, resolved once per process. */\n all(): Promise<T[]>;\n /**\n * One item by slug. Served from `all()` once that has loaded; otherwise a direct entry\n * fetch, falling back to a scan of `all()`.\n */\n bySlug(slug: string): Promise<T | undefined>;\n /** The first featured item, or the first item when none is marked. */\n featured(): Promise<T | undefined>;\n /** Every featured item, in `all()` order. */\n allFeatured(): Promise<T[]>;\n /** Drop the memoized results. Next call re-fetches. */\n reset(): void;\n};\n\nexport type RepositoryContext = {\n fetcher: MarvinFetcher;\n renderMarkdown: MarkdownRenderer;\n};\n\nfunction defaultSlugOf<T>(item: T): string | undefined {\n const slug = (item as { slug?: unknown }).slug;\n return typeof slug === 'string' ? slug : undefined;\n}\n\nfunction defaultIsFeatured<T>(item: T): boolean {\n return Boolean((item as { featured?: unknown }).featured);\n}\n\nexport function createRepository<T>(\n context: RepositoryContext,\n options: RepositoryOptions<T>\n): Repository<T> {\n const { fetcher, renderMarkdown } = context;\n const collections = options.collections ?? (options.collection ? [options.collection] : []);\n const slugOf = options.slugOf ?? defaultSlugOf;\n const isFeatured = options.isFeatured ?? defaultIsFeatured;\n\n /** The loaded list, plus the slugs whose items were built from full entries. */\n type Loaded = { items: T[]; fromFullEntry: Set<string> };\n let loadedPromise: Promise<Loaded> | null = null;\n let allPromise: Promise<T[]> | null = null;\n const bySlugCache = new Map<string, Promise<T | undefined>>();\n\n function arrange(items: T[]): T[] {\n const filtered = options.filter ? items.filter(options.filter) : items;\n return options.sort ? [...filtered].sort(options.sort) : filtered;\n }\n\n function transform(entry: MarvinContentEntry, collection?: string): Promise<T> {\n const slug = (entry as { slug?: string }).slug ?? '';\n const fields = createFieldAccessor(entry, {\n renderMarkdown,\n href: options.href?.(slug, entry),\n collection,\n });\n return Promise.resolve(options.transform(entry, fields));\n }\n\n async function useFallback(): Promise<Loaded> {\n return { items: arrange((await options.fallback?.()) ?? []), fromFullEntry: new Set() };\n }\n\n async function load(): Promise<Loaded> {\n if (collections.length > 0 && fetcher.backend.hasBackend()) {\n try {\n const entries = options.hydrate\n ? await fetcher.hydratedCollectionEntriesFallback(collections)\n : await fetcher.collectionEntriesFallback(collections);\n\n if (entries.length > 0) {\n const items = await Promise.all(entries.map((entry) => transform(entry)));\n const fromFullEntry = new Set<string>();\n items.forEach((item, index) => {\n const slug = slugOf(item);\n if (slug !== undefined && isFullEntry(entries[index])) fromFullEntry.add(slug);\n });\n return { items: arrange(items), fromFullEntry };\n }\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(\n `Collection \"${collections[0]}\" failed, using fallback: ${errorMessage(error)}`\n );\n }\n }\n\n return useFallback();\n }\n\n function findInList(items: T[], slug: string): T | undefined {\n return items.find((item) => slugOf(item) === slug);\n }\n\n async function loadBySlug(slug: string): Promise<T | undefined> {\n // Once a hydrated list has loaded, every slug in it is already transformed from a full entry —\n // a per-entry fetch would be a second request for the same content (~1.4s per detail page on\n // a 280-entry site). An item built from a bare list item is NOT served: it has no assets[] or\n // resources[], so a detail page would lose its role-picked images and resource links.\n let listed: T | undefined;\n if (loadedPromise) {\n const loaded = await loadedPromise;\n listed = findInList(loaded.items, slug);\n if (listed && loaded.fromFullEntry.has(slug)) return listed;\n }\n\n if (fetcher.backend.hasBackend()) {\n try {\n const entry = await fetcher.entry(slug);\n if (entry) return await transform(entry);\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(`Entry \"${slug}\" failed, using fallback: ${errorMessage(error)}`);\n }\n }\n\n // No backend, no such entry, or a transform that blew up: fall back to the resolved list,\n // which is either Marvin's or the static data — the caller doesn't need to care which.\n return listed ?? findInList(await all(), slug);\n }\n\n function all(): Promise<T[]> {\n if (!allPromise) {\n loadedPromise = load();\n allPromise = loadedPromise.then((loaded) => loaded.items);\n }\n return allPromise;\n }\n\n return {\n all,\n\n bySlug(slug: string): Promise<T | undefined> {\n let cached = bySlugCache.get(slug);\n if (!cached) {\n cached = loadBySlug(slug);\n bySlugCache.set(slug, cached);\n }\n return cached;\n },\n\n async allFeatured(): Promise<T[]> {\n return (await all()).filter(isFeatured);\n },\n\n async featured(): Promise<T | undefined> {\n const items = await all();\n return items.find(isFeatured) ?? items[0];\n },\n\n reset() {\n allPromise = null;\n loadedPromise = null;\n bySlugCache.clear();\n },\n };\n}\n","import { entryEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { createMarkdownRenderer, preserveSoftBreaks, type MarkdownRenderer } from './markdown.js';\nimport { asString, assetUrl, entryField, selectAssetByRole } from './normalize.js';\nimport type { SectionLanding } from './types.js';\n\nexport type SectionLandingOptions = {\n /** Asset role to use as the hero image. Default `'hero'`. */\n heroRole?: string;\n /** Field holding the intro copy. Default `'body'`, falling back to the entry summary. */\n bodyField?: string;\n};\n\n/**\n * Resolve a section-landing header from a Marvin `page` entry with the given slug.\n *\n * The intro is rendered with authored line breaks preserved — landing copy is written as a few\n * deliberate lines, not a soft-wrapped paragraph, and losing those breaks changes the layout.\n */\nexport async function loadSectionLanding(\n fetcher: MarvinFetcher,\n slug: string,\n renderMarkdown: MarkdownRenderer = createMarkdownRenderer(),\n options: SectionLandingOptions = {}\n): Promise<SectionLanding> {\n const entry = await fetcher.entry(slug);\n if (!entry) return {};\n\n const source =\n asString(entryField(entry, options.bodyField ?? 'body')) ?? asString(entry.summary);\n\n return {\n title: asString(entry.title),\n introHtml: source\n ? await renderMarkdown(preserveSoftBreaks(source), { embeds: entryEmbeds(entry) })\n : undefined,\n hero: assetUrl(selectAssetByRole(entry, options.heroRole ?? 'hero')),\n };\n}\n","/**\n * Site identity + SEO.\n *\n * Marvin's site configuration is optional almost everywhere; a template that had to guard every\n * field would be unreadable. This resolves it once into an `ApiSite` where title, description,\n * siteName, locale and timezone are always present. Logo, favicon and SEO image are the URLs\n * Marvin returns, used as-is.\n */\n\nimport type { MarvinSite, SiteConfiguration } from '@inneropen/marvin-sdk';\nimport { siteEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { asRecord, asString } from './normalize.js';\nimport type { ApiSeo, ApiSite } from './types.js';\n\nexport type SiteOptions = {\n /**\n * Static identity to fall back to, field by field, when Marvin is unreachable or leaves a\n * value unset. Anything omitted here falls through to a neutral default.\n */\n fallback?: Partial<ApiSite> | (() => Partial<ApiSite>);\n /** Default locale when neither Marvin nor the fallback sets one. Default `'en-US'`. */\n defaultLocale?: string;\n /** Default timezone when neither Marvin nor the fallback sets one. Default `'UTC'`. */\n defaultTimezone?: string;\n};\n\n/**\n * Build resolved SEO from Marvin's raw SiteSeo block, applying identity fallbacks so title,\n * description and siteName are never empty. Absent → sensible defaults (robots `index,follow`;\n * og type `website`; large-image Twitter card).\n */\nexport function buildSeo(\n raw: Record<string, unknown> | undefined,\n identity: { title: string; description: string; canonicalUrl?: string; siteName: string }\n): ApiSeo {\n const seo = raw ?? {};\n const verification = asRecord(seo.verification);\n\n return {\n title: asString(seo.title) ?? identity.title,\n titleTemplate: asString(seo.titleTemplate),\n description: asString(seo.description) ?? identity.description,\n keywords: Array.isArray(seo.keywords)\n ? seo.keywords.filter((keyword): keyword is string => typeof keyword === 'string')\n : [],\n robots: asString(seo.robots) ?? 'index,follow',\n image: asString(seo.image),\n imageAlt: asString(seo.imageAlt),\n ogType: asString(seo.ogType) ?? 'website',\n siteName: asString(seo.siteName) ?? identity.siteName,\n twitterCard: asString(seo.twitterCard) ?? 'summary_large_image',\n twitterHandle: asString(seo.twitterHandle),\n twitterCreator: asString(seo.twitterCreator),\n fbAppId: asString(seo.fbAppId),\n themeColor: asString(seo.themeColor),\n publisher: asString(seo.publisher),\n canonicalUrl: identity.canonicalUrl,\n verification: {\n google: asString(verification.google),\n bing: asString(verification.bing),\n pinterest: asString(verification.pinterest),\n yandex: asString(verification.yandex),\n },\n };\n}\n\nfunction normalizeSocial(social: unknown): Record<string, string> {\n return Object.fromEntries(\n Object.entries(asRecord(social)).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0\n )\n );\n}\n\n/** The offline site: the caller's fallback, with neutral defaults under it. */\nexport function buildFallbackSite(options: SiteOptions = {}): ApiSite {\n const provided = typeof options.fallback === 'function' ? options.fallback() : options.fallback;\n const fallback = provided ?? {};\n const title = fallback.title ?? fallback.name ?? 'Untitled site';\n const description = fallback.description ?? '';\n\n return {\n ...fallback,\n name: fallback.name ?? title,\n title,\n description,\n locale: fallback.locale ?? options.defaultLocale ?? 'en-US',\n timezone: fallback.timezone ?? options.defaultTimezone ?? 'UTC',\n social: fallback.social ?? {},\n metadata: fallback.metadata ?? {},\n seo:\n fallback.seo ??\n buildSeo(undefined, { title, description, siteName: fallback.name ?? title }),\n };\n}\n\nfunction transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite {\n const config: SiteConfiguration = marvinSite.site;\n const metadata = asRecord(config.metadataJson);\n const title =\n asString(config.title) ?? asString(marvinSite.workspace?.name) ?? fallback.title;\n const description = asString(config.description) ?? fallback.description;\n\n return {\n workspaceSlug: asString(marvinSite.workspace?.slug),\n workspaceName: asString(marvinSite.workspace?.name),\n name: title,\n title,\n tagline: asString(config.tagline) ?? fallback.tagline,\n description,\n canonicalUrl: asString(config.canonicalUrl) ?? fallback.canonicalUrl,\n logo: asString(config.logo) ?? fallback.logo,\n favicon: asString(config.favicon) ?? fallback.favicon,\n locale: asString(config.locale) ?? fallback.locale,\n timezone: asString(config.timezone) ?? fallback.timezone,\n email: asString(config.contactEmail) ?? fallback.email,\n imprint: asString(metadata.imprint) ?? fallback.imprint,\n // Marvin's social map only: once Marvin answers, the static one must not leak back in.\n social: normalizeSocial(config.social),\n // Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older\n // SDKs drop the typed field but pass the blob through untouched, and workspaces configured\n // before the typed field existed still only have the blob.\n seo: buildSeo(asRecord((config as { seo?: unknown }).seo ?? metadata.seo), {\n title,\n description,\n canonicalUrl: asString(config.canonicalUrl),\n siteName: title,\n }),\n // Older servers and SDKs don't send it; `siteEmbeds` returns undefined then.\n embeds: siteEmbeds(\n (config as { embeds?: unknown }).embeds ?? (marvinSite as { embeds?: unknown }).embeds\n ),\n metadata,\n };\n}\n\n/** A usable image reference: an absolute URL, a root-relative path or a data URI. */\nfunction isUrlLike(value: string): boolean {\n return /^https?:\\/\\//.test(value) || value.startsWith('/') || value.startsWith('data:');\n}\n\nexport type SiteLoader = {\n get(): Promise<ApiSite>;\n /**\n * The build's one \"is Marvin up?\" answer: true when the site configuration came from Marvin.\n * When it did, Marvin's content is final (an empty or hidden collection shows nothing); the\n * static fallbacks stand in only when it didn't.\n */\n online(): Promise<boolean>;\n reset(): void;\n};\n\nexport function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions = {}): SiteLoader {\n const fallback = buildFallbackSite(options);\n let promise: Promise<ApiSite> | null = null;\n let fromMarvin = false;\n\n async function load(): Promise<ApiSite> {\n const marvinSite = await fetcher.site();\n fromMarvin = marvinSite !== null;\n if (!marvinSite) return fallback;\n\n const site = transformMarvinSite(marvinSite, fallback);\n\n // Brand-asset registry: every `brand.<name>` that is a URL (or root-relative path / data URI),\n // used as-is. Marvin no longer serves assets by slug, so a bare slug can't be resolved here and\n // is left out; `site.logo`, `site.favicon` and `site.seo.image` arrive already resolved.\n const brand: Record<string, string> = {};\n for (const [name, value] of Object.entries(asRecord(site.metadata.brand))) {\n if (typeof value === 'string' && isUrlLike(value.trim())) brand[name] = value.trim();\n }\n\n return {\n ...site,\n brand,\n // Convenience aliases for common callers; all of these also live in `site.brand`.\n logo: brand.logo ?? site.logo,\n favicon: brand.favicon ?? site.favicon,\n seal: brand.seal ?? site.seal,\n };\n }\n\n return {\n get() {\n // Memoized: components like a per-card brand stamp render many instances per page, so\n // this keeps brand-slug resolution to once per process rather than once per render.\n promise ??= load();\n return promise;\n },\n async online() {\n await this.get();\n return fromMarvin;\n },\n reset() {\n promise = null;\n fromMarvin = false;\n },\n };\n}\n","/**\n * `@inneropen/marvin-astro` — site integration for Marvin CMS.\n *\n * Sits above `@inneropen/marvin-sdk` (transport). This layer is the part every Marvin-backed site\n * rewrites: repositories with a static fallback, site chrome from collections, and payload\n * normalization. Astro components (forms, media embeds, entry-type renderers) live under\n * `@inneropen/marvin-astro/components`.\n *\n * ```ts\n * // src/lib/content.ts\n * export const marvin = createMarvinContent({ site: { fallback: staticSite } });\n *\n * export const posts = marvin.repository({\n * collections: ['bench-notes', 'journal', 'blog'],\n * hydrate: true,\n * href: (slug) => `/bench-notes/${slug}`,\n * fallback: () => staticPosts,\n * transform: async (entry, f) => ({\n * slug: entry.slug,\n * title: entry.title ?? 'Untitled',\n * noteNumber: f.string('noteNumber'),\n * bodyHtml: await f.markdown('body'),\n * image: f.image({ roles: ['hero', 'featured', 'card'] }),\n * }),\n * });\n * ```\n */\n\nimport { createBackend } from './client.js';\nimport { createChromeLoader, type ChromeOptions } from './chrome.js';\nimport { createFetcher } from './fetch.js';\nimport { createMarkdownRenderer } from './markdown.js';\nimport { createRepository, type Repository, type RepositoryOptions } from './repository.js';\nimport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nimport { createSiteLoader, type SiteOptions } from './site.js';\nimport type { MarvinAstroConfig } from './config.js';\nimport type { ApiSite, ApiSiteChrome, SectionLanding } from './types.js';\n\nexport type MarvinContentOptions = MarvinAstroConfig & {\n site?: SiteOptions;\n chrome?: ChromeOptions;\n};\n\nexport type MarvinContent = ReturnType<typeof createMarvinContent>;\n\n/**\n * Build the content layer for one Marvin workspace.\n *\n * Connection settings come from `MARVIN_API_URL`, `MARVIN_SITE_CLIENT_TOKEN` and\n * `MARVIN_WORKSPACE_SLUG` unless passed explicitly. Everything returned degrades to its static\n * fallback when the backend is unreachable, so a site still builds offline.\n */\nexport function createMarvinContent(options: MarvinContentOptions = {}) {\n const backend = createBackend(options);\n const fetcher = createFetcher(backend);\n const renderMarkdown = createMarkdownRenderer(backend.config.markdown);\n const siteLoader = createSiteLoader(fetcher, options.site);\n const chromeLoader = createChromeLoader(fetcher, siteLoader, options.chrome);\n const repositories: Repository<unknown>[] = [];\n\n return {\n /** The resolved configuration, with the token still in place — don't log it. */\n config: backend.config,\n /** Latch control and the raw SDK client. */\n backend,\n /** Guarded, never-throwing wrappers around every published endpoint. */\n fetch: fetcher,\n renderMarkdown,\n\n /** True when Marvin is configured and not currently latched out as unreachable. */\n hasBackend: () => backend.hasBackend(),\n\n /** Site identity, SEO and logo/favicon URLs. Memoized per process. */\n getSite(): Promise<ApiSite> {\n return siteLoader.get();\n },\n\n /** Navigation, footer, legal, social and inquiry links. Memoized per process. */\n getSiteChrome(): Promise<ApiSiteChrome> {\n return chromeLoader.get();\n },\n\n /** Title/intro/hero for an index page, driven by a `page` entry of the same slug. */\n getSectionLanding(slug: string, sectionOptions?: SectionLandingOptions): Promise<SectionLanding> {\n return loadSectionLanding(fetcher, slug, renderMarkdown, sectionOptions);\n },\n\n /** A collection-backed content repository: `all()` / `bySlug()` / `featured()`. */\n repository<T>(repositoryOptions: RepositoryOptions<T>): Repository<T> {\n const repository = createRepository<T>({ fetcher, renderMarkdown }, repositoryOptions);\n repositories.push(repository as Repository<unknown>);\n return repository;\n },\n\n /** Drop every memoized result and re-open the failure latch. */\n reset() {\n backend.clearLatch();\n siteLoader.reset();\n chromeLoader.reset();\n for (const repository of repositories) repository.reset();\n },\n };\n}\n\nexport {\n createBackend,\n errorMessage,\n isNetworkFailure,\n type MarvinBackend,\n} from './client.js';\nexport {\n DEFAULT_DEV_RETRY_MS,\n DEFAULT_HYDRATE_CONCURRENCY,\n ENV_KEYS,\n describeConfig,\n readEnv,\n resolveConfig,\n type MarvinAstroConfig,\n type MarvinLogger,\n type ResolvedConfig,\n} from './config.js';\nexport { createFetcher, isFullEntry, type HydratedEntry, type MarvinFetcher } from './fetch.js';\nexport {\n createFieldAccessor,\n type FieldAccessor,\n type IconFieldOptions,\n type ImageFieldOptions,\n type MarkdownFieldOptions,\n type ResourceFieldOptions,\n} from './fields.js';\nexport { formatDisplayDate, selectValuesForPage } from './format.js';\nexport {\n createMarkdownRenderer,\n preserveSoftBreaks,\n type MarkdownOptions,\n type MarkdownRenderer,\n type RenderMarkdownOptions,\n} from './markdown.js';\nexport { entryEmbeds, findEmbed, siteEmbeds } from './embeds.js';\nexport * from './normalize.js';\nexport {\n createRepository,\n type Repository,\n type RepositoryContext,\n type RepositoryOptions,\n} from './repository.js';\nexport {\n buildFallbackSite,\n buildSeo,\n createSiteLoader,\n type SiteLoader,\n type SiteOptions,\n} from './site.js';\nexport {\n createChromeLoader,\n defaultResolveHref,\n metadataLink,\n socialLinkFromKey,\n toNavigationLink,\n type ChromeLoader,\n type ChromeOptions,\n type HrefContext,\n type NavigationContext,\n type NavigationLinkInput,\n type ResolveHref,\n type SocialLinkInput,\n} from './chrome.js';\nexport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nexport type * from './types.js';\n"],"mappings":";AAaA,SAAS,0BAA0B;;;ACG5B,IAAM,WAAW;AAAA,EACtB,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,OAAO;AACT;AAGO,IAAM,uBAAuB;AAG7B,IAAM,8BAA8B;AAqD3C,SAAS,UAA+C;AACtD,MAAI;AACF,WAAQ,YAA6D;AAAA,EACvE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAA6D;AACpE,MAAI;AACF,WAAO,OAAO,YAAY,cAAc,QAAQ,MAAM;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,SAAS,OAAoC;AACpD,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,IAAI,QAAQ;AACxE;AAGO,SAAS,QACd,KACA,UACoB;AACpB,SAAO,SAAS,WAAW,GAAG,CAAC,KAAK,SAAS,QAAQ,IAAI,GAAG,CAAC,KAAK,SAAS,WAAW,IAAI,GAAG,CAAC;AAChG;AAEA,SAAS,aAAa,UAAwD;AAC5E,QAAM,WAAW,QAAQ,GAAG;AAC5B,MAAI,OAAO,aAAa,UAAW,QAAO;AAC1C,QAAM,OAAO,QAAQ,YAAY,QAAQ,KAAK,QAAQ,QAAQ,QAAQ;AACtE,SAAO,SAAS;AAClB;AAEO,SAAS,cAAc,UAA6B,CAAC,GAAmB;AAC7E,QAAM,MAAM,QAAQ;AACpB,QAAM,SAAS,QAAQ,UAAU,QAAQ,SAAS,QAAQ,GAAG,KAAK;AAClE,QAAM,kBAAkB,QAAQ,mBAAmB,QAAQ,SAAS,iBAAiB,GAAG,KAAK;AAC7F,QAAM,gBAAgB,QAAQ,iBAAiB,QAAQ,SAAS,eAAe,GAAG,KAAK;AACvF,QAAM,QAAQ,QAAQ,SAAS,QAAQ,SAAS,OAAO,GAAG,MAAM;AAEhE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cACE,QAAQ,iBACP,aAAa,GAAG,IAAI,OAAO,oBAAoB;AAAA,IAClD,oBAAoB,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,sBAAsB,2BAA2B,CAAC;AAAA,IACrG,QAAQ,QAAQ,UAAU;AAAA,IAC1B,UAAU,QAAQ;AAAA,IAClB,KAAK,QAAQ,QAAQ,MAAM,KAAK,IAAI;AAAA,IACpC,cAAc,QAAQ;AAAA,IACtB,YAAY,QAAQ,UAAU,mBAAmB,aAAa;AAAA,EAChE;AACF;AAGO,SAAS,eAAe,QAAiD;AAC9E,QAAM,QAAQ,OAAO;AACrB,SAAO;AAAA,IACL,CAAC,SAAS,MAAM,GAAG,OAAO,UAAU;AAAA,IACpC,CAAC,SAAS,eAAe,GAAG,QAAQ,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,OAAO,EAAE,CAAC,KAAK;AAAA,IAC/E,CAAC,SAAS,aAAa,GAAG,OAAO,iBAAiB;AAAA,IAClD,QAAQ,OAAO;AAAA,EACjB;AACF;;;AD9HO,SAAS,aAAa,OAAwB;AACnD,MAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,SAAO,OAAO,KAAK;AACrB;AAMO,SAAS,iBAAiB,OAAyB;AACxD,QAAM,UAAU,aAAa,KAAK,EAAE,YAAY;AAChD,SACE,QAAQ,SAAS,eAAe,KAChC,QAAQ,SAAS,cAAc,KAC/B,QAAQ,SAAS,cAAc,KAC/B,QAAQ,SAAS,WAAW,KAC5B,QAAQ,SAAS,WAAW;AAEhC;AAiBO,SAAS,cAAc,UAA6B,CAAC,GAAkB;AAC5E,QAAM,SAAS,cAAc,OAAO;AACpC,MAAI,SAA8B;AAClC,MAAI,YAA2B;AAC/B,MAAI,YAAY;AAEhB,WAAS,YAAqB;AAC5B,QAAI,cAAc,KAAM,QAAO;AAC/B,QAAI,OAAO,iBAAiB,OAAO,kBAAmB,QAAO;AAC7D,QAAI,OAAO,IAAI,IAAI,YAAY,OAAO,aAAc,QAAO;AAG3D,gBAAY;AACZ,WAAO;AAAA,EACT;AAEA,WAAS,aAAsB;AAC7B,QAAI,OAAO,SAAS,CAAC,WAAW;AAC9B,kBAAY;AACZ,aAAO,OAAO,IAAI,6BAA6B,eAAe,MAAM,CAAC;AAAA,IACvE;AACA,QAAI,CAAC,OAAO,WAAY,QAAO;AAC/B,WAAO,CAAC,UAAU;AAAA,EACpB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AACX,kBAAY;AAAA,IACd;AAAA,IACA,SAAS,OAAgB;AACvB,UAAI,iBAAiB,KAAK,EAAG,aAAY,OAAO,IAAI;AAAA,IACtD;AAAA,IACA,KAAK,SAAiB;AACpB,aAAO,OAAO,KAAK,kBAAkB,OAAO,EAAE;AAAA,IAChD;AAAA,IACA,SAAuB;AACrB,UAAI,CAAC,OAAO,YAAY;AACtB,cAAM,IAAI;AAAA,UACR;AAAA,QAEF;AAAA,MACF;AACA,iBAAY,OAAO,eACf,OAAO,aAAa;AAAA,QAClB,QAAQ,OAAO;AAAA,QACf,iBAAiB,OAAO;AAAA,QACxB,eAAe,OAAO;AAAA,QACtB,OAAO,OAAO;AAAA,MAChB,CAAC,IACD,mBAAmB;AAAA,QACjB,QAAQ,OAAO;AAAA,QACf,iBAAiB,OAAO;AAAA,QACxB,eAAe,OAAO;AAAA,QACtB,OAAO,OAAO;AAAA,MAChB,CAAC;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;AE1GO,SAAS,SAAS,OAAoC;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,IAAI,QAAQ;AACxE;AAEO,SAAS,SAAS,OAAyC;AAChE,SAAO,SAAS,OAAO,UAAU,WAAY,QAAoC,CAAC;AACpF;AAEO,SAAS,SAAS,OAAoC;AAC3D,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,UAAM,SAAS,OAAO,KAAK;AAC3B,WAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAAA,EAC5C;AACA,SAAO;AACT;AAEO,SAAS,cAAc,OAAsC;AAClE,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAElC,QAAM,QAAQ,MAAM;AAAA,IAClB,CAAC,SAAyB,OAAO,SAAS,YAAY,KAAK,KAAK,EAAE,SAAS;AAAA,EAC7E;AACA,SAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;AAGO,SAAS,cAAc,OAAoD;AAChF,QAAM,QAAQ;AACd,SAAO,SAAS,MAAM,YAAY,MAAM,YAAY;AACtD;AAGO,SAAS,UAAU,OAAoD;AAC5E,QAAM,QAAQ;AACd,SAAO,SAAS,MAAM,QAAQ,MAAM,QAAQ;AAC9C;AAMO,SAAS,WAAwB,OAA2B,KAA4B;AAC7F,QAAM,WAAY,MAA+C;AACjE,MAAI,OAAO,aAAa,YAAY;AAClC,WAAO,SAAS,KAAK,OAAO,GAAG;AAAA,EACjC;AACA,SAAO,UAAU,KAAK,EAAE,GAAG;AAC7B;AAUO,SAAS,MAAmB,OAA2B,KAA4B;AACxF,QAAM,QAAQ,WAAW,OAAO,GAAG;AACnC,MAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,GAAI,QAAO;AAClE,QAAM,WAAW,cAAc,KAAK,EAAE,GAAG;AACzC,SAAQ,aAAa,KAAK,SAAY;AACxC;AAMO,SAAS,WAAwB,OAAgB,KAA4B;AAClF,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAEhD,QAAM,QAAQ;AACd,QAAM,SAAS,SAAS,MAAM,KAAK;AACnC,SAAQ,MAAM,GAAG,KACf,SAAS,MAAM,aAAa,EAAE,GAAG,KACjC,SAAS,MAAM,iBAAiB,EAAE,GAAG,KACrC,SAAS,MAAM,YAAY,EAAE,GAAG,KAChC,SAAS,MAAM,SAAS,EAAE,GAAG,KAC7B,SAAS,MAAM,QAAQ,EAAE,GAAG,KAC5B,OAAO,GAAG,KACV,SAAS,OAAO,YAAY,EAAE,GAAG,KACjC,SAAS,OAAO,SAAS,EAAE,GAAG,KAC9B,SAAS,OAAO,QAAQ,EAAE,GAAG;AACjC;AAGO,SAAS,cAA2B,UAAmB,KAA4B;AACxF,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AAEtD,QAAM,QAAQ;AACd,QAAM,SAAS,SAAS,MAAM,QAAQ;AACtC,SAAQ,MAAM,GAAG,KACf,SAAS,MAAM,aAAa,EAAE,GAAG,KACjC,SAAS,MAAM,YAAY,EAAE,GAAG,KAChC,SAAS,MAAM,SAAS,EAAE,GAAG,KAC7B,SAAS,MAAM,QAAQ,EAAE,GAAG,KAC5B,OAAO,GAAG,KACV,SAAS,OAAO,YAAY,EAAE,GAAG,KACjC,SAAS,OAAO,SAAS,EAAE,GAAG,KAC9B,SAAS,OAAO,QAAQ,EAAE,GAAG;AACjC;AAMO,SAAS,gBAAgB,OAAqC;AACnE,QAAM,SAAU,MAAwC;AACxD,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,OAAO,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ;AAAA,EACzE;AAEA,QAAM,MAAO,MAAoC;AACjD,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AAEjC,SAAO,IACJ;AAAA,IAAI,CAAC,SACJ,OAAO,SAAS,WAAW,OAAQ,SAAS,SAAS,IAAI,EAAE,UAAU,EAAE,QAAQ,SAAS,IAAI,EAAE;AAAA,EAChG,EACC,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ;AAC9D;AAOO,SAAS,eACd,OACA,gBACoB;AACpB,QAAM,MAAO,MAAoC;AACjD,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO;AAEhC,aAAW,QAAQ,KAAK;AACtB,QAAI,OAAO,SAAS,SAAU;AAC9B,UAAM,SAAS,SAAS,IAAI;AAC5B,UAAM,OAAO,SAAS,OAAO,IAAI,KAAK,SAAS,SAAS,OAAO,UAAU,EAAE,IAAI;AAC/E,QAAI,SAAS,gBAAgB;AAC3B,aAAO,SAAS,SAAS,OAAO,aAAa,EAAE,IAAI,KAAK,SAAS,OAAO,IAAI;AAAA,IAC9E;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,WAAW,OAAoC;AAC7D,QAAM,SAAS,SAAS,KAAK;AAC7B,MAAI,OAAQ,QAAO;AAEnB,QAAM,QAAQ,SAAS,KAAK;AAC5B,QAAM,IAAI,OAAO,MAAM,MAAM,WAAW,MAAM,IAAI;AAClD,QAAM,IAAI,OAAO,MAAM,MAAM,WAAW,MAAM,IAAI;AAClD,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO;AAEnC,SAAO,GAAG,IAAI,IAAI,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,IAAI,GAAG;AACvD;AAEO,SAAS,YAAY,OAAsD;AAChF,QAAM,SAAU,MAA+B;AAC/C,SAAO,MAAM,QAAQ,MAAM,IAAK,SAAuC,CAAC;AAC1E;AAEO,SAAS,eAAe,OAAsD;AACnF,QAAM,YAAa,MAAkC;AACrD,SAAO,MAAM,QAAQ,SAAS,IAAK,YAA0C,CAAC;AAChF;AAEA,SAAS,kBAAkB,QAA4B,OAAgB,KAAyB;AAC9F,MAAI,CAAC,SAAS,CAAC,KAAK,OAAQ,QAAO;AACnC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,WAAW,SAAS,QAAQ,KAAK,SAAS,MAAM,CAAC;AAC1D;AAEA,SAAS,aAAa,OAAgC,SAAsC;AAC1F,QAAM,OAAO,SAAS,WAAW,OAAO,MAAM,CAAC;AAC/C,QAAM,QAAQ,SAAS,WAAW,OAAO,OAAO,CAAC;AACjD,QAAM,OAAO,SAAS,WAAW,OAAO,WAAW,CAAC;AACpD,QAAM,WAAW,SAAS,WAAW,OAAO,UAAU,CAAC;AAEvD,QAAM,kBAAkB,QAAQ,QAAQ,QAAQ,QAAQ,OAAO,MAAM;AACrE,QAAM,mBAAmB,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,MAAM;AACxE,QAAM,cAAc,kBAAkB,MAAM,QAAQ,MAAM,QAAQ,KAAK;AACvE,QAAM,eAAe,kBAAkB,OAAO,QAAQ,OAAO,QAAQ,MAAM;AAC3E,QAAM,cAAc,kBAAkB,MAAM,QAAQ,MAAM,QAAQ,KAAK;AACvE,QAAM,cAAc,QAAQ,WAAW,aAAa,QAAQ,WAAW;AACvE,QAAM,iBAAiB,QAAQ,QAAQ,YAAY,KAAK,CAAC,QAAQ,CAAC;AAIlE,QAAM,sBACJ,mBAAmB,mBAAmB,eAAe,gBAAgB,iBAAiB;AAExF,SAAO,uBAAuB,eAAe;AAC/C;AAEO,SAAS,iBACd,OACA,UAA8B,CAAC,GACM;AACrC,SAAO,YAAY,KAAK,EAAE,KAAK,CAAC,UAAU,aAAa,OAAO,OAAO,CAAC;AACxE;AAOO,SAAS,kBACd,UACG,OACkC;AACrC,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,YAAY,KAAK,EAAE,KAAK,CAAC,UAAU,SAAS,WAAW,OAAO,MAAM,CAAC,MAAM,IAAI;AAC7F,QAAI,MAAO,QAAO;AAAA,EACpB;AACA,SAAO;AACT;AAGO,SAAS,oBACd,OACqC;AACrC,QAAM,WAAW,SAAU,MAAsC,aAAa;AAC9E,SAAO,OAAO,KAAK,QAAQ,EAAE,SAAS,IAAI,WAAW;AACvD;AAEO,SAAS,iBACd,OACA,UAAsD,CAAC,GAClB;AACrC,SACE,iBAAiB,OAAO,EAAE,GAAG,SAAS,MAAM,QAAQ,CAAC,KACrD,iBAAiB,OAAO,EAAE,GAAG,SAAS,OAAO,CAAC,OAAO,GAAG,cAAc,KAAK,CAAC;AAEhF;AAEO,SAAS,gBAAgB,OAAgE;AAC9F,SACE,iBAAiB,OAAO;AAAA,IACtB,OAAO,CAAC,MAAM;AAAA,IACd,OAAO,CAAC,OAAO,OAAO;AAAA,IACtB,UAAU;AAAA,IACV,cAAc;AAAA,EAChB,CAAC,KACD,iBAAiB,OAAO;AAAA,IACtB,OAAO,CAAC,MAAM;AAAA,IACd,OAAO,CAAC,OAAO,OAAO;AAAA,IACtB,cAAc;AAAA,EAChB,CAAC;AAEL;AAEO,SAAS,SAAS,OAAoC;AAC3D,SAAO,SAAS,WAAW,OAAO,WAAW,CAAC;AAChD;AAEO,SAAS,SAAS,OAAgB,UAAuC;AAC9E,SAAO,SAAS,WAAW,OAAO,SAAS,CAAC,KAAK;AACnD;AAEO,SAAS,aAAa,UAAuC;AAClE,SAAO,SAAS,cAAc,UAAU,MAAM,CAAC;AACjD;AAEO,SAAS,eAAe,MAAuB;AACpD,SAAO,0BAA0B,KAAK,IAAI;AAC5C;;;ACxMO,SAAS,iBAAiB,MAA8C;AAC7E,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,UAAU,eAAe,KAAK,IAAI;AAAA,IAClC,MAAM,KAAK;AAAA,EACb;AACF;AAGO,IAAM,qBAAkC,CAAC,QAAQ,YAAY;AAClE,QAAM,SAAS,QAAQ,YAAY,KAAK,CAAC,SAAS,CAAC,QAAQ,eAAe,IAAI,IAAI,CAAC;AACnF,SAAO,SAAS,IAAI,MAAM,IAAI,QAAQ,IAAI,KAAK,IAAI,QAAQ,IAAI;AACjE;AAGO,SAAS,aAAa,OAA+C;AAC1E,QAAM,OAAO,SAAS,KAAK;AAC3B,QAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,QAAM,OAAO,SAAS,KAAK,IAAI;AAC/B,MAAI,CAAC,SAAS,CAAC,KAAM,QAAO;AAE5B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa,SAAS,KAAK,WAAW,KAAK,SAAS,KAAK,OAAO;AAAA,IAChE,UAAU,eAAe,IAAI;AAAA,EAC/B;AACF;AAGA,SAAS,YAAY,KAAqB;AACxC,SAAO,IAAI,QAAQ,SAAS,GAAG,EAAE,QAAQ,SAAS,CAAC,WAAW,OAAO,YAAY,CAAC;AACpF;AAEO,SAAS,kBAAkB,KAAa,MAA6B;AAC1E,SAAO;AAAA,IACL,OAAO,YAAY,GAAG;AAAA,IACtB;AAAA,IACA,MAAM;AAAA,IACN,UAAU,eAAe,IAAI;AAAA,EAC/B;AACF;AAMO,SAAS,cAAc,MAAc,OAAuB;AACjE,MAAI,KAAK,WAAW,SAAS,EAAG,QAAO;AACvC,MAAI;AACF,UAAM,OAAO,IAAI,IAAI,IAAI,EAAE,SAAS,QAAQ,cAAc,EAAE;AAC5D,WAAO,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAAM,YAAY;AAAA,EACjD,QAAQ;AACN,WAAO,MAAM,YAAY;AAAA,EAC3B;AACF;AAOO,SAAS,mBACd,SACA,YACA,UAAyB,CAAC,GACZ;AACd,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,mBAAmB,QAAQ,oBAAoB;AACrD,QAAM,mBAAmB,QAAQ,oBAAoB;AACrD,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,wBAAwB,QAAQ,yBAAyB;AAC/D,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,iBAAiB,oBAAI,IAAI,CAAC,gBAAgB,kBAAkB,gBAAgB,CAAC;AACnF,QAAM,WAAW,QAAQ,YAAY,CAAC;AAEtC,MAAI,UAAyC;AAE7C,WAAS,YACP,OACA,gBACA,SACmB;AACnB,UAAM,OAAO,MAAM,QAAQ;AAC3B,UAAM,OACJ,SAAS,WAAW,OAAO,MAAM,CAAC,KAClC,SAAS,WAAW,OAAO,KAAK,CAAC,KACjC,SAAS,WAAW,OAAO,MAAM,CAAC,KAClC,YAAY,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,gBAAgB,KAAK;AAAA,MAClC;AAAA,IACF,CAAC;AACH,UAAM,QACJ,SAAS,WAAW,OAAO,OAAO,CAAC,KACnC,SAAS,SAAU,MAAqC,YAAY,EAAE,KAAK,KAC3E,SAAS,MAAM,KAAK,KACpB;AAEF,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,aACE,SAAS,MAAM,OAAO,KAAK,SAAU,MAAoC,WAAW;AAAA,MACtF,UAAU,eAAe,IAAI;AAAA,MAC7B,MAAM,eAAe,OAAO,cAAc;AAAA,IAC5C;AAAA,EACF;AAEA,iBAAe,qBACb,MACA,SACA,QACmE;AACnE,QAAI,CAAC,OAAQ,QAAO,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,EAAE;AAI7C,UAAM,UAAU,MAAM,QAAQ,0BAA0B,IAAI;AAC5D,WAAO,EAAE,OAAO,QAAQ,IAAI,CAAC,UAAU,YAAY,OAAO,MAAM,OAAO,CAAC,GAAG,QAAQ;AAAA,EACrF;AAEA,WAAS,iBAAiB,OAAmD;AAC3E,QAAI,MAAM,UAAU,sBAAuB,QAAO,CAAC,KAAK;AAExD,UAAM,WAAW,KAAK,KAAK,MAAM,SAAS,CAAC;AAC3C,WAAO,CAAC,MAAM,MAAM,GAAG,QAAQ,GAAG,MAAM,MAAM,QAAQ,CAAC;AAAA,EACzD;AAGA,iBAAe,cAAc,QAAkD;AAC7E,UAAM,SAAS,MAAM,qBAAqB,kBAAkB,UAAU,MAAM;AAC5E,QAAI,OAAO,MAAM,WAAW,EAAG,QAAO;AAEtC,WAAO,OAAO,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,MACxC,GAAG;AAAA,MACH,MACE,SAAS,WAAW,OAAO,QAAQ,KAAK,GAAG,MAAM,CAAC,KAAK,cAAc,KAAK,MAAM,KAAK,KAAK;AAAA,IAC9F,EAAE;AAAA,EACJ;AAGA,WAAS,gBAAgB,MAAe,QAAkC;AACxE,UAAM,QAAQ,OAAO,QAAQ,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,kBAAkB,KAAK,IAAI,CAAC;AAC3F,QAAI,KAAK,SAAS,CAAC,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,OAAO,GAAG;AAC9D,YAAM,KAAK,kBAAkB,SAAS,UAAU,KAAK,KAAK,EAAE,CAAC;AAAA,IAC/D;AACA,QAAI,MAAM,SAAS,KAAK,OAAQ,QAAO;AAEvC,YAAQ,SAAS,eAAe,CAAC,GAAG,IAAI,CAAC,UAAU;AAAA,MACjD,GAAG,iBAAiB,IAAI;AAAA,MACxB,MAAM,KAAK;AAAA,IACb,EAAE;AAAA,EACJ;AAEA,iBAAe,OAA+B;AAC5C,UAAM,OAAO,MAAM,WAAW,IAAI;AAClC,UAAM,cAAc,SAAS,kBAAkB,CAAC,GAAG,IAAI,gBAAgB;AACvE,UAAM,gBAAgB,SAAS,oBAAoB,CAAC,GAAG;AAAA,MAAI,CAAC,UAC1D,MAAM,IAAI,gBAAgB;AAAA,IAC5B;AAIA,UAAM,SAAS,MAAM,WAAW,OAAO;AACvC,UAAM,OAAO,MAAM,qBAAqB,gBAAgB,QAAQ,MAAM;AACtE,UAAM,SAAS,MAAM,qBAAqB,kBAAkB,UAAU,MAAM;AAE5E,UAAM,SAAS,MAAM,cAAc,MAAM;AAGzC,UAAM,cAAc,OAAO,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS;AACzE,UAAM,eAAe,SAAS,cAAc,CAAC,GAAG,IAAI,gBAAgB;AAEpE,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB,SAAS,KAAK,QAAQ;AAAA,MACtC,kBAAkB,SAAS,iBAAiB,WAAW,IAAI;AAAA,MAC3D,YAAY,SAAS,OAAO,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,SAAS,IAAI;AAAA,MAC9E,aAAa,UAAU,gBAAgB,MAAM,MAAM;AAAA,MACnD,SAAS,SACL,aAAa,KAAK,SAAS,OAAO,IAClC,SAAS,WAAW,iBAAiB,SAAS,OAAO;AAAA,IAC3D;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AACJ,kBAAY,KAAK;AACjB,aAAO;AAAA,IACT;AAAA,IACA,QAAQ;AACN,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;;;AChPO,SAAS,YAAY,MAAwB;AAClD,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,MAAI,OAAQ,KAA6B,UAAU,WAAY,QAAO;AACtE,QAAM,MAAM;AACZ,MAAI,gBAAgB,OAAO,mBAAmB,IAAK,QAAO;AAC1D,SAAO,MAAM,QAAQ,IAAI,MAAM,KAAK,MAAM,QAAQ,IAAI,SAAS;AACjE;AAGO,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAElC,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAGA,eAAe,mBACb,OACA,OACA,KACc;AACd,QAAM,UAAU,IAAI,MAAS,MAAM,MAAM;AACzC,MAAI,OAAO;AAEX,iBAAe,SAAwB;AACrC,WAAO,OAAO,MAAM,QAAQ;AAC1B,YAAM,QAAQ;AACd,cAAQ,KAAK,IAAI,MAAM,IAAI,MAAM,KAAK,CAAC;AAAA,IACzC;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,MAAM;AAC5E,QAAM,QAAQ,IAAI,OAAO;AACzB,SAAO;AACT;AAEO,SAAS,cAAc,SAAwB;AACpD,iBAAe,QAAW,OAAe,OAAU,KAAmC;AACpF,QAAI,CAAC,QAAQ,WAAW,EAAG,QAAO;AAClC,QAAI;AACF,aAAO,MAAM,IAAI;AAAA,IACnB,SAAS,OAAO;AACd,cAAQ,SAAS,KAAK;AACtB,cAAQ,KAAK,GAAG,KAAK,iBAAiB,aAAa,KAAK,CAAC,EAAE;AAC3D,aAAO;AAAA,IACT;AAAA,EACF;AAEA,iBAAe,MAAM,MAAqC;AACxD,WAAO,QAAQ,UAAU,IAAI,KAAK,MAAM,MAAM,QAAQ,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,EAC5E;AAUA,iBAAe,kBAAkB,MAAqC;AACpE,QAAI,CAAC,QAAQ,WAAW,EAAG,QAAO;AAElC,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,kBAAkB,WAAW;AAC5D,UAAI,UAAU,GAAG;AACf,cAAM,MAAM,qBAAqB,MAAM,UAAU,EAAE;AACnD,YAAI,QAAQ,UAAU,EAAG,SAAQ,WAAW;AAAA,MAC9C;AACA,UAAI;AACF,eAAO,MAAM,QAAQ,OAAO,EAAE,MAAM,IAAI;AAAA,MAC1C,SAAS,OAAO;AACd,kBAAU;AAAA,MACZ;AAAA,IACF;AAEA,YAAQ,SAAS,OAAO;AACxB,YAAQ;AAAA,MACN,UAAU,IAAI,mBAAmB,gBAAgB,cAAc,aAAa,OAAO,CAAC;AAAA,IACtF;AACA,WAAO;AAAA,EACT;AAqBA,WAAS,0BAA0B,MAA6C;AAC9E,WAAO,QAAQ,uBAAuB,IAAI,KAAK,CAAC,GAAG,MAAM;AACvD,YAAM,cAAc,QAAQ,OAAO,EAAE;AACrC,YAAM,UAAU,YAAY;AAC5B,aAAO,QAAQ,KAAK,aAAa,MAAM,EAAE,QAAQ,OAAO,CAAC;AAAA,IAC3D,CAAC;AAAA,EACH;AAEA,iBAAe,QAAQ,SAAyD;AAC9E,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA,QAAQ,OAAO;AAAA,MACf,OAAO,SAAS;AACd,YAAI,YAAY,IAAI,EAAG,QAAO;AAC9B,eAAO,kBAAkB,KAAK,QAAQ,EAAE;AAAA,MAC1C;AAAA,IACF;AAEA,WAAO,SAAS,OAAO,CAAC,SAAgC,QAAQ,IAAI,CAAC;AAAA,EACvE;AAEA,SAAO;AAAA,IACL;AAAA,IAEA,cAAqD;AACnD,aAAO,QAAQ,eAAe,CAAC,GAAG,MAAM,QAAQ,OAAO,EAAE,YAAY,KAAK,CAAC;AAAA,IAC7E;AAAA,IAEA,MAAM,WAAW,MAA0C;AACzD,aAAO,QAAQ,eAAe,IAAI,KAAK,MAAM,YAAY;AACvD,cAAM,QAAQ,MAAM,QAAQ,OAAO,EAAE,YAAY,IAAI,IAAI;AACzD,eAAO,MAAM,QAAQ,KAAK,IAAI,OAAO;AAAA,MACvC,CAAC;AAAA,IACH;AAAA,IAEA,kBAAkB,MAA0C;AAC1D,aAAO;AAAA,QAAQ,uBAAuB,IAAI;AAAA,QAAK,CAAC;AAAA,QAAG,MACjD,QAAQ,OAAO,EAAE,YAAY,QAAQ,IAAI;AAAA,MAC3C;AAAA,IACF;AAAA;AAAA,IAGA,MAAM,0BAA0B,OAA6C;AAC3E,iBAAW,QAAQ,OAAO;AACxB,cAAM,UAAU,MAAM,KAAK,kBAAkB,IAAI;AACjD,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AACA,aAAO,CAAC;AAAA,IACV;AAAA,IAEA;AAAA;AAAA,IAGA,MAAM,0BAA0B,MAAwC;AACtE,aAAO,QAAQ,MAAM,0BAA0B,IAAI,CAAC;AAAA,IACtD;AAAA,IAEA,MAAM,kCAAkC,OAA2C;AACjF,iBAAW,QAAQ,OAAO;AACxB,cAAM,UAAU,MAAM,KAAK,0BAA0B,IAAI;AACzD,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AACA,aAAO,CAAC;AAAA,IACV;AAAA,IAEA;AAAA,IACA;AAAA,IAEA,OAAmC;AACjC,aAAO,QAAQ,QAAQ,MAAM,MAAM,QAAQ,OAAO,EAAE,QAAQ,CAAC;AAAA,IAC/D;AAAA,IAEA,YAAuC;AACrC,aAAO,QAAQ,aAAa,MAAM,MAAM,QAAQ,OAAO,EAAE,aAAa,CAAC;AAAA,IACzE;AAAA,EACF;AACF;;;AC7NA,SAAS,cAA+C;;;AC6ExD,IAAM,cAAc,oBAAI,IAAY,CAAC,UAAU,eAAe,CAAC;AAE/D,SAAS,gBAAgB,OAAyB;AAChD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,SAAU,MAA+B;AAC/C,MAAI,WAAW,OAAW,QAAO;AAEjC,QAAM,SAAU,MAA+B;AAC/C,MAAI,OAAO,WAAW,YAAY;AAChC,WAAO,SAAS,OAAO,KAAK,KAAK,CAAC,EAAE;AAAA,EACtC;AACA,SAAO;AACT;AAMO,SAAS,YAAY,OAA0C;AACpE,QAAM,MAAM,gBAAgB,KAAK;AACjC,MAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,EAAG,QAAO;AAElE,QAAM,SAAuB,CAAC;AAC9B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAA8B,GAAG;AACzE,QAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC/D,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AACnD;AAGO,SAAS,UACd,QACA,KACyB;AACzB,MAAI,CAAC,UAAU,CAAC,IAAK,QAAO;AAC5B,QAAM,MAAM,IAAI,KAAK;AACrB,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,OAAO,UAAU,eAAe,KAAK,QAAQ,GAAG,IAAI,OAAO,GAAG,IAAI;AAC3E;AAGO,SAAS,WAAW,KAA4C;AACrE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,QAAQ,SAAS,GAAG;AAC1B,QAAM,OAAO,SAAS,MAAM,IAAI;AAChC,QAAM,eAAe,MAAM,QAAQ,MAAM,YAAY,IACjD,MAAM,aAAa;AAAA,IACjB,CAAC,WAA6B,OAAO,WAAW,YAAY,OAAO,KAAK,EAAE,SAAS;AAAA,EACrF,IACA,CAAC;AAEL,SAAO;AAAA;AAAA,IAEL,MAAM,QAAQ,YAAY,IAAI,IAAI,IAAK,OAA2B;AAAA,IAClE,aAAa,SAAS,MAAM,WAAW,KAAK;AAAA,IAC5C;AAAA,EACF;AACF;;;ADlHA,IAAM,SAAS,uBAAO,eAAe;AAKrC,IAAM,WAAW;AAMV,SAAS,mBACd,OACA,QACoB;AACpB,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,MAAI,CAAC,SAAS,KAAK,IAAI,EAAG,QAAO;AACjC,QAAM,OAAO,UAAU,QAAQ,IAAI,GAAG;AACtC,SAAO,OAAO,SAAS,YAAY,KAAK,KAAK,IAAI,OAAO;AAC1D;AAgBO,SAAS,uBAAuB,UAA2B,CAAC,GAAqB;AACtF,QAAM,SAAS,IAAI,OAAO;AAC1B,SAAO,WAAW,EAAE,KAAK,MAAM,QAAQ,OAAO,GAAG,QAAQ,CAAC;AAC1D,SAAO,IAAI;AAAA,IACT,UAAU;AAAA,MACR,UAAU,OAAO;AACf,cAAM,SAAU,KAAK,QAAmC,MAAM;AAC9D,cAAM,OAAO,mBAAmB,OAAO,MAAM;AAE7C,eAAO,SAAS,SAAY,QAAQ,GAAG,IAAI;AAAA;AAAA,MAC7C;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,OAAO,UAAU,gBAAgB,CAAC,MAAM;AAC7C,UAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK,MAAM,IAAK,YAAY;AAC9E,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,eAAuC,EAAE,CAAC,MAAM,GAAG,cAAc,UAAU,KAAK;AACtF,WAAO,MAAM,OAAO,MAAM,QAAQ,YAAY;AAAA,EAChD;AACF;AAGO,SAAS,mBAAmB,QAAwB;AACzD,SAAO,OAAO,QAAQ,oBAAoB,MAAM;AAClD;;;AEnFA,IAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,kBAAkB,OAAsD;AACtF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,MAAM,2BAA2B,KAAK,OAAO;AACnD,MAAI,KAAK;AACP,UAAM,CAAC,EAAE,MAAM,OAAO,GAAG,IAAI;AAC7B,UAAM,OAAO,OAAO,OAAO,KAAK,IAAI,CAAC;AACrC,QAAI,KAAM,QAAO,GAAG,IAAI,IAAI,GAAG,KAAK,IAAI;AAAA,EAC1C;AAEA,SAAO;AACT;AASO,SAAS,oBAAuB,OAAY,UAAkB,QAAQ,GAAQ;AACnF,QAAM,SAAS,CAAC,GAAG,KAAK;AACxB,MAAI,OAAO;AAEX,aAAW,aAAa,UAAU;AAChC,WAAQ,OAAO,KAAK,UAAU,WAAW,CAAC,MAAO;AAAA,EACnD;AAEA,WAAS,QAAQ,OAAO,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG;AACzD,WAAQ,OAAO,UAAU,eAAgB;AAEzC,UAAM,SAAS,QAAQ,QAAQ;AAE/B,KAAC,OAAO,KAAK,GAAG,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,MAAM,GAAG,OAAO,KAAK,CAAC;AAAA,EAClE;AAEA,SAAO,OAAO,MAAM,GAAG,KAAK;AAC9B;;;ACiBA,IAAM,sBAAsB,CAAC,QAAQ,YAAY,MAAM;AAYhD,SAAS,oBACd,OACA,SACA;AACA,QAAM,QAAQ,SAAU,MAA8B,KAAK;AAC3D,QAAM,SAAS,YAAY,KAAK;AAEhC,QAAM,WAAW;AAAA;AAAA,IAEf;AAAA;AAAA,IAEA,MAAM,QAAQ;AAAA,IACd,YAAY,QAAQ;AAAA;AAAA,IAGpB,IAAI,KAAsB;AACxB,aAAO,MAAM,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,OAAO,KAAiC;AACtC,aAAO,SAAS,MAAM,OAAO,GAAG,CAAC;AAAA,IACnC;AAAA,IAEA,OAAO,KAAiC;AACtC,aAAO,SAAS,MAAM,OAAO,GAAG,CAAC;AAAA,IACnC;AAAA;AAAA,IAGA,KAAK,KAAsB;AACzB,YAAM,QAAQ,MAAM,OAAO,GAAG;AAC9B,UAAI,OAAO,UAAU,UAAW,QAAO;AACvC,UAAI,OAAO,UAAU,UAAU;AAC7B,eAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,EAAE,SAAS,MAAM,KAAK,EAAE,YAAY,CAAC;AAAA,MACvE;AACA,UAAI,OAAO,UAAU,SAAU,QAAO,UAAU;AAChD,aAAO,QAAQ,KAAK;AAAA,IACtB;AAAA;AAAA,IAGA,KAAK,KAAmC;AACtC,YAAM,QAAQ,MAAM,OAAO,GAAG;AAC9B,YAAM,QAAQ,cAAc,KAAK;AACjC,UAAI,MAAO,QAAO;AAClB,YAAM,SAAS,SAAS,KAAK;AAC7B,aAAO,SAAS,CAAC,MAAM,IAAI;AAAA,IAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAwB,KAAa,SAAgC,UAAgB;AACnF,YAAM,QAAQ,SAAS,MAAM,OAAO,GAAG,CAAC;AACxC,YAAM,MAAM,mBAAmB,MAAM,UAAU,IAAI,IAAY,OAAO;AACtE,aAAO,SAAS,IAAI,IAAI,KAAU,IAAK,QAAc;AAAA,IACvD;AAAA;AAAA,IAGA,KAAK,KAAiC;AACpC,aAAO,kBAAkB,SAAS,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IACtD;AAAA;AAAA,IAGA,cAAkC;AAChC,YAAM,QAAS,MAAoC;AACnD,aAAO,SAAS,OAAO,OAAO,KAAK,IAAI;AAAA,IACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,SACJ,MAAM,QACN,UAAgC,CAAC,GACJ;AAC7B,YAAM,MACJ,MAAyB,OAAO,GAAG,KAClC,MAAkD;AACrD,YAAM,SAAS,MAAM,QAAQ,GAAG,IAAI,IAAI,KAAK,MAAM,IAAI,SAAS,GAAG;AACnE,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,QAAQ,eAAe,QAAQ,aAAa,mBAAmB,MAAM,IAAI,QAAQ;AAAA,QACtF,QAAQ,QAAQ,WAAW,QAAQ,SAAY;AAAA,MACjD,CAAC;AAAA,IACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,KAAsC;AAC1C,aAAO,UAAU,QAAQ,SAAS,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,UAA6B,CAAC,GAAyB;AAC3D,YAAM;AAAA,QACJ;AAAA,QACA,cAAc;AAAA,QACd,KAAK;AAAA,QACL,qBAAqB;AAAA,QACrB,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,eAAe;AAAA,QACf,GAAG;AAAA,MACL,IAAI;AACJ,YAAM,cAAc,eAAe,SAAS;AAE5C,UAAI,aAAa;AACf,cAAM,WAAW,SAAS,cAAc,KAAK,EAAE,WAAW,CAAC;AAC3D,cAAMA,OAAM,SAAS,SAAS,GAAG;AACjC,YAAIA,MAAK;AACP,iBAAO;AAAA,YACL,KAAAA;AAAA,YACA,KAAK,SAAS,SAAS,GAAG,KAAK;AAAA,YAC/B,YAAY,WAAW,SAAS,UAAU;AAAA,UAC5C;AAAA,QACF;AAAA,MACF;AAEA,YAAM,SACH,aAAa,SAAS,kBAAkB,OAAO,GAAG,WAAW,IAAI,WAClE,iBAAiB,OAAO,EAAE,GAAG,MAAM,OAAO,QAAQ,aAAa,CAAC,MAC/D,qBAAqB,oBAAoB,KAAK,IAAI;AAErD,YAAM,MAAM,SAAS,KAAK;AAC1B,UAAI,CAAC,IAAK,QAAO;AAEjB,aAAO;AAAA,QACL;AAAA,QACA,KAAK,SAAS,OAAO,WAAW,KAAK;AAAA,QACrC,YAAY,WAAW,WAAW,OAAO,YAAY,CAAC;AAAA,MACxD;AAAA,IACF;AAAA;AAAA,IAGA,KAAK,UAA4B,CAAC,GAAuB;AACvD,YAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,qBAAqB,MAAM,IAAI;AACzD,YAAM,QAAQ,kBAAkB,OAAO,GAAG,KAAK,KAAK,gBAAgB,KAAK;AACzE,aACE,SAAS,KAAK,MAAM,qBAAqB,SAAS,oBAAoB,KAAK,CAAC,IAAI;AAAA,IAEpF;AAAA;AAAA,IAGA,MAAM,UAA8B,CAAC,GAAwC;AAC3E,aAAO,iBAAiB,OAAO,OAAO,KAAK,oBAAoB,KAAK;AAAA,IACtE;AAAA;AAAA,IAGA,eAAe,OAAsD;AACnE,aAAO,kBAAkB,OAAO,GAAG,KAAK;AAAA,IAC1C;AAAA;AAAA,IAGA,SAAoC;AAClC,aAAO,YAAY,KAAK;AAAA,IAC1B;AAAA;AAAA,IAGA,OAAO,UAA6B,CAAC,GAAsC;AACzE,YAAM,SAAS,oBAAI,IAAI,CAAC,GAAI,QAAQ,SAAS,CAAC,GAAI,GAAI,QAAQ,UAAU,CAAC,CAAE,CAAC;AAC5E,YAAM,cAAc,QAAQ,OAAO,SAAS;AAC5C,YAAM,SAA4C,CAAC;AAEnD,iBAAW,SAAS,YAAY,KAAK,GAAG;AACtC,cAAM,QAAQ,SAAS,WAAW,OAAO,OAAO,CAAC,KAAK,SAAS,WAAW,OAAO,MAAM,CAAC;AACxF,cAAM,MAAM,SAAS,KAAK;AAC1B,YAAI,CAAC,IAAK;AACV,YAAI,OAAO,OAAO,MAAM,CAAC,SAAS,CAAC,OAAO,IAAI,KAAK,GAAI;AAEvD,eAAO,KAAK;AAAA,UACV;AAAA,UACA,KAAK,SAAS,OAAO,WAAW,KAAK;AAAA,UACrC,YAAY,WAAW,WAAW,OAAO,YAAY,CAAC;AAAA,UACtD;AAAA,QACF,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,UAAU,UAAgC,CAAC,GAAsB;AAC/D,YAAM,QAAQ,QAAQ,OAAO,SAAS,IAAI,IAAI,QAAQ,KAAK,IAAI;AAE/D,aAAO,eAAe,KAAK,EACxB,IAAI,CAAC,iBAAiB;AACrB,cAAM,SAAS,SAAS,aAAa,QAAQ;AAC7C,cAAM,SAAS,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AACzD,cAAM,OAAO,SAAS,cAAc,QAAQ,MAAM,CAAC;AACnD,eAAO;AAAA,UACL,MAAM,SAAS,cAAc,QAAQ,MAAM,CAAC,KAAK;AAAA,UACjD,MAAM,SAAS,cAAc,QAAQ,cAAc,CAAC,KAAK;AAAA,UACzD,MAAM,SAAS,WAAW,cAAc,MAAM,CAAC;AAAA,UAC/C;AAAA,UACA,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,IAAI,IAAI;AAAA,QACpD;AAAA,MACF,CAAC,EACA,OAAO,CAAC,aAAa,QAAQ,SAAS,IAAI,CAAC,EAC3C,OAAO,CAAC,aAAa,CAAC,SAAS,MAAM,IAAI,SAAS,IAAI,CAAC,EACvD,OAAO,CAAC,aAAa,CAAC,QAAQ,QAAQ,SAAS,SAAS,QAAQ,IAAI;AAAA,IACzE;AAAA;AAAA,IAGA,SAAS,UAAgC,CAAC,GAAgC;AACxE,aAAO,SAAS,UAAU,OAAO,EAAE,CAAC;AAAA,IACtC;AAAA;AAAA,IAGA,WAAoC;AAClC,aAAO,cAAc,KAAK;AAAA,IAC5B;AAAA;AAAA,IAGA,OAAgC;AAC9B,aAAO,UAAU,KAAK;AAAA,IACxB;AAAA;AAAA,IAGA,cAAwB;AACtB,aAAO,gBAAgB,KAAK;AAAA,IAC9B;AAAA;AAAA,IAGA,KAAK,gBAA4C;AAC/C,aAAO,eAAe,OAAO,cAAc;AAAA,IAC7C;AAAA,EACF;AAEA,SAAO;AACT;;;AClQA,SAAS,cAAiB,MAA6B;AACrD,QAAM,OAAQ,KAA4B;AAC1C,SAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAEA,SAAS,kBAAqB,MAAkB;AAC9C,SAAO,QAAS,KAAgC,QAAQ;AAC1D;AAEO,SAAS,iBACd,SACA,SACe;AACf,QAAM,EAAE,SAAS,eAAe,IAAI;AACpC,QAAM,cAAc,QAAQ,gBAAgB,QAAQ,aAAa,CAAC,QAAQ,UAAU,IAAI,CAAC;AACzF,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,aAAa,QAAQ,cAAc;AAIzC,MAAI,gBAAwC;AAC5C,MAAI,aAAkC;AACtC,QAAM,cAAc,oBAAI,IAAoC;AAE5D,WAAS,QAAQ,OAAiB;AAChC,UAAM,WAAW,QAAQ,SAAS,MAAM,OAAO,QAAQ,MAAM,IAAI;AACjE,WAAO,QAAQ,OAAO,CAAC,GAAG,QAAQ,EAAE,KAAK,QAAQ,IAAI,IAAI;AAAA,EAC3D;AAEA,WAAS,UAAU,OAA2B,YAAiC;AAC7E,UAAM,OAAQ,MAA4B,QAAQ;AAClD,UAAM,SAAS,oBAAoB,OAAO;AAAA,MACxC;AAAA,MACA,MAAM,QAAQ,OAAO,MAAM,KAAK;AAAA,MAChC;AAAA,IACF,CAAC;AACD,WAAO,QAAQ,QAAQ,QAAQ,UAAU,OAAO,MAAM,CAAC;AAAA,EACzD;AAEA,iBAAe,cAA+B;AAC5C,WAAO,EAAE,OAAO,QAAS,MAAM,QAAQ,WAAW,KAAM,CAAC,CAAC,GAAG,eAAe,oBAAI,IAAI,EAAE;AAAA,EACxF;AAEA,iBAAe,OAAwB;AACrC,QAAI,YAAY,SAAS,KAAK,QAAQ,QAAQ,WAAW,GAAG;AAC1D,UAAI;AACF,cAAM,UAAU,QAAQ,UACpB,MAAM,QAAQ,kCAAkC,WAAW,IAC3D,MAAM,QAAQ,0BAA0B,WAAW;AAEvD,YAAI,QAAQ,SAAS,GAAG;AACtB,gBAAM,QAAQ,MAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,UAAU,UAAU,KAAK,CAAC,CAAC;AACxE,gBAAM,gBAAgB,oBAAI,IAAY;AACtC,gBAAM,QAAQ,CAAC,MAAM,UAAU;AAC7B,kBAAM,OAAO,OAAO,IAAI;AACxB,gBAAI,SAAS,UAAa,YAAY,QAAQ,KAAK,CAAC,EAAG,eAAc,IAAI,IAAI;AAAA,UAC/E,CAAC;AACD,iBAAO,EAAE,OAAO,QAAQ,KAAK,GAAG,cAAc;AAAA,QAChD;AAAA,MACF,SAAS,OAAO;AACd,gBAAQ,QAAQ,SAAS,KAAK;AAC9B,gBAAQ,QAAQ;AAAA,UACd,eAAe,YAAY,CAAC,CAAC,6BAA6B,aAAa,KAAK,CAAC;AAAA,QAC/E;AAAA,MACF;AAAA,IACF;AAEA,WAAO,YAAY;AAAA,EACrB;AAEA,WAAS,WAAW,OAAY,MAA6B;AAC3D,WAAO,MAAM,KAAK,CAAC,SAAS,OAAO,IAAI,MAAM,IAAI;AAAA,EACnD;AAEA,iBAAe,WAAW,MAAsC;AAK9D,QAAI;AACJ,QAAI,eAAe;AACjB,YAAM,SAAS,MAAM;AACrB,eAAS,WAAW,OAAO,OAAO,IAAI;AACtC,UAAI,UAAU,OAAO,cAAc,IAAI,IAAI,EAAG,QAAO;AAAA,IACvD;AAEA,QAAI,QAAQ,QAAQ,WAAW,GAAG;AAChC,UAAI;AACF,cAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI;AACtC,YAAI,MAAO,QAAO,MAAM,UAAU,KAAK;AAAA,MACzC,SAAS,OAAO;AACd,gBAAQ,QAAQ,SAAS,KAAK;AAC9B,gBAAQ,QAAQ,KAAK,UAAU,IAAI,6BAA6B,aAAa,KAAK,CAAC,EAAE;AAAA,MACvF;AAAA,IACF;AAIA,WAAO,UAAU,WAAW,MAAM,IAAI,GAAG,IAAI;AAAA,EAC/C;AAEA,WAAS,MAAoB;AAC3B,QAAI,CAAC,YAAY;AACf,sBAAgB,KAAK;AACrB,mBAAa,cAAc,KAAK,CAAC,WAAW,OAAO,KAAK;AAAA,IAC1D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IAEA,OAAO,MAAsC;AAC3C,UAAI,SAAS,YAAY,IAAI,IAAI;AACjC,UAAI,CAAC,QAAQ;AACX,iBAAS,WAAW,IAAI;AACxB,oBAAY,IAAI,MAAM,MAAM;AAAA,MAC9B;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,cAA4B;AAChC,cAAQ,MAAM,IAAI,GAAG,OAAO,UAAU;AAAA,IACxC;AAAA,IAEA,MAAM,WAAmC;AACvC,YAAM,QAAQ,MAAM,IAAI;AACxB,aAAO,MAAM,KAAK,UAAU,KAAK,MAAM,CAAC;AAAA,IAC1C;AAAA,IAEA,QAAQ;AACN,mBAAa;AACb,sBAAgB;AAChB,kBAAY,MAAM;AAAA,IACpB;AAAA,EACF;AACF;;;AC5LA,eAAsB,mBACpB,SACA,MACA,iBAAmC,uBAAuB,GAC1D,UAAiC,CAAC,GACT;AACzB,QAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI;AACtC,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,QAAM,SACJ,SAAS,WAAW,OAAO,QAAQ,aAAa,MAAM,CAAC,KAAK,SAAS,MAAM,OAAO;AAEpF,SAAO;AAAA,IACL,OAAO,SAAS,MAAM,KAAK;AAAA,IAC3B,WAAW,SACP,MAAM,eAAe,mBAAmB,MAAM,GAAG,EAAE,QAAQ,YAAY,KAAK,EAAE,CAAC,IAC/E;AAAA,IACJ,MAAM,SAAS,kBAAkB,OAAO,QAAQ,YAAY,MAAM,CAAC;AAAA,EACrE;AACF;;;ACNO,SAAS,SACd,KACA,UACQ;AACR,QAAM,MAAM,OAAO,CAAC;AACpB,QAAM,eAAe,SAAS,IAAI,YAAY;AAE9C,SAAO;AAAA,IACL,OAAO,SAAS,IAAI,KAAK,KAAK,SAAS;AAAA,IACvC,eAAe,SAAS,IAAI,aAAa;AAAA,IACzC,aAAa,SAAS,IAAI,WAAW,KAAK,SAAS;AAAA,IACnD,UAAU,MAAM,QAAQ,IAAI,QAAQ,IAChC,IAAI,SAAS,OAAO,CAAC,YAA+B,OAAO,YAAY,QAAQ,IAC/E,CAAC;AAAA,IACL,QAAQ,SAAS,IAAI,MAAM,KAAK;AAAA,IAChC,OAAO,SAAS,IAAI,KAAK;AAAA,IACzB,UAAU,SAAS,IAAI,QAAQ;AAAA,IAC/B,QAAQ,SAAS,IAAI,MAAM,KAAK;AAAA,IAChC,UAAU,SAAS,IAAI,QAAQ,KAAK,SAAS;AAAA,IAC7C,aAAa,SAAS,IAAI,WAAW,KAAK;AAAA,IAC1C,eAAe,SAAS,IAAI,aAAa;AAAA,IACzC,gBAAgB,SAAS,IAAI,cAAc;AAAA,IAC3C,SAAS,SAAS,IAAI,OAAO;AAAA,IAC7B,YAAY,SAAS,IAAI,UAAU;AAAA,IACnC,WAAW,SAAS,IAAI,SAAS;AAAA,IACjC,cAAc,SAAS;AAAA,IACvB,cAAc;AAAA,MACZ,QAAQ,SAAS,aAAa,MAAM;AAAA,MACpC,MAAM,SAAS,aAAa,IAAI;AAAA,MAChC,WAAW,SAAS,aAAa,SAAS;AAAA,MAC1C,QAAQ,SAAS,aAAa,MAAM;AAAA,IACtC;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,QAAyC;AAChE,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,SAAS,MAAM,CAAC,EAAE;AAAA,MAC/B,CAAC,UAAqC,OAAO,MAAM,CAAC,MAAM,YAAY,MAAM,CAAC,EAAE,SAAS;AAAA,IAC1F;AAAA,EACF;AACF;AAGO,SAAS,kBAAkB,UAAuB,CAAC,GAAY;AACpE,QAAM,WAAW,OAAO,QAAQ,aAAa,aAAa,QAAQ,SAAS,IAAI,QAAQ;AACvF,QAAM,WAAW,YAAY,CAAC;AAC9B,QAAM,QAAQ,SAAS,SAAS,SAAS,QAAQ;AACjD,QAAM,cAAc,SAAS,eAAe;AAE5C,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,SAAS,QAAQ;AAAA,IACvB;AAAA,IACA;AAAA,IACA,QAAQ,SAAS,UAAU,QAAQ,iBAAiB;AAAA,IACpD,UAAU,SAAS,YAAY,QAAQ,mBAAmB;AAAA,IAC1D,QAAQ,SAAS,UAAU,CAAC;AAAA,IAC5B,UAAU,SAAS,YAAY,CAAC;AAAA,IAChC,KACE,SAAS,OACT,SAAS,QAAW,EAAE,OAAO,aAAa,UAAU,SAAS,QAAQ,MAAM,CAAC;AAAA,EAChF;AACF;AAEA,SAAS,oBAAoB,YAAwB,UAA4B;AAC/E,QAAM,SAA4B,WAAW;AAC7C,QAAM,WAAW,SAAS,OAAO,YAAY;AAC7C,QAAM,QACJ,SAAS,OAAO,KAAK,KAAK,SAAS,WAAW,WAAW,IAAI,KAAK,SAAS;AAC7E,QAAM,cAAc,SAAS,OAAO,WAAW,KAAK,SAAS;AAE7D,SAAO;AAAA,IACL,eAAe,SAAS,WAAW,WAAW,IAAI;AAAA,IAClD,eAAe,SAAS,WAAW,WAAW,IAAI;AAAA,IAClD,MAAM;AAAA,IACN;AAAA,IACA,SAAS,SAAS,OAAO,OAAO,KAAK,SAAS;AAAA,IAC9C;AAAA,IACA,cAAc,SAAS,OAAO,YAAY,KAAK,SAAS;AAAA,IACxD,MAAM,SAAS,OAAO,IAAI,KAAK,SAAS;AAAA,IACxC,SAAS,SAAS,OAAO,OAAO,KAAK,SAAS;AAAA,IAC9C,QAAQ,SAAS,OAAO,MAAM,KAAK,SAAS;AAAA,IAC5C,UAAU,SAAS,OAAO,QAAQ,KAAK,SAAS;AAAA,IAChD,OAAO,SAAS,OAAO,YAAY,KAAK,SAAS;AAAA,IACjD,SAAS,SAAS,SAAS,OAAO,KAAK,SAAS;AAAA;AAAA,IAEhD,QAAQ,gBAAgB,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA,IAIrC,KAAK,SAAS,SAAU,OAA6B,OAAO,SAAS,GAAG,GAAG;AAAA,MACzE;AAAA,MACA;AAAA,MACA,cAAc,SAAS,OAAO,YAAY;AAAA,MAC1C,UAAU;AAAA,IACZ,CAAC;AAAA;AAAA,IAED,QAAQ;AAAA,MACL,OAAgC,UAAW,WAAoC;AAAA,IAClF;AAAA,IACA;AAAA,EACF;AACF;AAGA,SAAS,UAAU,OAAwB;AACzC,SAAO,eAAe,KAAK,KAAK,KAAK,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,OAAO;AACxF;AAaO,SAAS,iBAAiB,SAAwB,UAAuB,CAAC,GAAe;AAC9F,QAAM,WAAW,kBAAkB,OAAO;AAC1C,MAAI,UAAmC;AACvC,MAAI,aAAa;AAEjB,iBAAe,OAAyB;AACtC,UAAM,aAAa,MAAM,QAAQ,KAAK;AACtC,iBAAa,eAAe;AAC5B,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,OAAO,oBAAoB,YAAY,QAAQ;AAKrD,UAAM,QAAgC,CAAC;AACvC,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,SAAS,KAAK,SAAS,KAAK,CAAC,GAAG;AACzE,UAAI,OAAO,UAAU,YAAY,UAAU,MAAM,KAAK,CAAC,EAAG,OAAM,IAAI,IAAI,MAAM,KAAK;AAAA,IACrF;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA;AAAA,MAEA,MAAM,MAAM,QAAQ,KAAK;AAAA,MACzB,SAAS,MAAM,WAAW,KAAK;AAAA,MAC/B,MAAM,MAAM,QAAQ,KAAK;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAGJ,kBAAY,KAAK;AACjB,aAAO;AAAA,IACT;AAAA,IACA,MAAM,SAAS;AACb,YAAM,KAAK,IAAI;AACf,aAAO;AAAA,IACT;AAAA,IACA,QAAQ;AACN,gBAAU;AACV,mBAAa;AAAA,IACf;AAAA,EACF;AACF;;;ACnJO,SAAS,oBAAoB,UAAgC,CAAC,GAAG;AACtE,QAAM,UAAU,cAAc,OAAO;AACrC,QAAM,UAAU,cAAc,OAAO;AACrC,QAAM,iBAAiB,uBAAuB,QAAQ,OAAO,QAAQ;AACrE,QAAM,aAAa,iBAAiB,SAAS,QAAQ,IAAI;AACzD,QAAM,eAAe,mBAAmB,SAAS,YAAY,QAAQ,MAAM;AAC3E,QAAM,eAAsC,CAAC;AAE7C,SAAO;AAAA;AAAA,IAEL,QAAQ,QAAQ;AAAA;AAAA,IAEhB;AAAA;AAAA,IAEA,OAAO;AAAA,IACP;AAAA;AAAA,IAGA,YAAY,MAAM,QAAQ,WAAW;AAAA;AAAA,IAGrC,UAA4B;AAC1B,aAAO,WAAW,IAAI;AAAA,IACxB;AAAA;AAAA,IAGA,gBAAwC;AACtC,aAAO,aAAa,IAAI;AAAA,IAC1B;AAAA;AAAA,IAGA,kBAAkB,MAAc,gBAAiE;AAC/F,aAAO,mBAAmB,SAAS,MAAM,gBAAgB,cAAc;AAAA,IACzE;AAAA;AAAA,IAGA,WAAc,mBAAwD;AACpE,YAAM,aAAa,iBAAoB,EAAE,SAAS,eAAe,GAAG,iBAAiB;AACrF,mBAAa,KAAK,UAAiC;AACnD,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,QAAQ;AACN,cAAQ,WAAW;AACnB,iBAAW,MAAM;AACjB,mBAAa,MAAM;AACnB,iBAAW,cAAc,aAAc,YAAW,MAAM;AAAA,IAC1D;AAAA,EACF;AACF;","names":["src"]}
package/dist/types.d.ts CHANGED
@@ -84,11 +84,11 @@ type ApiSite = {
84
84
  canonicalUrl?: string;
85
85
  logo?: string;
86
86
  favicon?: string;
87
- /** Round maker's stamp / seal — resolved from `site_metadata_json.brand.seal` (asset slug). */
87
+ /** Round maker's stamp / seal — `site_metadata_json.brand.seal`, when it is a URL. */
88
88
  seal?: string;
89
89
  /**
90
- * Brand-asset registry: every `site_metadata_json.brand.<name>` slug resolved to its URL.
91
- * Add a shared asset with one config line; components read `site.brand.<name>`.
90
+ * Brand-asset registry: every `site_metadata_json.brand.<name>` that is a URL, path or data URI,
91
+ * used as-is. Bare asset slugs are left out (Marvin no longer serves assets by slug).
92
92
  */
93
93
  brand?: Record<string, string>;
94
94
  locale: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneropen/marvin-astro",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Site integration for Marvin CMS on Astro — repositories, site chrome, and payload normalization",
5
5
  "type": "module",
6
6
  "exports": {
package/src/chrome.ts CHANGED
@@ -55,9 +55,9 @@ export type ChromeOptions = {
55
55
  /** Collection holding the footer nav. Default `'footer-navigation'`. */
56
56
  footerCollection?: string;
57
57
  /**
58
- * Collection holding the social links. Default `'social-links'`. When it exists, its entries
59
- * (label, href, optional `icon` field) are exactly the social links, in order, and the site
60
- * `social` map is left out; when it doesn't, the social map is used.
58
+ * Collection holding the social links. Default `'social-links'`. When it has entries, they
59
+ * (label, href, optional `icon` field) are exactly the social links, in order; otherwise the
60
+ * site `social` map is used.
61
61
  */
62
62
  socialCollection?: string;
63
63
  /**
@@ -197,21 +197,14 @@ export function createChromeLoader(
197
197
  async function navigationCollection(
198
198
  slug: string,
199
199
  context: NavigationContext,
200
- staticLinks: ApiNavigationLink[]
201
- ): Promise<{ links: ApiNavigationLink[]; entries: HydratedEntry[]; fromBackend: boolean }> {
202
- if (!fetcher.backend.hasBackend()) return { links: staticLinks, entries: [], fromBackend: false };
200
+ online: boolean
201
+ ): Promise<{ links: ApiNavigationLink[]; entries: HydratedEntry[] }> {
202
+ if (!online) return { links: [], entries: [] };
203
203
 
204
204
  // Hydrated: a nav entry's `href`/`label` overrides are schema fields, absent from list items.
205
+ // Empty, hidden or missing all read as empty: Marvin is up, so its answer is final.
205
206
  const entries = await fetcher.hydratedCollectionEntries(slug);
206
- // An existing collection that is empty means "show nothing"; the static links stand in only
207
- // when Marvin has no such collection or can't be reached.
208
- if (entries.length === 0) {
209
- const exists = (await fetcher.collection(slug)) !== null;
210
- return { links: exists ? [] : staticLinks, entries, fromBackend: exists };
211
- }
212
-
213
- const links = entries.map((entry) => entryToLink(entry, slug, context));
214
- return { links, entries, fromBackend: true };
207
+ return { links: entries.map((entry) => entryToLink(entry, slug, context)), entries };
215
208
  }
216
209
 
217
210
  function groupFooterLinks(links: ApiNavigationLink[]): ApiNavigationLink[][] {
@@ -221,10 +214,10 @@ export function createChromeLoader(
221
214
  return [links.slice(0, midpoint), links.slice(midpoint)];
222
215
  }
223
216
 
224
- /** The social-links collection as links, or null when Marvin has no such collection. */
225
- async function socialEntries(): Promise<ApiSocialLink[] | null> {
226
- const social = await navigationCollection(socialCollection, 'footer', []);
227
- if (!social.fromBackend) return null;
217
+ /** The social-links collection as links, or null when it has no entries. */
218
+ async function socialEntries(online: boolean): Promise<ApiSocialLink[] | null> {
219
+ const social = await navigationCollection(socialCollection, 'footer', online);
220
+ if (social.links.length === 0) return null;
228
221
 
229
222
  return social.links.map((link, index) => ({
230
223
  ...link,
@@ -233,13 +226,13 @@ export function createChromeLoader(
233
226
  }));
234
227
  }
235
228
 
236
- /** The site `social` map plus the contact email; the static social links when both are empty. */
237
- function siteSocialLinks(site: ApiSite): ApiSocialLink[] {
229
+ /** The site `social` map plus the contact email; offline, the static social links if both are empty. */
230
+ function siteSocialLinks(site: ApiSite, online: boolean): ApiSocialLink[] {
238
231
  const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));
239
232
  if (site.email && !links.some((link) => link.icon === 'email')) {
240
233
  links.push(socialLinkFromKey('email', `mailto:${site.email}`));
241
234
  }
242
- if (links.length > 0) return links;
235
+ if (links.length > 0 || online) return links;
243
236
 
244
237
  return (fallback.socialLinks ?? []).map((link) => ({
245
238
  ...toNavigationLink(link),
@@ -254,10 +247,13 @@ export function createChromeLoader(
254
247
  group.map(toNavigationLink)
255
248
  );
256
249
 
257
- const main = await navigationCollection(mainCollection, 'main', staticMain);
258
- const footer = await navigationCollection(footerCollection, 'footer', staticFooter.flat());
250
+ // The build's one "is Marvin up?" check: online, Marvin's content is final everywhere below;
251
+ // offline, every part of the chrome is the static fallback.
252
+ const online = await siteLoader.online();
253
+ const main = await navigationCollection(mainCollection, 'main', online);
254
+ const footer = await navigationCollection(footerCollection, 'footer', online);
259
255
 
260
- const social = await socialEntries();
256
+ const social = await socialEntries(online);
261
257
 
262
258
  // Split footer links by membership role: legal → the legal strip; everything else → columns.
263
259
  const columnLinks = footer.links.filter((link) => link.role !== legalRole);
@@ -265,15 +261,13 @@ export function createChromeLoader(
265
261
 
266
262
  return {
267
263
  site,
268
- mainNavigation: main.links,
269
- footerNavigation: footer.fromBackend ? groupFooterLinks(columnLinks) : staticFooter,
270
- legalLinks: footer.fromBackend
271
- ? footer.links.filter((link) => link.role === legalRole)
272
- : staticLegal,
273
- socialLinks: social ?? siteSocialLinks(site),
274
- inquiry:
275
- metadataLink(site.metadata.inquiry) ??
276
- (fallback.inquiry ? toNavigationLink(fallback.inquiry) : undefined),
264
+ mainNavigation: online ? main.links : staticMain,
265
+ footerNavigation: online ? groupFooterLinks(columnLinks) : staticFooter,
266
+ legalLinks: online ? footer.links.filter((link) => link.role === legalRole) : staticLegal,
267
+ socialLinks: social ?? siteSocialLinks(site, online),
268
+ inquiry: online
269
+ ? metadataLink(site.metadata.inquiry)
270
+ : fallback.inquiry && toNavigationLink(fallback.inquiry),
277
271
  };
278
272
  }
279
273
 
package/src/fetch.ts CHANGED
@@ -8,9 +8,7 @@ import type {
8
8
  Collection,
9
9
  CollectionEntry,
10
10
  Entry,
11
- MarvinAsset,
12
11
  MarvinEntry,
13
- MarvinResource,
14
12
  MarvinSite,
15
13
  PublishedCollectionSummary,
16
14
  Workspace,
@@ -220,17 +218,5 @@ export function createFetcher(backend: MarvinBackend) {
220
218
  workspace(): Promise<Workspace | null> {
221
219
  return guarded('Workspace', null, () => backend.client().getWorkspace());
222
220
  },
223
-
224
- assets(type?: string): Promise<MarvinAsset[]> {
225
- return guarded('Assets', [], () => backend.client().assets.list({ type }));
226
- },
227
-
228
- asset(slugOrId: string): Promise<MarvinAsset | null> {
229
- return guarded(`Asset "${slugOrId}"`, null, () => backend.client().assets.get(slugOrId));
230
- },
231
-
232
- resources(resourceType?: string): Promise<MarvinResource[]> {
233
- return guarded('Resources', [], () => backend.client().resources.list({ resourceType }));
234
- },
235
221
  };
236
222
  }
package/src/index.ts CHANGED
@@ -70,7 +70,7 @@ export function createMarvinContent(options: MarvinContentOptions = {}) {
70
70
  /** True when Marvin is configured and not currently latched out as unreachable. */
71
71
  hasBackend: () => backend.hasBackend(),
72
72
 
73
- /** Resolved site identity, SEO and brand assets. Memoized per process. */
73
+ /** Site identity, SEO and logo/favicon URLs. Memoized per process. */
74
74
  getSite(): Promise<ApiSite> {
75
75
  return siteLoader.get();
76
76
  },
package/src/site.ts CHANGED
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * Marvin's site configuration is optional almost everywhere; a template that had to guard every
5
5
  * field would be unreadable. This resolves it once into an `ApiSite` where title, description,
6
- * siteName, locale and timezone are always present, and where brand asset slugs have already
7
- * become URLs.
6
+ * siteName, locale and timezone are always present. Logo, favicon and SEO image are the URLs
7
+ * Marvin returns, used as-is.
8
8
  */
9
9
 
10
10
  import type { MarvinSite, SiteConfiguration } from '@inneropen/marvin-sdk';
11
11
  import { siteEmbeds } from './embeds.js';
12
12
  import type { MarvinFetcher } from './fetch.js';
13
- import { asRecord, asString, assetUrl } from './normalize.js';
13
+ import { asRecord, asString } from './normalize.js';
14
14
  import type { ApiSeo, ApiSite } from './types.js';
15
15
 
16
16
  export type SiteOptions = {
@@ -116,7 +116,8 @@ function transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite
116
116
  timezone: asString(config.timezone) ?? fallback.timezone,
117
117
  email: asString(config.contactEmail) ?? fallback.email,
118
118
  imprint: asString(metadata.imprint) ?? fallback.imprint,
119
- social: { ...fallback.social, ...normalizeSocial(config.social) },
119
+ // Marvin's social map only: once Marvin answers, the static one must not leak back in.
120
+ social: normalizeSocial(config.social),
120
121
  // Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older
121
122
  // SDKs drop the typed field but pass the blob through untouched, and workspaces configured
122
123
  // before the typed field existed still only have the blob.
@@ -134,77 +135,49 @@ function transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite
134
135
  };
135
136
  }
136
137
 
137
- function looksLikeAssetSlug(value: string): boolean {
138
- return !/^https?:\/\//.test(value) && !value.startsWith('/') && !value.startsWith('data:');
138
+ /** A usable image reference: an absolute URL, a root-relative path or a data URI. */
139
+ function isUrlLike(value: string): boolean {
140
+ return /^https?:\/\//.test(value) || value.startsWith('/') || value.startsWith('data:');
139
141
  }
140
142
 
141
143
  export type SiteLoader = {
142
144
  get(): Promise<ApiSite>;
145
+ /**
146
+ * The build's one "is Marvin up?" answer: true when the site configuration came from Marvin.
147
+ * When it did, Marvin's content is final (an empty or hidden collection shows nothing); the
148
+ * static fallbacks stand in only when it didn't.
149
+ */
150
+ online(): Promise<boolean>;
143
151
  reset(): void;
144
152
  };
145
153
 
146
154
  export function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions = {}): SiteLoader {
147
155
  const fallback = buildFallbackSite(options);
148
156
  let promise: Promise<ApiSite> | null = null;
149
-
150
- async function resolveAssetSlug(slug: string | undefined): Promise<string | undefined> {
151
- if (!slug) return undefined;
152
- return assetUrl(await fetcher.asset(slug));
153
- }
154
-
155
- // Resolve a value only when it looks like a bare asset slug; a URL/path/data URI (or a slug with
156
- // no matching asset) is returned unchanged.
157
- async function resolveMaybeSlug(value: string | undefined): Promise<string | undefined> {
158
- if (!value || !looksLikeAssetSlug(value)) return value;
159
- return (await resolveAssetSlug(value)) ?? value;
160
- }
157
+ let fromMarvin = false;
161
158
 
162
159
  async function load(): Promise<ApiSite> {
163
160
  const marvinSite = await fetcher.site();
161
+ fromMarvin = marvinSite !== null;
164
162
  if (!marvinSite) return fallback;
165
163
 
166
164
  const site = transformMarvinSite(marvinSite, fallback);
167
165
 
168
- // Brand-asset registry: resolve EVERY `brand.<name>` slug → URL in one pass. A site adds a
169
- // shared asset with one config line (`brand.paperFold = "paper-fold"`) plus an upload;
170
- // components then read `site.brand.paperFold`. No code change per asset.
171
- const brandPairs = Object.entries(asRecord(site.metadata.brand)).filter(
172
- (entry): entry is [string, string] =>
173
- typeof entry[1] === 'string' && entry[1].trim().length > 0
174
- );
175
- const resolved = await Promise.all(
176
- brandPairs.map(async ([name, slug]) => [name, await resolveAssetSlug(slug)] as const)
177
- );
166
+ // Brand-asset registry: every `brand.<name>` that is a URL (or root-relative path / data URI),
167
+ // used as-is. Marvin no longer serves assets by slug, so a bare slug can't be resolved here and
168
+ // is left out; `site.logo`, `site.favicon` and `site.seo.image` arrive already resolved.
178
169
  const brand: Record<string, string> = {};
179
- const brandBySlug = new Map<string, string>();
180
- for (const [index, [name, url]] of resolved.entries()) {
181
- if (!url) continue;
182
- brand[name] = url;
183
- brandBySlug.set(brandPairs[index][1], url);
170
+ for (const [name, value] of Object.entries(asRecord(site.metadata.brand))) {
171
+ if (typeof value === 'string' && isUrlLike(value.trim())) brand[name] = value.trim();
184
172
  }
185
173
 
186
- // The og:image may be given as a bare asset slug too. It is usually one of the brand slugs
187
- // (a monogram, a logo), so check what was just resolved before fetching it again.
188
- const rawImage = site.seo.image;
189
- const ogImage =
190
- rawImage && looksLikeAssetSlug(rawImage)
191
- ? (brandBySlug.get(rawImage) ?? (await resolveAssetSlug(rawImage)))
192
- : undefined;
193
-
194
- // The top-level `site_logo` / `site_favicon` fields may be bare asset slugs too (like brand.*
195
- // and og:image) — resolve them so they can reference an uploaded asset by slug. The brand-map
196
- // aliases still win when present.
197
- const logo = brand.logo ?? (await resolveMaybeSlug(site.logo));
198
- const favicon = brand.favicon ?? (await resolveMaybeSlug(site.favicon));
199
-
200
174
  return {
201
175
  ...site,
202
176
  brand,
203
177
  // Convenience aliases for common callers; all of these also live in `site.brand`.
204
- logo,
205
- favicon,
178
+ logo: brand.logo ?? site.logo,
179
+ favicon: brand.favicon ?? site.favicon,
206
180
  seal: brand.seal ?? site.seal,
207
- seo: { ...site.seo, image: ogImage ?? site.seo.image },
208
181
  };
209
182
  }
210
183
 
@@ -215,8 +188,13 @@ export function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions =
215
188
  promise ??= load();
216
189
  return promise;
217
190
  },
191
+ async online() {
192
+ await this.get();
193
+ return fromMarvin;
194
+ },
218
195
  reset() {
219
196
  promise = null;
197
+ fromMarvin = false;
220
198
  },
221
199
  };
222
200
  }
package/src/types.ts CHANGED
@@ -93,11 +93,11 @@ export type ApiSite = {
93
93
  canonicalUrl?: string;
94
94
  logo?: string;
95
95
  favicon?: string;
96
- /** Round maker's stamp / seal — resolved from `site_metadata_json.brand.seal` (asset slug). */
96
+ /** Round maker's stamp / seal — `site_metadata_json.brand.seal`, when it is a URL. */
97
97
  seal?: string;
98
98
  /**
99
- * Brand-asset registry: every `site_metadata_json.brand.<name>` slug resolved to its URL.
100
- * Add a shared asset with one config line; components read `site.brand.<name>`.
99
+ * Brand-asset registry: every `site_metadata_json.brand.<name>` that is a URL, path or data URI,
100
+ * used as-is. Bare asset slugs are left out (Marvin no longer serves assets by slug).
101
101
  */
102
102
  brand?: Record<string, string>;
103
103
  locale: string;