@inneropen/marvin-astro 1.1.0 → 1.1.1

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
@@ -73,6 +73,9 @@ const { site, mainNavigation } = await marvin.getSiteChrome();
73
73
 
74
74
  `posts.all()` · `posts.bySlug(slug)` · `posts.featured()` · `posts.allFeatured()` · `posts.reset()`
75
75
 
76
+ `bySlug` serves from the loaded list once `all()` has resolved — no extra request per detail
77
+ page — and only fetches the entry directly when nothing is loaded yet or the slug isn't in it.
78
+
76
79
  ## Why `hydrate`
77
80
 
78
81
  The collection endpoint returns `PublishedEntryListItem`, which carries core fields and
@@ -82,6 +85,14 @@ which includes it.
82
85
  So if a transform reads any schema-defined field — anything beyond title/slug/summary/metadata —
83
86
  `hydrate: true` is required or those fields come back `undefined`. It costs one request per entry.
84
87
 
88
+ Those requests run at most `hydrateConcurrency` at a time (default 6) rather than all at once, and
89
+ a read that fails is retried with backoff (3 attempts) before the entry is dropped, so a single
90
+ transient failure neither loses an item nor latches the backend off for the rest of the build.
91
+
92
+ ```ts
93
+ createMarvinContent({ hydrateConcurrency: 4 });
94
+ ```
95
+
85
96
  ## Field precedence
86
97
 
87
98
  Every reader on `f` resolves `data_json` first and `metadata_json` second. An empty string counts
package/dist/index.d.ts CHANGED
@@ -39,6 +39,8 @@ declare const ENV_KEYS: {
39
39
  };
40
40
  /** Dev default for the network-failure latch: retry a downed backend after 30s. */
41
41
  declare const DEFAULT_DEV_RETRY_MS = 30000;
42
+ /** How many entry reads `hydrate` keeps in flight at once. */
43
+ declare const DEFAULT_HYDRATE_CONCURRENCY = 6;
42
44
  type MarvinAstroConfig = {
43
45
  apiUrl?: string;
44
46
  siteClientToken?: string;
@@ -51,6 +53,12 @@ type MarvinAstroConfig = {
51
53
  * consistently rather than half-succeed with some pages live and some pages static.
52
54
  */
53
55
  retryAfterMs?: number;
56
+ /**
57
+ * Maximum entry reads in flight at once while hydrating a collection. Firing every read at
58
+ * once makes a large collection time out on itself; the default of 6 keeps a 280-entry
59
+ * hydration well inside the SDK's per-request timeout.
60
+ */
61
+ hydrateConcurrency?: number;
54
62
  logger?: MarvinLogger;
55
63
  /** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */
56
64
  markdown?: MarkdownOptions;
@@ -73,6 +81,7 @@ type ResolvedConfig = {
73
81
  workspaceSlug: string;
74
82
  debug: boolean;
75
83
  retryAfterMs: number;
84
+ hydrateConcurrency: number;
76
85
  logger: MarvinLogger;
77
86
  markdown?: MarkdownOptions;
78
87
  now: () => number;
@@ -404,7 +413,10 @@ type RepositoryOptions<T> = {
404
413
  type Repository<T> = {
405
414
  /** Every item, resolved once per process. */
406
415
  all(): Promise<T[]>;
407
- /** One item by slug — a direct entry fetch, falling back to a scan of `all()`. */
416
+ /**
417
+ * One item by slug. Served from `all()` once that has loaded; otherwise a direct entry
418
+ * fetch, falling back to a scan of `all()`.
419
+ */
408
420
  bySlug(slug: string): Promise<T | undefined>;
409
421
  /** The first featured item, or the first item when none is marked. */
410
422
  featured(): Promise<T | undefined>;
@@ -573,4 +585,4 @@ declare function createMarvinContent(options?: MarvinContentOptions): {
573
585
  reset(): void;
574
586
  };
575
587
 
576
- export { ApiImage, ApiNavigationLink, ApiResourceLink, ApiSeo, ApiSite, ApiSiteChrome, ApiSocialLink, AssetSelectOptions, type ChromeLoader, type ChromeOptions, DEFAULT_DEV_RETRY_MS, ENV_KEYS, type FieldAccessor, type HrefContext, type HydratedEntry, type IconFieldOptions, type ImageFieldOptions, type MarkdownFieldOptions, type MarkdownOptions, type MarkdownRenderer, type MarvinAstroConfig, type MarvinBackend, type MarvinContent, MarvinContentEntry, type MarvinContentOptions, type MarvinFetcher, type MarvinLogger, type NavigationContext, type NavigationLinkInput, type Repository, type RepositoryContext, type RepositoryOptions, type ResolveHref, type ResolvedConfig, type ResourceFieldOptions, SectionLanding, type SectionLandingOptions, type SiteLoader, type SiteOptions, type SocialLinkInput, asNumber, asRecord, asString, asStringArray, assetAlt, assetField, assetUrl, buildFallbackSite, buildSeo, collectionRole, collectionSlugs, createBackend, createChromeLoader, createFetcher, createFieldAccessor, createMarkdownRenderer, createMarvinContent, createRepository, createSiteLoader, defaultResolveHref, describeConfig, entryAssets, entryData, entryField, entryMetadata, entryResources, errorMessage, field, focalPoint, formatDisplayDate, isExternalHref, isNetworkFailure, loadSectionLanding, metadataLink, preserveSoftBreaks, readEnv, resolveConfig, resourceField, resourceRole, selectAssetByRole, selectEntryAsset, selectFeaturedAsset, selectIconAsset, selectImageAsset, selectValuesForPage, socialLinkFromKey, toNavigationLink };
588
+ export { ApiImage, ApiNavigationLink, ApiResourceLink, ApiSeo, ApiSite, ApiSiteChrome, ApiSocialLink, AssetSelectOptions, type ChromeLoader, type ChromeOptions, DEFAULT_DEV_RETRY_MS, DEFAULT_HYDRATE_CONCURRENCY, ENV_KEYS, type FieldAccessor, type HrefContext, type HydratedEntry, type IconFieldOptions, type ImageFieldOptions, type MarkdownFieldOptions, type MarkdownOptions, type MarkdownRenderer, type MarvinAstroConfig, type MarvinBackend, type MarvinContent, MarvinContentEntry, type MarvinContentOptions, type MarvinFetcher, type MarvinLogger, type NavigationContext, type NavigationLinkInput, type Repository, type RepositoryContext, type RepositoryOptions, type ResolveHref, type ResolvedConfig, type ResourceFieldOptions, SectionLanding, type SectionLandingOptions, type SiteLoader, type SiteOptions, type SocialLinkInput, asNumber, asRecord, asString, asStringArray, assetAlt, assetField, assetUrl, buildFallbackSite, buildSeo, collectionRole, collectionSlugs, createBackend, createChromeLoader, createFetcher, createFieldAccessor, createMarkdownRenderer, createMarvinContent, createRepository, createSiteLoader, defaultResolveHref, describeConfig, entryAssets, entryData, entryField, entryMetadata, entryResources, errorMessage, field, focalPoint, formatDisplayDate, isExternalHref, isNetworkFailure, loadSectionLanding, metadataLink, preserveSoftBreaks, readEnv, resolveConfig, resourceField, resourceRole, selectAssetByRole, selectEntryAsset, selectFeaturedAsset, selectIconAsset, selectImageAsset, selectValuesForPage, socialLinkFromKey, toNavigationLink };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ var ENV_KEYS = {
9
9
  debug: "MARVIN_DEBUG"
10
10
  };
11
11
  var DEFAULT_DEV_RETRY_MS = 3e4;
12
+ var DEFAULT_HYDRATE_CONCURRENCY = 6;
12
13
  function metaEnv() {
13
14
  try {
14
15
  return import.meta.env;
@@ -47,6 +48,7 @@ function resolveConfig(options = {}) {
47
48
  workspaceSlug,
48
49
  debug,
49
50
  retryAfterMs: options.retryAfterMs ?? (isProduction(env) ? Number.POSITIVE_INFINITY : DEFAULT_DEV_RETRY_MS),
51
+ hydrateConcurrency: Math.max(1, Math.floor(options.hydrateConcurrency ?? DEFAULT_HYDRATE_CONCURRENCY)),
50
52
  logger: options.logger ?? console,
51
53
  markdown: options.markdown,
52
54
  now: options.now ?? (() => Date.now()),
@@ -402,6 +404,24 @@ function createChromeLoader(fetcher, siteLoader, options = {}) {
402
404
  }
403
405
 
404
406
  // src/fetch.ts
407
+ var HYDRATE_ATTEMPTS = 3;
408
+ var HYDRATE_BACKOFF_MS = 500;
409
+ function sleep(ms) {
410
+ return new Promise((resolve) => setTimeout(resolve, ms));
411
+ }
412
+ async function mapWithConcurrency(items, limit, run) {
413
+ const results = new Array(items.length);
414
+ let next = 0;
415
+ async function worker() {
416
+ while (next < items.length) {
417
+ const index = next++;
418
+ results[index] = await run(items[index]);
419
+ }
420
+ }
421
+ const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
422
+ await Promise.all(workers);
423
+ return results;
424
+ }
405
425
  function createFetcher(backend) {
406
426
  async function guarded(label, empty, run) {
407
427
  if (!backend.hasBackend()) return empty;
@@ -416,14 +436,36 @@ function createFetcher(backend) {
416
436
  async function entry(slug) {
417
437
  return guarded(`Entry "${slug}"`, null, () => backend.client().entry(slug));
418
438
  }
439
+ async function entryForHydration(slug) {
440
+ if (!backend.hasBackend()) return null;
441
+ let failure;
442
+ for (let attempt = 1; attempt <= HYDRATE_ATTEMPTS; attempt++) {
443
+ if (attempt > 1) {
444
+ await sleep(HYDRATE_BACKOFF_MS * 2 ** (attempt - 2));
445
+ if (backend.isLatched()) backend.clearLatch();
446
+ }
447
+ try {
448
+ return await backend.client().entry(slug);
449
+ } catch (error) {
450
+ failure = error;
451
+ }
452
+ }
453
+ backend.remember(failure);
454
+ backend.warn(
455
+ `Entry "${slug}" dropped after ${HYDRATE_ATTEMPTS} attempts: ${errorMessage(failure)}`
456
+ );
457
+ return null;
458
+ }
419
459
  async function hydrate(entries) {
420
- const hydrated = await Promise.all(
421
- entries.map(async (item) => {
460
+ const hydrated = await mapWithConcurrency(
461
+ entries,
462
+ backend.config.hydrateConcurrency,
463
+ async (item) => {
422
464
  if (typeof item.field === "function" || "data" in item || "dataJson" in item) {
423
465
  return item;
424
466
  }
425
- return entry(item.slug ?? "");
426
- })
467
+ return entryForHydration(item.slug ?? "");
468
+ }
427
469
  );
428
470
  return hydrated.filter((item) => Boolean(item));
429
471
  }
@@ -769,7 +811,14 @@ function createRepository(context, options) {
769
811
  }
770
812
  return useFallback();
771
813
  }
814
+ function findInList(items, slug) {
815
+ return items.find((item) => slugOf(item) === slug);
816
+ }
772
817
  async function loadBySlug(slug) {
818
+ if (allPromise) {
819
+ const listed = findInList(await allPromise, slug);
820
+ if (listed) return listed;
821
+ }
773
822
  if (fetcher.backend.hasBackend()) {
774
823
  try {
775
824
  const entry = await fetcher.entry(slug);
@@ -779,7 +828,7 @@ function createRepository(context, options) {
779
828
  fetcher.backend.warn(`Entry "${slug}" failed, using fallback: ${errorMessage(error)}`);
780
829
  }
781
830
  }
782
- return (await all()).find((item) => slugOf(item) === slug);
831
+ return findInList(await all(), slug);
783
832
  }
784
833
  function all() {
785
834
  allPromise ??= load();
@@ -1009,6 +1058,7 @@ function createMarvinContent(options = {}) {
1009
1058
  }
1010
1059
  export {
1011
1060
  DEFAULT_DEV_RETRY_MS,
1061
+ DEFAULT_HYDRATE_CONCURRENCY,
1012
1062
  ENV_KEYS,
1013
1063
  asNumber,
1014
1064
  asRecord,
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/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\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 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 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 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 { ApiNavigationLink, ApiSiteChrome, ApiSocialLink, MarvinContentEntry } 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 * 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\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 legalRole = options.legalRole ?? 'legal';\n const footerColumnThreshold = options.footerColumnThreshold ?? 4;\n const resolveHref = options.resolveHref ?? defaultResolveHref;\n const navCollections = new Set([mainCollection, footerCollection]);\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[]; fromBackend: boolean }> {\n if (!fetcher.backend.hasBackend()) return { links: staticLinks, 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 if (entries.length === 0) return { links: staticLinks, fromBackend: false };\n\n return { links: entries.map((entry) => entryToLink(entry, slug, context)), 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 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 socialLinks = Object.entries(site.social).map(([key, href]) =>\n socialLinkFromKey(key, href)\n );\n if (site.email && !socialLinks.some((link) => link.icon === 'email')) {\n socialLinks.push(socialLinkFromKey('email', `mailto:${site.email}`));\n }\n\n // Split footer links by membership role: legal → the legal strip; everything else → columns.\n const legalLinks = footer.fromBackend\n ? footer.links.filter((link) => link.role === legalRole)\n : [];\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: legalLinks.length > 0 ? legalLinks : staticLegal,\n socialLinks:\n socialLinks.length > 0\n ? socialLinks\n : (fallback.socialLinks ?? []).map((link) => ({\n ...toNavigationLink(link),\n icon: link.icon,\n })),\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\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 * Turn list items into full entries.\n *\n * The collection endpoint returns `PublishedEntryListItem` — core fields plus\n * `metadata_json`, but NOT `data_json`. `PublishedEntryRead` (the single-entry endpoint)\n * includes it. So any field defined by the entry type's schema is unreadable from a list\n * item; it only appears after this hydration pass. Items that already carry data are passed\n * through untouched.\n */\n async function hydrate(entries: MarvinContentEntry[]): Promise<HydratedEntry[]> {\n const hydrated = await Promise.all(\n entries.map(async (item) => {\n if (\n typeof (item as { field?: unknown }).field === 'function' ||\n 'data' in item ||\n 'dataJson' in item\n ) {\n return item as HydratedEntry;\n }\n return entry(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 async hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]> {\n return hydrate(await this.collectionEntries(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 } from 'marked';\n\nexport type MarkdownOptions = MarkedOptions;\n\nexport type MarkdownRenderer = (\n markdown: string | string[] | null | undefined\n) => Promise<string>;\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 */\nexport function createMarkdownRenderer(options: MarkdownOptions = {}): MarkdownRenderer {\n const marked = new Marked();\n marked.setOptions({ gfm: true, breaks: false, ...options });\n\n return async (markdown) => {\n const source = Array.isArray(markdown) ? markdown.join('\\n\\n') : (markdown ?? '');\n if (!source) return '';\n return await marked.parse(source);\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","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 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\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\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 },\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 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 omits `data_json`; the\n * single-entry endpoint returns `PublishedEntryRead`, which includes it. So if the transform\n * reads ANY schema-defined field — anything not title/slug/summary/metadata — this must be\n * `true` or those fields come back `undefined`. It costs one request per entry.\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 /** One item by slug — a direct entry fetch, falling back to a scan of `all()`. */\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 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<T[]> {\n return arrange((await options.fallback?.()) ?? []);\n }\n\n async function load(): Promise<T[]> {\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 return arrange(items);\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 async function loadBySlug(slug: string): Promise<T | undefined> {\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 (await all()).find((item) => slugOf(item) === slug);\n }\n\n function all(): Promise<T[]> {\n allPromise ??= load();\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 bySlugCache.clear();\n },\n };\n}\n","import 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 ? await renderMarkdown(preserveSoftBreaks(source)) : 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 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 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) and beside `@inneropen/marvin-renderers-core`\n * (entry-type → component mapping). This layer is the part every Marvin-backed site rewrites:\n * repositories with a static fallback, site chrome from collections, and payload normalization.\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 ENV_KEYS,\n describeConfig,\n readEnv,\n resolveConfig,\n type MarvinAstroConfig,\n type MarvinLogger,\n type ResolvedConfig,\n} from './config.js';\nexport { createFetcher, 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} from './markdown.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/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 { ApiNavigationLink, ApiSiteChrome, ApiSocialLink, MarvinContentEntry } 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 * 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\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 legalRole = options.legalRole ?? 'legal';\n const footerColumnThreshold = options.footerColumnThreshold ?? 4;\n const resolveHref = options.resolveHref ?? defaultResolveHref;\n const navCollections = new Set([mainCollection, footerCollection]);\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[]; fromBackend: boolean }> {\n if (!fetcher.backend.hasBackend()) return { links: staticLinks, 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 if (entries.length === 0) return { links: staticLinks, fromBackend: false };\n\n return { links: entries.map((entry) => entryToLink(entry, slug, context)), 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 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 socialLinks = Object.entries(site.social).map(([key, href]) =>\n socialLinkFromKey(key, href)\n );\n if (site.email && !socialLinks.some((link) => link.icon === 'email')) {\n socialLinks.push(socialLinkFromKey('email', `mailto:${site.email}`));\n }\n\n // Split footer links by membership role: legal → the legal strip; everything else → columns.\n const legalLinks = footer.fromBackend\n ? footer.links.filter((link) => link.role === legalRole)\n : [];\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: legalLinks.length > 0 ? legalLinks : staticLegal,\n socialLinks:\n socialLinks.length > 0\n ? socialLinks\n : (fallback.socialLinks ?? []).map((link) => ({\n ...toNavigationLink(link),\n icon: link.icon,\n })),\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/** 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 plus\n * `metadata_json`, but NOT `data_json`. `PublishedEntryRead` (the single-entry endpoint)\n * includes it. So any field defined by the entry type's schema is unreadable from a list\n * item; it only appears after this hydration pass. Items that already carry data are passed\n * through untouched.\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 async function hydrate(entries: MarvinContentEntry[]): Promise<HydratedEntry[]> {\n const hydrated = await mapWithConcurrency(\n entries,\n backend.config.hydrateConcurrency,\n async (item) => {\n if (\n typeof (item as { field?: unknown }).field === 'function' ||\n 'data' in item ||\n 'dataJson' in item\n ) {\n return item as HydratedEntry;\n }\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 async hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]> {\n return hydrate(await this.collectionEntries(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 } from 'marked';\n\nexport type MarkdownOptions = MarkedOptions;\n\nexport type MarkdownRenderer = (\n markdown: string | string[] | null | undefined\n) => Promise<string>;\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 */\nexport function createMarkdownRenderer(options: MarkdownOptions = {}): MarkdownRenderer {\n const marked = new Marked();\n marked.setOptions({ gfm: true, breaks: false, ...options });\n\n return async (markdown) => {\n const source = Array.isArray(markdown) ? markdown.join('\\n\\n') : (markdown ?? '');\n if (!source) return '';\n return await marked.parse(source);\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","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 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\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\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 },\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 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 omits `data_json`; the\n * single-entry endpoint returns `PublishedEntryRead`, which includes it. So if the transform\n * reads ANY schema-defined field — anything not title/slug/summary/metadata — this must be\n * `true` or those fields come back `undefined`. It costs one request per entry.\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 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<T[]> {\n return arrange((await options.fallback?.()) ?? []);\n }\n\n async function load(): Promise<T[]> {\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 return arrange(items);\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 the list has loaded, every slug in it is already transformed — a per-entry fetch\n // would be a second request (and a second hydration) for the same content. On a 280-entry\n // site that was ~1.4s per detail page.\n if (allPromise) {\n const listed = findInList(await allPromise, slug);\n if (listed) 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 findInList(await all(), slug);\n }\n\n function all(): Promise<T[]> {\n allPromise ??= load();\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 bySlugCache.clear();\n },\n };\n}\n","import 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 ? await renderMarkdown(preserveSoftBreaks(source)) : 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 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 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) and beside `@inneropen/marvin-renderers-core`\n * (entry-type → component mapping). This layer is the part every Marvin-backed site rewrites:\n * repositories with a static fallback, site chrome from collections, and payload normalization.\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, 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} from './markdown.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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneropen/marvin-astro",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Site integration for Marvin CMS on Astro — repositories, site chrome, and payload normalization",
5
5
  "type": "module",
6
6
  "exports": {