@inneropen/marvin-astro 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +51 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chrome.ts +73 -32
- package/src/site.ts +15 -1
package/README.md
CHANGED
|
@@ -141,7 +141,15 @@ a site adds a shared brand asset with one config line and reads `site.brand.<nam
|
|
|
141
141
|
change per asset. `logo`, `favicon` and `seal` are aliased onto the top level.
|
|
142
142
|
|
|
143
143
|
`getSiteChrome()` reads `main-navigation` and `footer-navigation` collections, splits
|
|
144
|
-
`role: 'legal'` entries into `legalLinks`, and groups the rest into footer columns.
|
|
144
|
+
`role: 'legal'` entries into `legalLinks`, and groups the rest into footer columns. Social links
|
|
145
|
+
come from a `social-links` collection when the workspace has one (each entry's label, href and
|
|
146
|
+
optional `icon` field, in order), otherwise from the site's `social` map.
|
|
147
|
+
|
|
148
|
+
Fallbacks follow one build-time check: did the site configuration come from Marvin
|
|
149
|
+
(`siteLoader.online()`)? If it did, Marvin's content is final — an empty, hidden or missing
|
|
150
|
+
collection renders nothing, and the static `site.fallback.social` / `chrome.fallback` links are
|
|
151
|
+
never mixed in. Only when Marvin is down or unconfigured does the whole chrome come from the static
|
|
152
|
+
fallbacks. A nav entry's
|
|
145
153
|
route comes from an explicit `href`/`url`/`path` field if it has one, otherwise from
|
|
146
154
|
`resolveHref`:
|
|
147
155
|
|
package/dist/index.d.ts
CHANGED
|
@@ -219,6 +219,12 @@ declare function buildSeo(raw: Record<string, unknown> | undefined, identity: {
|
|
|
219
219
|
declare function buildFallbackSite(options?: SiteOptions): ApiSite;
|
|
220
220
|
type SiteLoader = {
|
|
221
221
|
get(): Promise<ApiSite>;
|
|
222
|
+
/**
|
|
223
|
+
* The build's one "is Marvin up?" answer: true when the site configuration came from Marvin.
|
|
224
|
+
* When it did, Marvin's content is final (an empty or hidden collection shows nothing); the
|
|
225
|
+
* static fallbacks stand in only when it didn't.
|
|
226
|
+
*/
|
|
227
|
+
online(): Promise<boolean>;
|
|
222
228
|
reset(): void;
|
|
223
229
|
};
|
|
224
230
|
declare function createSiteLoader(fetcher: MarvinFetcher, options?: SiteOptions): SiteLoader;
|
|
@@ -258,6 +264,12 @@ type ChromeOptions = {
|
|
|
258
264
|
mainCollection?: string;
|
|
259
265
|
/** Collection holding the footer nav. Default `'footer-navigation'`. */
|
|
260
266
|
footerCollection?: string;
|
|
267
|
+
/**
|
|
268
|
+
* Collection holding the social links. Default `'social-links'`. When it has entries, they
|
|
269
|
+
* (label, href, optional `icon` field) are exactly the social links, in order; otherwise the
|
|
270
|
+
* site `social` map is used.
|
|
271
|
+
*/
|
|
272
|
+
socialCollection?: string;
|
|
261
273
|
/**
|
|
262
274
|
* Route for a nav entry that carries no explicit `href`/`url`/`path` field.
|
|
263
275
|
*
|
package/dist/index.js
CHANGED
|
@@ -325,13 +325,23 @@ function socialLinkFromKey(key, href) {
|
|
|
325
325
|
external: isExternalHref(href)
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
|
+
function socialIconFor(href, label) {
|
|
329
|
+
if (href.startsWith("mailto:")) return "email";
|
|
330
|
+
try {
|
|
331
|
+
const host = new URL(href).hostname.replace(/^(www|m)\./, "");
|
|
332
|
+
return host.split(".")[0] ?? label.toLowerCase();
|
|
333
|
+
} catch {
|
|
334
|
+
return label.toLowerCase();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
328
337
|
function createChromeLoader(fetcher, siteLoader, options = {}) {
|
|
329
338
|
const mainCollection = options.mainCollection ?? "main-navigation";
|
|
330
339
|
const footerCollection = options.footerCollection ?? "footer-navigation";
|
|
340
|
+
const socialCollection = options.socialCollection ?? "social-links";
|
|
331
341
|
const legalRole = options.legalRole ?? "legal";
|
|
332
342
|
const footerColumnThreshold = options.footerColumnThreshold ?? 4;
|
|
333
343
|
const resolveHref = options.resolveHref ?? defaultResolveHref;
|
|
334
|
-
const navCollections = /* @__PURE__ */ new Set([mainCollection, footerCollection]);
|
|
344
|
+
const navCollections = /* @__PURE__ */ new Set([mainCollection, footerCollection, socialCollection]);
|
|
335
345
|
const fallback = options.fallback ?? {};
|
|
336
346
|
let promise = null;
|
|
337
347
|
function entryToLink(entry, collectionSlug, context) {
|
|
@@ -352,44 +362,54 @@ function createChromeLoader(fetcher, siteLoader, options = {}) {
|
|
|
352
362
|
role: collectionRole(entry, collectionSlug)
|
|
353
363
|
};
|
|
354
364
|
}
|
|
355
|
-
async function navigationCollection(slug, context,
|
|
356
|
-
if (!
|
|
365
|
+
async function navigationCollection(slug, context, online) {
|
|
366
|
+
if (!online) return { links: [], entries: [] };
|
|
357
367
|
const entries = await fetcher.hydratedCollectionEntries(slug);
|
|
358
|
-
|
|
359
|
-
return { links: entries.map((entry) => entryToLink(entry, slug, context)), fromBackend: true };
|
|
368
|
+
return { links: entries.map((entry) => entryToLink(entry, slug, context)), entries };
|
|
360
369
|
}
|
|
361
370
|
function groupFooterLinks(links) {
|
|
362
371
|
if (links.length <= footerColumnThreshold) return [links];
|
|
363
372
|
const midpoint = Math.ceil(links.length / 2);
|
|
364
373
|
return [links.slice(0, midpoint), links.slice(midpoint)];
|
|
365
374
|
}
|
|
375
|
+
async function socialEntries(online) {
|
|
376
|
+
const social = await navigationCollection(socialCollection, "footer", online);
|
|
377
|
+
if (social.links.length === 0) return null;
|
|
378
|
+
return social.links.map((link, index) => ({
|
|
379
|
+
...link,
|
|
380
|
+
icon: asString(entryField(social.entries[index], "icon")) ?? socialIconFor(link.href, link.label)
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
function siteSocialLinks(site, online) {
|
|
384
|
+
const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));
|
|
385
|
+
if (site.email && !links.some((link) => link.icon === "email")) {
|
|
386
|
+
links.push(socialLinkFromKey("email", `mailto:${site.email}`));
|
|
387
|
+
}
|
|
388
|
+
if (links.length > 0 || online) return links;
|
|
389
|
+
return (fallback.socialLinks ?? []).map((link) => ({
|
|
390
|
+
...toNavigationLink(link),
|
|
391
|
+
icon: link.icon
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
366
394
|
async function load() {
|
|
367
395
|
const site = await siteLoader.get();
|
|
368
396
|
const staticMain = (fallback.mainNavigation ?? []).map(toNavigationLink);
|
|
369
397
|
const staticFooter = (fallback.footerNavigation ?? []).map(
|
|
370
398
|
(group) => group.map(toNavigationLink)
|
|
371
399
|
);
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
);
|
|
377
|
-
if (site.email && !socialLinks.some((link) => link.icon === "email")) {
|
|
378
|
-
socialLinks.push(socialLinkFromKey("email", `mailto:${site.email}`));
|
|
379
|
-
}
|
|
380
|
-
const legalLinks = footer.fromBackend ? footer.links.filter((link) => link.role === legalRole) : [];
|
|
400
|
+
const online = await siteLoader.online();
|
|
401
|
+
const main = await navigationCollection(mainCollection, "main", online);
|
|
402
|
+
const footer = await navigationCollection(footerCollection, "footer", online);
|
|
403
|
+
const social = await socialEntries(online);
|
|
381
404
|
const columnLinks = footer.links.filter((link) => link.role !== legalRole);
|
|
382
405
|
const staticLegal = (fallback.legalLinks ?? []).map(toNavigationLink);
|
|
383
406
|
return {
|
|
384
407
|
site,
|
|
385
|
-
mainNavigation: main.links,
|
|
386
|
-
footerNavigation:
|
|
387
|
-
legalLinks:
|
|
388
|
-
socialLinks:
|
|
389
|
-
|
|
390
|
-
icon: link.icon
|
|
391
|
-
})),
|
|
392
|
-
inquiry: metadataLink(site.metadata.inquiry) ?? (fallback.inquiry ? toNavigationLink(fallback.inquiry) : void 0)
|
|
408
|
+
mainNavigation: online ? main.links : staticMain,
|
|
409
|
+
footerNavigation: online ? groupFooterLinks(columnLinks) : staticFooter,
|
|
410
|
+
legalLinks: online ? footer.links.filter((link) => link.role === legalRole) : staticLegal,
|
|
411
|
+
socialLinks: social ?? siteSocialLinks(site, online),
|
|
412
|
+
inquiry: online ? metadataLink(site.metadata.inquiry) : fallback.inquiry && toNavigationLink(fallback.inquiry)
|
|
393
413
|
};
|
|
394
414
|
}
|
|
395
415
|
return {
|
|
@@ -1045,7 +1065,8 @@ function transformMarvinSite(marvinSite, fallback) {
|
|
|
1045
1065
|
timezone: asString(config.timezone) ?? fallback.timezone,
|
|
1046
1066
|
email: asString(config.contactEmail) ?? fallback.email,
|
|
1047
1067
|
imprint: asString(metadata.imprint) ?? fallback.imprint,
|
|
1048
|
-
social:
|
|
1068
|
+
// Marvin's social map only: once Marvin answers, the static one must not leak back in.
|
|
1069
|
+
social: normalizeSocial(config.social),
|
|
1049
1070
|
// Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older
|
|
1050
1071
|
// SDKs drop the typed field but pass the blob through untouched, and workspaces configured
|
|
1051
1072
|
// before the typed field existed still only have the blob.
|
|
@@ -1068,6 +1089,7 @@ function looksLikeAssetSlug(value) {
|
|
|
1068
1089
|
function createSiteLoader(fetcher, options = {}) {
|
|
1069
1090
|
const fallback = buildFallbackSite(options);
|
|
1070
1091
|
let promise = null;
|
|
1092
|
+
let fromMarvin = false;
|
|
1071
1093
|
async function resolveAssetSlug(slug) {
|
|
1072
1094
|
if (!slug) return void 0;
|
|
1073
1095
|
return assetUrl(await fetcher.asset(slug));
|
|
@@ -1078,6 +1100,7 @@ function createSiteLoader(fetcher, options = {}) {
|
|
|
1078
1100
|
}
|
|
1079
1101
|
async function load() {
|
|
1080
1102
|
const marvinSite = await fetcher.site();
|
|
1103
|
+
fromMarvin = marvinSite !== null;
|
|
1081
1104
|
if (!marvinSite) return fallback;
|
|
1082
1105
|
const site = transformMarvinSite(marvinSite, fallback);
|
|
1083
1106
|
const brandPairs = Object.entries(asRecord(site.metadata.brand)).filter(
|
|
@@ -1112,8 +1135,13 @@ function createSiteLoader(fetcher, options = {}) {
|
|
|
1112
1135
|
promise ??= load();
|
|
1113
1136
|
return promise;
|
|
1114
1137
|
},
|
|
1138
|
+
async online() {
|
|
1139
|
+
await this.get();
|
|
1140
|
+
return fromMarvin;
|
|
1141
|
+
},
|
|
1115
1142
|
reset() {
|
|
1116
1143
|
promise = null;
|
|
1144
|
+
fromMarvin = false;
|
|
1117
1145
|
}
|
|
1118
1146
|
};
|
|
1119
1147
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/config.ts","../src/normalize.ts","../src/chrome.ts","../src/fetch.ts","../src/markdown.ts","../src/embeds.ts","../src/format.ts","../src/fields.ts","../src/repository.ts","../src/sections.ts","../src/site.ts","../src/index.ts"],"sourcesContent":["/**\n * SDK client construction and the network-failure latch.\n *\n * The latch exists because a static build asks for content once per path. When the backend is\n * down, that is N failed requests with N timeouts before the build gives up. The latch trips on\n * the first network failure and short-circuits the rest.\n *\n * Unlike the hand-rolled original it is *retryable*: it expires after `retryAfterMs` so a dev\n * server recovers on its own when the backend comes back, instead of serving stale static data\n * until someone restarts it. Production keeps the permanent latch — a build should fail fast and\n * consistently rather than half-succeed.\n */\n\nimport { createMarvinClient } from '@inneropen/marvin-sdk';\nimport type { MarvinClient } from '@inneropen/marvin-sdk';\nimport {\n describeConfig,\n resolveConfig,\n type MarvinAstroConfig,\n type ResolvedConfig,\n} from './config.js';\n\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\n/**\n * A network failure means \"the backend is unreachable\", not \"this request was bad\". Only the\n * former should trip the latch — a 404 on one entry says nothing about the next one.\n */\nexport function isNetworkFailure(error: unknown): boolean {\n const message = errorMessage(error).toLowerCase();\n return (\n message.includes('network error') ||\n message.includes('fetch failed') ||\n message.includes('econnrefused') ||\n message.includes('enotfound') ||\n message.includes('etimedout')\n );\n}\n\nexport type MarvinBackend = {\n readonly config: ResolvedConfig;\n /** True when Marvin is configured AND not currently latched out. */\n hasBackend(): boolean;\n /** The SDK client. Throws when Marvin is not configured. */\n client(): MarvinClient;\n /** Trip the latch if this error indicates the backend is unreachable. */\n remember(error: unknown): void;\n /** True while the latch is closed (backend considered unreachable). */\n isLatched(): boolean;\n /** Manually re-open the latch, e.g. after a deploy. */\n clearLatch(): void;\n warn(message: string): void;\n};\n\nexport function createBackend(options: MarvinAstroConfig = {}): MarvinBackend {\n const config = resolveConfig(options);\n let client: MarvinClient | null = null;\n let latchedAt: number | null = null;\n let envLogged = false;\n\n function isLatched(): boolean {\n if (latchedAt === null) return false;\n if (config.retryAfterMs === Number.POSITIVE_INFINITY) return true;\n if (config.now() - latchedAt < config.retryAfterMs) return true;\n\n // Expired — re-open and let the next call find out whether the backend is back.\n latchedAt = null;\n return false;\n }\n\n function hasBackend(): boolean {\n if (config.debug && !envLogged) {\n envLogged = true;\n config.logger.log('[marvin-astro] env check:', describeConfig(config));\n }\n if (!config.configured) return false;\n return !isLatched();\n }\n\n return {\n config,\n hasBackend,\n isLatched,\n clearLatch() {\n latchedAt = null;\n },\n remember(error: unknown) {\n if (isNetworkFailure(error)) latchedAt = config.now();\n },\n warn(message: string) {\n config.logger.warn(`[marvin-astro] ${message}`);\n },\n client(): MarvinClient {\n if (!config.configured) {\n throw new Error(\n '[marvin-astro] Marvin is not configured. Set MARVIN_API_URL, ' +\n 'MARVIN_SITE_CLIENT_TOKEN and MARVIN_WORKSPACE_SLUG, or pass them to createMarvinContent().'\n );\n }\n client ??= (config.createClient\n ? config.createClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })\n : createMarvinClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })) as MarvinClient;\n return client;\n },\n };\n}\n","/**\n * Configuration + environment resolution.\n *\n * Precedence for every value: explicit option → `import.meta.env` → `process.env`.\n * Astro exposes server-side env through `import.meta.env`; a plain Node script\n * (a smoke test, a build hook) only has `process.env`. Both work, unchanged.\n */\n\nimport type { MarkdownOptions } from './markdown.js';\n\nexport type MarvinLogger = {\n log: (message: string, ...args: unknown[]) => void;\n warn: (message: string, ...args: unknown[]) => void;\n error: (message: string, ...args: unknown[]) => void;\n};\n\nexport const ENV_KEYS = {\n apiUrl: 'MARVIN_API_URL',\n siteClientToken: 'MARVIN_SITE_CLIENT_TOKEN',\n workspaceSlug: 'MARVIN_WORKSPACE_SLUG',\n debug: 'MARVIN_DEBUG',\n} as const;\n\n/** Dev default for the network-failure latch: retry a downed backend after 30s. */\nexport const DEFAULT_DEV_RETRY_MS = 30_000;\n\n/** How many entry reads `hydrate` keeps in flight at once. */\nexport const DEFAULT_HYDRATE_CONCURRENCY = 6;\n\nexport type MarvinAstroConfig = {\n apiUrl?: string;\n siteClientToken?: string;\n workspaceSlug?: string;\n /** Log the resolved env once at startup, and enable SDK request logging. */\n debug?: boolean;\n /**\n * How long the network-failure latch stays closed before the backend is retried.\n * Defaults to 30s in dev and `Infinity` in production — a build should fail fast and\n * consistently rather than half-succeed with some pages live and some pages static.\n */\n retryAfterMs?: number;\n /**\n * Maximum entry reads in flight at once while hydrating a collection. Firing every read at\n * once makes a large collection time out on itself; the default of 6 keeps a 280-entry\n * hydration well inside the SDK's per-request timeout.\n */\n hydrateConcurrency?: number;\n logger?: MarvinLogger;\n /** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */\n markdown?: MarkdownOptions;\n /** Explicit env source. Highest priority after direct options — mainly a test seam. */\n env?: Record<string, string | undefined>;\n /** Injectable clock, so latch expiry is testable without waiting. */\n now?: () => number;\n /** Injectable SDK client factory — a test seam; defaults to the SDK's `createMarvinClient`. */\n createClient?: (config: MarvinClientOptions) => unknown;\n};\n\nexport type MarvinClientOptions = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n};\n\nexport type ResolvedConfig = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n retryAfterMs: number;\n hydrateConcurrency: number;\n logger: MarvinLogger;\n markdown?: MarkdownOptions;\n now: () => number;\n createClient?: (config: MarvinClientOptions) => unknown;\n /** True when all three connection values are present. */\n configured: boolean;\n};\n\nfunction metaEnv(): Record<string, unknown> | undefined {\n try {\n return (import.meta as unknown as { env?: Record<string, unknown> }).env;\n } catch {\n return undefined;\n }\n}\n\nfunction processEnv(): Record<string, string | undefined> | undefined {\n try {\n return typeof process !== 'undefined' ? process.env : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction nonEmpty(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\n/** Read one env var, honouring the documented precedence. */\nexport function readEnv(\n key: string,\n override?: Record<string, string | undefined>\n): string | undefined {\n return nonEmpty(override?.[key]) ?? nonEmpty(metaEnv()?.[key]) ?? nonEmpty(processEnv()?.[key]);\n}\n\nfunction isProduction(override?: Record<string, string | undefined>): boolean {\n const prodFlag = metaEnv()?.PROD;\n if (typeof prodFlag === 'boolean') return prodFlag;\n const mode = readEnv('NODE_ENV', override) ?? readEnv('MODE', override);\n return mode === 'production';\n}\n\nexport function resolveConfig(options: MarvinAstroConfig = {}): ResolvedConfig {\n const env = options.env;\n const apiUrl = options.apiUrl ?? readEnv(ENV_KEYS.apiUrl, env) ?? '';\n const siteClientToken = options.siteClientToken ?? readEnv(ENV_KEYS.siteClientToken, env) ?? '';\n const workspaceSlug = options.workspaceSlug ?? readEnv(ENV_KEYS.workspaceSlug, env) ?? '';\n const debug = options.debug ?? readEnv(ENV_KEYS.debug, env) === 'true';\n\n return {\n apiUrl,\n siteClientToken,\n workspaceSlug,\n debug,\n retryAfterMs:\n options.retryAfterMs ??\n (isProduction(env) ? Number.POSITIVE_INFINITY : DEFAULT_DEV_RETRY_MS),\n hydrateConcurrency: Math.max(1, Math.floor(options.hydrateConcurrency ?? DEFAULT_HYDRATE_CONCURRENCY)),\n logger: options.logger ?? console,\n markdown: options.markdown,\n now: options.now ?? (() => Date.now()),\n createClient: options.createClient,\n configured: Boolean(apiUrl && siteClientToken && workspaceSlug),\n };\n}\n\n/** One-line summary of the resolved env, with the token masked. For `debug` logging. */\nexport function describeConfig(config: ResolvedConfig): Record<string, unknown> {\n const token = config.siteClientToken;\n return {\n [ENV_KEYS.apiUrl]: config.apiUrl || 'MISSING',\n [ENV_KEYS.siteClientToken]: token ? `${token.slice(0, 12)}${'*'.repeat(20)}` : 'MISSING',\n [ENV_KEYS.workspaceSlug]: config.workspaceSlug || 'MISSING',\n hasAll: config.configured,\n };\n}\n","/**\n * Reading Marvin payloads.\n *\n * The published API returns the same logical value in several shapes depending on which\n * endpoint produced it (list item vs full read, SDK `Entry` wrapper vs raw JSON, placement\n * metadata vs asset metadata). Every reader here is total: it takes `unknown`, checks, and\n * returns `undefined` rather than throwing.\n */\n\nimport type { AssetSelectOptions, MarvinContentEntry } from './types.js';\n\n/** A non-empty string, or `undefined`. Empty/whitespace strings are treated as absent. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};\n}\n\nexport function asNumber(value: unknown): number | undefined {\n if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\nexport function asStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n\n const items = value.filter(\n (item): item is string => typeof item === 'string' && item.trim().length > 0\n );\n return items.length > 0 ? items : undefined;\n}\n\n/** `metadata_json` — the free-form blob every entry carries, whatever its schema. */\nexport function entryMetadata(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { metadata?: unknown; metadataJson?: unknown };\n return asRecord(value.metadata ?? value.metadataJson);\n}\n\n/** `data_json` — the schema-defined fields. Absent on list items until hydrated. */\nexport function entryData(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { data?: unknown; dataJson?: unknown };\n return asRecord(value.data ?? value.dataJson);\n}\n\n/**\n * Read a schema field (`data_json`) from an entry, through the SDK's `Entry` accessor when\n * present and off the raw payload otherwise.\n */\nexport function entryField<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const accessor = (entry as { field?: (key: string) => unknown }).field;\n if (typeof accessor === 'function') {\n return accessor.call(entry, key) as T | undefined;\n }\n return entryData(entry)[key] as T | undefined;\n}\n\n/**\n * The precedence rule: structured schema field first (`data_json`), legacy blob second\n * (`metadata_json`).\n *\n * An empty string counts as absent, which matters more than it sounds — an entry type that\n * declares a field the author left blank stores `\"\"`, and without this the legacy value that\n * *is* set would never surface.\n */\nexport function field<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const value = entryField(entry, key);\n if (value !== undefined && value !== null && value !== '') return value as T;\n const fallback = entryMetadata(entry)[key];\n return (fallback === '' ? undefined : fallback) as T | undefined;\n}\n\n/**\n * Read a value off a published asset placement, checking every level the API might have put it\n * at: the placement itself, its metadata blobs, then the underlying asset and its blobs.\n */\nexport function assetField<T = unknown>(asset: unknown, key: string): T | undefined {\n if (!asset || typeof asset !== 'object') return undefined;\n\n const value = asset as Record<string, unknown>;\n const nested = asRecord(value.asset);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.placementMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/** Same layered read as {@link assetField}, for resource relationships. */\nexport function resourceField<T = unknown>(resource: unknown, key: string): T | undefined {\n if (!resource || typeof resource !== 'object') return undefined;\n\n const value = resource as Record<string, unknown>;\n const nested = asRecord(value.resource);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/**\n * The entry's collection memberships as slugs. Collections arrive as bare slugs on list items,\n * flat `{slug}` objects, or the SDK's nested `{collection: {slug}}` — all three are read here.\n */\nexport function collectionSlugs(entry: MarvinContentEntry): string[] {\n const direct = (entry as { collectionSlugs?: unknown }).collectionSlugs;\n if (Array.isArray(direct)) {\n return direct.filter((slug): slug is string => typeof slug === 'string');\n }\n\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return [];\n\n return raw\n .map((item) =>\n typeof item === 'string' ? item : (asRecord(asRecord(item).collection).slug ?? asRecord(item).slug)\n )\n .filter((slug): slug is string => typeof slug === 'string');\n}\n\n/**\n * The entry's membership role within one specific collection (e.g. `'legal'` in\n * `footer-navigation`). A hydrated entry carries all of its memberships; this finds the one for\n * the given collection.\n */\nexport function collectionRole(\n entry: MarvinContentEntry,\n collectionSlug: string\n): string | undefined {\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return undefined;\n\n for (const item of raw) {\n if (typeof item === 'string') continue;\n const record = asRecord(item);\n const slug = asString(record.slug) ?? asString(asRecord(record.collection).slug);\n if (slug === collectionSlug) {\n return asString(asRecord(record.entryMetadata).role) ?? asString(record.role);\n }\n }\n return undefined;\n}\n\n/**\n * A CSS `object-position` value from a focal point. Marvin stores these either pre-formatted\n * (`\"40% 60%\"`) or as `{x, y}` in 0–1 or 0–100 units.\n */\nexport function focalPoint(value: unknown): string | undefined {\n const direct = asString(value);\n if (direct) return direct;\n\n const point = asRecord(value);\n const x = typeof point.x === 'number' ? point.x : undefined;\n const y = typeof point.y === 'number' ? point.y : undefined;\n if (x == null || y == null) return undefined;\n\n return `${x > 1 ? x : x * 100}% ${y > 1 ? y : y * 100}%`;\n}\n\nexport function entryAssets(entry: MarvinContentEntry): Record<string, unknown>[] {\n const assets = (entry as { assets?: unknown }).assets;\n return Array.isArray(assets) ? (assets as Record<string, unknown>[]) : [];\n}\n\nexport function entryResources(entry: MarvinContentEntry): Record<string, unknown>[] {\n const resources = (entry as { resources?: unknown }).resources;\n return Array.isArray(resources) ? (resources as Record<string, unknown>[]) : [];\n}\n\nfunction includesPreferred(actual: string | undefined, exact?: string, any?: string[]): boolean {\n if (!exact && !any?.length) return true;\n if (!actual) return false;\n return actual === exact || Boolean(any?.includes(actual));\n}\n\nfunction assetMatches(asset: Record<string, unknown>, options: AssetSelectOptions): boolean {\n const role = asString(assetField(asset, 'role'));\n const usage = asString(assetField(asset, 'usage'));\n const type = asString(assetField(asset, 'assetType'));\n const mimeType = asString(assetField(asset, 'mimeType'));\n\n const hasRoleCriteria = Boolean(options.role || options.roles?.length);\n const hasUsageCriteria = Boolean(options.usage || options.usages?.length);\n const roleMatches = includesPreferred(role, options.role, options.roles);\n const usageMatches = includesPreferred(usage, options.usage, options.usages);\n const typeMatches = includesPreferred(type, options.type, options.types);\n const mimeMatches = options.mimeType ? mimeType === options.mimeType : true;\n const unroledAllowed = Boolean(options.allowUnroled) && !role && !usage;\n\n // Role and usage are alternatives, not a conjunction: an asset placed with `role: hero` and\n // one placed with `usage: hero` both satisfy a hero request.\n const relationshipMatches =\n hasRoleCriteria || hasUsageCriteria ? roleMatches || usageMatches || unroledAllowed : true;\n\n return relationshipMatches && typeMatches && mimeMatches;\n}\n\nexport function selectEntryAsset(\n entry: MarvinContentEntry,\n options: AssetSelectOptions = {}\n): Record<string, unknown> | undefined {\n return entryAssets(entry).find((asset) => assetMatches(asset, options));\n}\n\n/**\n * Match on role ALONE, exactly. `selectEntryAsset` ORs role against a vacuously-true usage\n * check, so `{roles: ['hero-grade']}` there matches the entry's first asset whatever its role.\n * When you mean \"the asset whose role is exactly this\", use this.\n */\nexport function selectAssetByRole(\n entry: MarvinContentEntry,\n ...roles: string[]\n): Record<string, unknown> | undefined {\n for (const role of roles) {\n const found = entryAssets(entry).find((asset) => asString(assetField(asset, 'role')) === role);\n if (found) return found;\n }\n return undefined;\n}\n\n/** The list-item shorthand: `featuredAsset` is present on collection entries. */\nexport function selectFeaturedAsset(\n entry: MarvinContentEntry\n): Record<string, unknown> | undefined {\n const featured = asRecord((entry as { featuredAsset?: unknown }).featuredAsset);\n return Object.keys(featured).length > 0 ? featured : undefined;\n}\n\nexport function selectImageAsset(\n entry: MarvinContentEntry,\n options: Omit<AssetSelectOptions, 'type' | 'types'> = {}\n): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, { ...options, type: 'image' }) ??\n selectEntryAsset(entry, { ...options, types: ['image'], allowUnroled: true })\n );\n}\n\nexport function selectIconAsset(entry: MarvinContentEntry): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n mimeType: 'image/svg+xml',\n allowUnroled: true,\n }) ??\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n allowUnroled: true,\n })\n );\n}\n\nexport function assetUrl(asset: unknown): string | undefined {\n return asString(assetField(asset, 'publicUrl'));\n}\n\nexport function assetAlt(asset: unknown, fallback?: string): string | undefined {\n return asString(assetField(asset, 'altText')) ?? fallback;\n}\n\nexport function resourceRole(resource: unknown): string | undefined {\n return asString(resourceField(resource, 'role'));\n}\n\nexport function isExternalHref(href: string): boolean {\n return /^(https?:|mailto:|tel:)/.test(href);\n}\n","/**\n * Site chrome: main nav, footer columns, legal strip, social links, inquiry link.\n *\n * All of it comes from ordinary Marvin collections, so a workspace can reorder its own\n * navigation without a deploy. Every piece has a static fallback for when the backend is down.\n */\n\nimport type { MarvinFetcher, HydratedEntry } from './fetch.js';\nimport {\n asRecord,\n asString,\n collectionRole,\n collectionSlugs,\n entryField,\n isExternalHref,\n} from './normalize.js';\nimport type { SiteLoader } from './site.js';\nimport type { 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/**\n * `collections.entries` as SDK 4.2+ declares it: `{ expand: 'full' }` asks the server for full\n * entries (`?expand=full`) and returns them as SDK `Entry` objects. Older SDKs take only the slug\n * and ignore the option, so the same call works with any SDK in the peer range; their list items\n * are then hydrated one by one.\n */\ntype ExpandingCollectionEntries = (\n slug: string,\n options?: { expand?: 'full' }\n) => Promise<MarvinContentEntry[]>;\n\n/**\n * True for a full entry (`PublishedEntryRead`), false for a list item (`PublishedEntryListItem`).\n *\n * `data` doesn't tell them apart: list items carry it too now. What differs is attachments. A full\n * read has `assets[]` and `resources[]` (placement objects, always present, possibly empty); a list\n * item has only `assetSlugs` / `resourceSlugs` and a `featuredAsset`. The SDK's `Entry` wrapper\n * only ever comes from the single-entry read.\n */\nexport function isFullEntry(item: unknown): boolean {\n if (!item || typeof item !== 'object') return false;\n if (typeof (item as { field?: unknown }).field === 'function') return true;\n const raw = item as Record<string, unknown>;\n if ('assetSlugs' in raw || 'resourceSlugs' in raw) return false;\n return Array.isArray(raw.assets) || Array.isArray(raw.resources);\n}\n\n/** A failed hydration read is retried this many times in total, backing off from this base. */\nexport const HYDRATE_ATTEMPTS = 3;\nexport const HYDRATE_BACKOFF_MS = 500;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `Promise.all(items.map(run))` with at most `limit` calls in flight. Order is preserved. */\nasync function mapWithConcurrency<T, R>(\n items: T[],\n limit: number,\n run: (item: T) => Promise<R>\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n\n async function worker(): Promise<void> {\n while (next < items.length) {\n const index = next++;\n results[index] = await run(items[index]);\n }\n }\n\n const workers = Array.from({ length: Math.min(limit, items.length) }, worker);\n await Promise.all(workers);\n return results;\n}\n\nexport function createFetcher(backend: MarvinBackend) {\n async function guarded<T>(label: string, empty: T, run: () => Promise<T>): Promise<T> {\n if (!backend.hasBackend()) return empty;\n try {\n return await run();\n } catch (error) {\n backend.remember(error);\n backend.warn(`${label} unavailable: ${errorMessage(error)}`);\n return empty;\n }\n }\n\n async function entry(slug: string): Promise<Entry | null> {\n return guarded(`Entry \"${slug}\"`, null, () => backend.client().entry(slug));\n }\n\n /**\n * One entry read that survives a transient failure.\n *\n * `entry()` can't be used here: `guarded` swallows the error and trips the latch, and a\n * tripped latch turns every remaining item into a silent null. So retry with backoff,\n * re-opening the latch first in case a sibling read tripped it, and only remember the\n * failure — and warn — once the attempts are spent.\n */\n async function entryForHydration(slug: string): Promise<Entry | null> {\n if (!backend.hasBackend()) return null;\n\n let failure: unknown;\n for (let attempt = 1; attempt <= HYDRATE_ATTEMPTS; attempt++) {\n if (attempt > 1) {\n await sleep(HYDRATE_BACKOFF_MS * 2 ** (attempt - 2));\n if (backend.isLatched()) backend.clearLatch();\n }\n try {\n return await backend.client().entry(slug);\n } catch (error) {\n failure = error;\n }\n }\n\n backend.remember(failure);\n backend.warn(\n `Entry \"${slug}\" dropped after ${HYDRATE_ATTEMPTS} attempts: ${errorMessage(failure)}`\n );\n return null;\n }\n\n /**\n * Turn list items into full entries.\n *\n * The collection endpoint returns `PublishedEntryListItem`: core fields, `metadata_json` and\n * (on current servers) `data`, but only asset/resource *slugs* — no `assets[]` placements (so\n * no roles: no `hero-grade` image) and no `resources[]`. `PublishedEntryRead` (the single-entry\n * endpoint) has all of it. Only items that are already full entries ({@link isFullEntry}) are\n * passed through; everything else is re-read, even when it carries `data`. A list fetched with\n * `expand=full` ({@link expandedCollectionEntries}) is all full entries, so nothing is re-read.\n *\n * Reads run at most `config.hydrateConcurrency` at a time. Unbounded, a 280-entry collection\n * fired 280 requests at once, most of which timed out and were dropped — and the first\n * timeout latched the backend off for the rest of the build.\n */\n /**\n * A collection's entries as full entries in one request, where the server and SDK support\n * `expand=full` (Marvin core with expand, `@inneropen/marvin-sdk` 4.2+). Otherwise this is\n * `collectionEntries`: list items, which {@link hydrate} reads one at a time.\n */\n function expandedCollectionEntries(slug: string): Promise<MarvinContentEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () => {\n const collections = backend.client().collections;\n const entries = collections.entries as ExpandingCollectionEntries;\n return entries.call(collections, slug, { expand: 'full' });\n });\n }\n\n async function hydrate(entries: MarvinContentEntry[]): Promise<HydratedEntry[]> {\n const hydrated = await mapWithConcurrency(\n entries,\n backend.config.hydrateConcurrency,\n async (item) => {\n if (isFullEntry(item)) return item as HydratedEntry;\n return entryForHydration(item.slug ?? '');\n }\n );\n\n return hydrated.filter((item): item is HydratedEntry => Boolean(item));\n }\n\n return {\n backend,\n\n collections(): Promise<PublishedCollectionSummary[]> {\n return guarded('Collections', [], () => backend.client().collections.list());\n },\n\n async collection(slug: string): Promise<Collection | null> {\n return guarded(`Collection \"${slug}\"`, null, async () => {\n const found = await backend.client().collections.get(slug);\n return Array.isArray(found) ? null : found;\n });\n },\n\n collectionEntries(slug: string): Promise<CollectionEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () =>\n backend.client().collections.entries(slug)\n );\n },\n\n /** Try each collection slug in order; return the first that yields entries. */\n async collectionEntriesFallback(slugs: string[]): Promise<CollectionEntry[]> {\n for (const slug of slugs) {\n const entries = await this.collectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n expandedCollectionEntries,\n\n /** Full entries: one `expand=full` request, or per-item reads where that isn't supported. */\n async hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]> {\n return hydrate(await expandedCollectionEntries(slug));\n },\n\n async hydratedCollectionEntriesFallback(slugs: string[]): Promise<HydratedEntry[]> {\n for (const slug of slugs) {\n const entries = await this.hydratedCollectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n hydrate,\n entry,\n\n site(): Promise<MarvinSite | null> {\n return guarded('Site', null, () => backend.client().getSite());\n },\n\n workspace(): Promise<Workspace | null> {\n return guarded('Workspace', null, () => backend.client().getWorkspace());\n },\n\n assets(type?: string): Promise<MarvinAsset[]> {\n return guarded('Assets', [], () => backend.client().assets.list({ type }));\n },\n\n asset(slugOrId: string): Promise<MarvinAsset | null> {\n return guarded(`Asset \"${slugOrId}\"`, null, () => backend.client().assets.get(slugOrId));\n },\n\n resources(resourceType?: string): Promise<MarvinResource[]> {\n return guarded('Resources', [], () => backend.client().resources.list({ resourceType }));\n },\n };\n}\n","import { Marked, type MarkedOptions, type Tokens } from 'marked';\nimport { findEmbed, type MarvinEmbeds } from './embeds.js';\n\nexport type MarkdownOptions = MarkedOptions;\n\n/** Per-call options for a {@link MarkdownRenderer}. */\nexport type RenderMarkdownOptions = {\n /**\n * The entry's `embeds` map. A paragraph that is exactly a URL with an entry here renders as\n * that embed's Marvin-built HTML instead of a link.\n */\n embeds?: MarvinEmbeds | null;\n};\n\nexport type MarkdownRenderer = (\n markdown: string | string[] | null | undefined,\n options?: RenderMarkdownOptions\n) => Promise<string>;\n\n// Per-call data rides on marked's parse options: marked merges them over the instance defaults and\n// hands the merged object to the renderer as `this.options` for that parse only. A symbol key\n// keeps it clear of marked's own option names.\nconst EMBEDS = Symbol('marvin.embeds');\ntype ParseOptionsWithEmbeds = MarkedOptions & { [EMBEDS]?: MarvinEmbeds | null };\n\n// One run of non-space characters starting with a scheme. `<url>`, `[text](url)` and a URL inside\n// a sentence all fail this, so they stay links.\nconst BARE_URL = /^https?:\\/\\/\\S+$/i;\n\n/**\n * The embed for a paragraph that is exactly a URL with an `embeds` entry, else `undefined`.\n * Exported for tests; the renderer below is the only caller.\n */\nexport function paragraphEmbedHtml(\n token: Pick<Tokens.Paragraph, 'text'>,\n embeds: MarvinEmbeds | null | undefined\n): string | undefined {\n if (!embeds) return undefined;\n const text = token.text.trim();\n if (!BARE_URL.test(text)) return undefined;\n const html = findEmbed(embeds, text)?.html;\n return typeof html === 'string' && html.trim() ? html : undefined;\n}\n\n/**\n * A markdown renderer with its own `marked` instance — options are per-site, not global, so two\n * `createMarvinContent()` instances in one process can't clobber each other's settings.\n *\n * `breaks: false` is the default because CMS body copy is authored as prose: a soft-wrapped\n * paragraph should render as one paragraph. Where authored line breaks must survive (a landing\n * intro, an address block), pass `breaks: true` or pre-convert with a trailing double space.\n *\n * Pass the entry's `embeds` to turn a bare provider URL on its own line into its player:\n *\n * ```ts\n * await renderMarkdown(body, { embeds: entryEmbeds(entry) });\n * ```\n */\nexport function createMarkdownRenderer(options: MarkdownOptions = {}): MarkdownRenderer {\n const marked = new Marked();\n marked.setOptions({ gfm: true, breaks: false, ...options });\n marked.use({\n renderer: {\n paragraph(token) {\n const embeds = (this.options as ParseOptionsWithEmbeds)[EMBEDS];\n const html = paragraphEmbedHtml(token, embeds);\n // `false` hands the token back to marked's default paragraph renderer.\n return html === undefined ? false : `${html}\\n`;\n },\n },\n });\n\n return async (markdown, renderOptions = {}) => {\n const source = Array.isArray(markdown) ? markdown.join('\\n\\n') : (markdown ?? '');\n if (!source) return '';\n const parseOptions: ParseOptionsWithEmbeds = { [EMBEDS]: renderOptions.embeds ?? null };\n return await marked.parse(source, parseOptions);\n };\n}\n\n/** Turn single newlines into hard breaks, leaving paragraph breaks alone. */\nexport function preserveSoftBreaks(source: string): string {\n return source.replace(/(?<!\\n)\\n(?!\\n)/g, ' \\n');\n}\n","/**\n * Media embeds: the published shapes Marvin hands a site, read tolerantly.\n *\n * Marvin resolves a pasted provider link (YouTube, Vimeo, Spotify, …) on the server and publishes\n * the result next to the entry as `entry.embeds`, keyed by the URL exactly as the author wrote it.\n * Stored markdown is never rewritten: a site that knows about embeds swaps the link for the\n * player; an older site just shows the link.\n *\n * These types are declared here rather than imported from `@inneropen/marvin-sdk` so the package\n * works with SDKs that predate embeds. Kind and status are open-ended so a newer server value\n * doesn't break an older site's types.\n */\n\nimport { asRecord, asString } from './normalize.js';\n\nexport type MarvinEmbedKind = 'video' | 'audio' | 'podcast' | 'playlist';\nexport type MarvinEmbedStatus = 'ok' | 'link' | 'unavailable';\nexport type MarvinEmbedMode = 'direct' | 'click_to_load';\n\n/**\n * A Marvin-built iframe. Every value comes from Marvin's provider registry. Exactly one of\n * `aspectRatio` / `height` is set; the other is `null`.\n */\nexport type MarvinEmbedIframe = {\n src: string;\n title: string;\n allow?: string | null;\n sandbox?: string | null;\n referrerpolicy?: string | null;\n /** CSS aspect ratio for video players, e.g. `\"16/9\"` (`\"9/16\"` for Shorts); `null` otherwise. */\n aspectRatio?: string | null;\n /** Fixed pixel height for audio / podcast / playlist players; `null` for video. */\n height?: number | null;\n};\n\n/** The plain link every embed falls back to. */\nexport type MarvinEmbedLink = {\n href: string;\n title: string;\n providerName: string;\n};\n\n/** Optional fields arrive as `null` rather than omitted; every reader here treats both alike. */\nexport type MarvinEmbed = {\n /** The URL exactly as written in the content. */\n url: string;\n canonicalUrl?: string | null;\n /** Provider key, e.g. `youtube`. */\n provider: string;\n /** Display name, e.g. `YouTube`. */\n providerName: string;\n kind: MarvinEmbedKind | (string & {});\n /** `ok` has an iframe; `link` and `unavailable` render as a link card. */\n status: MarvinEmbedStatus | (string & {});\n title?: string | null;\n authorName?: string | null;\n thumbnailUrl?: string | null;\n iframe?: MarvinEmbedIframe | null;\n link: MarvinEmbedLink;\n /** Marvin-built HTML for the site's embed mode (an iframe, a click-to-load facade, or a link). */\n html: string;\n};\n\n/** `entry.embeds`: every resolved embed on the entry, keyed by the URL as written. */\nexport type MarvinEmbeds = Record<string, MarvinEmbed>;\n\n/**\n * `site.embeds` (next to `site.seo`): the site's embed privacy mode, consent text (`{provider}`\n * already substituted per embed in the published `html`) and the frame sources a CSP must allow.\n */\nexport type MarvinSiteEmbeds = {\n mode: MarvinEmbedMode;\n consentText: string;\n /** Origins/hosts the site's embeds may frame — feed these into a `frame-src` directive. */\n frameSources: string[];\n};\n\nconst EMBED_MODES = new Set<string>(['direct', 'click_to_load']);\n\nfunction readEmbedsField(entry: unknown): unknown {\n if (!entry || typeof entry !== 'object') return undefined;\n const direct = (entry as { embeds?: unknown }).embeds;\n if (direct !== undefined) return direct;\n // The SDK's `Entry` class keeps the payload private; its `toJSON()` hands it back.\n const toJSON = (entry as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === 'function') {\n return asRecord(toJSON.call(entry)).embeds;\n }\n return undefined;\n}\n\n/**\n * The entry's `embeds` map, or `undefined` when it has none (an older server, or no embeds).\n * Values that aren't objects carrying a URL are dropped.\n */\nexport function entryEmbeds(entry: unknown): MarvinEmbeds | undefined {\n const raw = readEmbedsField(entry);\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;\n\n const embeds: MarvinEmbeds = {};\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n embeds[key] = value as MarvinEmbed;\n }\n }\n return Object.keys(embeds).length > 0 ? embeds : undefined;\n}\n\n/** The embed for `url` as written, tolerating surrounding whitespace. */\nexport function findEmbed(\n embeds: MarvinEmbeds | null | undefined,\n url: string | null | undefined\n): MarvinEmbed | undefined {\n if (!embeds || !url) return undefined;\n const key = url.trim();\n if (!key) return undefined;\n return Object.prototype.hasOwnProperty.call(embeds, key) ? embeds[key] : undefined;\n}\n\n/** Normalize `site.embeds`; `undefined` when the server doesn't send it. */\nexport function siteEmbeds(raw: unknown): MarvinSiteEmbeds | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const value = asRecord(raw);\n const mode = asString(value.mode);\n const frameSources = Array.isArray(value.frameSources)\n ? value.frameSources.filter(\n (source): source is string => typeof source === 'string' && source.trim().length > 0\n )\n : [];\n\n return {\n // Unknown or missing mode → the privacy-preserving default.\n mode: mode && EMBED_MODES.has(mode) ? (mode as MarvinEmbedMode) : 'click_to_load',\n consentText: asString(value.consentText) ?? '',\n frameSources,\n };\n}\n","const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\n/**\n * Format a date value for display as \"Mon DD, YYYY\".\n *\n * ISO dates/timestamps (e.g. a raw `publishedAt` like \"2026-07-21T21:22:42.045708Z\") are read in\n * UTC — via the date part only, so there's no off-by-one day shift from the viewer's timezone.\n * Values that are already human-readable (e.g. a hand-entered \"Jul 02, 2024\") pass through\n * unchanged, and empty input returns `undefined` so callers can omit the field.\n */\nexport function formatDisplayDate(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(trimmed);\n if (iso) {\n const [, year, month, day] = iso;\n const name = MONTHS[Number(month) - 1];\n if (name) return `${name} ${day}, ${year}`;\n }\n\n return trimmed;\n}\n\n/**\n * Pick `count` items deterministically from `items`, seeded by `pageSlug`.\n *\n * Same page → same selection on every build, different pages → different selections. Use for\n * rotating pull-quotes, value bands, related links: variety across the site without the\n * hydration mismatch a random pick would cause.\n */\nexport function selectValuesForPage<T>(items: T[], pageSlug: string, count = 4): T[] {\n const seeded = [...items];\n let seed = 0;\n\n for (const character of pageSlug) {\n seed = (seed * 31 + character.charCodeAt(0)) >>> 0;\n }\n\n for (let index = seeded.length - 1; index > 0; index -= 1) {\n seed = (seed * 1664525 + 1013904223) >>> 0;\n\n const target = seed % (index + 1);\n\n [seeded[index], seeded[target]] = [seeded[target], seeded[index]];\n }\n\n return seeded.slice(0, count);\n}\n","/**\n * The field accessor handed to a repository's `transform`.\n *\n * Every reader defaults to the documented precedence — `data_json` first, `metadata_json`\n * second — so a transform reads like a field list instead of a pile of coalescing operators.\n */\n\nimport { entryEmbeds, findEmbed, type MarvinEmbed } from './embeds.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport { preserveSoftBreaks } from './markdown.js';\nimport {\n asNumber,\n asRecord,\n asString,\n asStringArray,\n assetAlt,\n assetField,\n assetUrl,\n collectionRole,\n collectionSlugs,\n entryAssets,\n entryData,\n entryMetadata,\n entryResources,\n field,\n focalPoint,\n resourceField,\n selectAssetByRole,\n selectFeaturedAsset,\n selectIconAsset,\n selectImageAsset,\n} from './normalize.js';\nimport { formatDisplayDate } from './format.js';\nimport type {\n ApiImage,\n ApiResourceLink,\n AssetSelectOptions,\n MarvinContentEntry,\n} from './types.js';\n\nexport type ImageFieldOptions = Omit<AssetSelectOptions, 'type' | 'types'> & {\n /**\n * Exact roles to prefer, in order, before the looser role/usage match. Use for derived\n * variants — e.g. `['hero-grade']` to prefer a colour-graded hero over the raw upload.\n */\n preferRoles?: string[];\n /** `metadata_json` key holding a hand-authored `{src, alt, focalPoint}`. */\n metadataKey?: string | false;\n /** Alt text when the asset carries none. Defaults to the entry title. */\n alt?: string;\n /** Fall back to the list item's `featuredAsset`. Default `true`. */\n fallbackToFeatured?: boolean;\n};\n\nexport type IconFieldOptions = {\n roles?: string[];\n /** Fall back to the list item's `featuredAsset` when no icon asset is attached. */\n fallbackToFeatured?: boolean;\n};\n\nexport type MarkdownFieldOptions = {\n /** Keep authored single newlines as hard breaks. Default `false`. */\n softBreaks?: boolean;\n /**\n * Render a bare provider URL on its own line as its player, using the entry's `embeds`.\n * Default `true`; `false` keeps every URL a plain link.\n */\n embeds?: boolean;\n};\n\nexport type ResourceFieldOptions = {\n /** Keep only these resource types, e.g. `['material', 'construction']`. */\n types?: string[];\n /** Keep only resources attached with this role. */\n role?: string;\n /** Build an href from the resource slug. */\n href?: (slug: string) => string;\n};\n\nconst DEFAULT_IMAGE_ROLES = ['hero', 'featured', 'card'];\n\nexport type FieldAccessor = ReturnType<typeof createFieldAccessor>;\n\nexport type FieldAccessorContext = {\n renderMarkdown: MarkdownRenderer;\n /** The route this entry resolves to, if the repository was given an `href` builder. */\n href?: string;\n /** The collection the entry was loaded from, when it came from one. */\n collection?: string;\n};\n\nexport function createFieldAccessor(\n entry: MarvinContentEntry,\n context: FieldAccessorContext\n) {\n const title = asString((entry as { title?: unknown }).title);\n const embeds = entryEmbeds(entry);\n\n const accessor = {\n /** The entry itself, for anything the accessor doesn't cover. */\n entry,\n /** The route this entry resolves to (from the repository's `href` option). */\n href: context.href,\n collection: context.collection,\n\n /** `data_json` → `metadata_json`, untyped. */\n raw(key: string): unknown {\n return field(entry, key);\n },\n\n string(key: string): string | undefined {\n return asString(field(entry, key));\n },\n\n number(key: string): number | undefined {\n return asNumber(field(entry, key));\n },\n\n /** Booleans authored as strings (`\"true\"`, `\"1\"`, `\"yes\"`) read as booleans. */\n bool(key: string): boolean {\n const value = field(entry, key);\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n return ['true', '1', 'yes', 'on'].includes(value.trim().toLowerCase());\n }\n if (typeof value === 'number') return value !== 0;\n return Boolean(value);\n },\n\n /** A string array, tolerating a single string authored in place of a list. */\n list(key: string): string[] | undefined {\n const value = field(entry, key);\n const array = asStringArray(value);\n if (array) return array;\n const single = asString(value);\n return single ? [single] : undefined;\n },\n\n /**\n * An enum-guarded read: the value if it's in `allowed`, else `fallback`. Replaces the\n * per-field `normalizeStatus`/`normalizeCategory`/`normalizeTone` guards that every site\n * ends up writing.\n */\n oneOf<T extends string>(key: string, allowed: readonly T[] | Set<T>, fallback: T): T {\n const value = asString(field(entry, key));\n const set = allowed instanceof Set ? allowed : new Set<string>(allowed);\n return value && set.has(value as T) ? (value as T) : fallback;\n },\n\n /** A field read as a display date (\"Mon DD, YYYY\"); already-formatted values pass through. */\n date(key: string): string | undefined {\n return formatDisplayDate(asString(field(entry, key)));\n },\n\n /** The raw ISO publish timestamp, if any. */\n publishedAt(): string | undefined {\n const value = (entry as { publishedAt?: unknown }).publishedAt;\n return value != null ? String(value) : undefined;\n },\n\n /**\n * Render a markdown field to HTML. Falls back to the entry's `contentMarkdown` when the\n * named field is absent. Returns `undefined` when there is nothing to render, so the\n * caller can omit the property rather than emit an empty string.\n */\n async markdown(\n key = 'body',\n options: MarkdownFieldOptions = {}\n ): Promise<string | undefined> {\n const raw =\n field<string | string[]>(entry, key) ??\n (entry as { contentMarkdown?: string | string[] }).contentMarkdown;\n const source = Array.isArray(raw) ? raw.join('\\n\\n') : asString(raw);\n if (!source) return undefined;\n return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source, {\n embeds: options.embeds === false ? undefined : embeds,\n });\n },\n\n /**\n * The resolved embed for an `embed` field (its value is the provider URL), or `undefined`\n * when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from\n * `@inneropen/marvin-astro/components`, or output `embed.html`.\n */\n embed(key: string): MarvinEmbed | undefined {\n return findEmbed(embeds, asString(field(entry, key)));\n },\n\n /**\n * The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,\n * the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the\n * list item's `featuredAsset`.\n */\n image(options: ImageFieldOptions = {}): ApiImage | undefined {\n const {\n preferRoles,\n metadataKey = 'featuredImage',\n alt: altFallback,\n fallbackToFeatured = true,\n roles = DEFAULT_IMAGE_ROLES,\n usages = DEFAULT_IMAGE_ROLES,\n allowUnroled = true,\n ...rest\n } = options;\n const fallbackAlt = altFallback ?? title ?? '';\n\n if (metadataKey) {\n const authored = asRecord(entryMetadata(entry)[metadataKey]);\n const src = asString(authored.src);\n if (src) {\n return {\n src,\n alt: asString(authored.alt) ?? fallbackAlt,\n focalPoint: focalPoint(authored.focalPoint),\n };\n }\n }\n\n const asset =\n (preferRoles?.length ? selectAssetByRole(entry, ...preferRoles) : undefined) ??\n selectImageAsset(entry, { ...rest, roles, usages, allowUnroled }) ??\n (fallbackToFeatured ? selectFeaturedAsset(entry) : undefined);\n\n const src = assetUrl(asset);\n if (!src) return undefined;\n\n return {\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n };\n },\n\n /** URL of the entry's icon asset (`role: icon`, SVG preferred). */\n icon(options: IconFieldOptions = {}): string | undefined {\n const { roles = ['icon'], fallbackToFeatured = false } = options;\n const asset = selectAssetByRole(entry, ...roles) ?? selectIconAsset(entry);\n return (\n assetUrl(asset) ?? (fallbackToFeatured ? assetUrl(selectFeaturedAsset(entry)) : undefined)\n );\n },\n\n /** The raw asset placement matching `options` — when you need more than src/alt/focal. */\n asset(options: AssetSelectOptions = {}): Record<string, unknown> | undefined {\n return selectImageAsset(entry, options) ?? selectFeaturedAsset(entry);\n },\n\n /** The asset whose role is EXACTLY one of `roles`, in preference order. */\n assetByRole(...roles: string[]): Record<string, unknown> | undefined {\n return selectAssetByRole(entry, ...roles);\n },\n\n /** All asset placements on the entry. */\n assets(): Record<string, unknown>[] {\n return entryAssets(entry);\n },\n\n /** Every image asset resolved, filtered by role/usage — support/detail galleries. */\n images(options: ImageFieldOptions = {}): (ApiImage & { usage?: string })[] {\n const wanted = new Set([...(options.roles ?? []), ...(options.usages ?? [])]);\n const fallbackAlt = options.alt ?? title ?? '';\n const images: (ApiImage & { usage?: string })[] = [];\n\n for (const asset of entryAssets(entry)) {\n const usage = asString(assetField(asset, 'usage')) ?? asString(assetField(asset, 'role'));\n const src = assetUrl(asset);\n if (!src) continue;\n if (wanted.size > 0 && (!usage || !wanted.has(usage))) continue;\n\n images.push({\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n usage,\n });\n }\n\n return images;\n },\n\n /** Attached resources, normalized to name/type/role/href. */\n resources(options: ResourceFieldOptions = {}): ApiResourceLink[] {\n const types = options.types?.length ? new Set(options.types) : undefined;\n\n return entryResources(entry)\n .map((relationship) => {\n const nested = asRecord(relationship.resource);\n const source = Object.keys(nested).length > 0 ? nested : relationship;\n const slug = asString(resourceField(source, 'slug'));\n return {\n name: asString(resourceField(source, 'name')) ?? '',\n type: asString(resourceField(source, 'resourceType')) ?? '',\n role: asString(assetField(relationship, 'role')),\n slug,\n href: slug && options.href ? options.href(slug) : undefined,\n };\n })\n .filter((resource) => Boolean(resource.name))\n .filter((resource) => !types || types.has(resource.type))\n .filter((resource) => !options.role || resource.role === options.role);\n },\n\n /** The first attached resource matching `options`. */\n resource(options: ResourceFieldOptions = {}): ApiResourceLink | undefined {\n return accessor.resources(options)[0];\n },\n\n /** The `metadata_json` blob. */\n metadata(): Record<string, unknown> {\n return entryMetadata(entry);\n },\n\n /** The `data_json` blob. Empty on a list item that was not hydrated. */\n data(): Record<string, unknown> {\n return entryData(entry);\n },\n\n /** Slugs of every collection the entry belongs to. */\n collections(): string[] {\n return collectionSlugs(entry);\n },\n\n /** The entry's membership role within `collectionSlug`. */\n role(collectionSlug: string): string | undefined {\n return collectionRole(entry, collectionSlug);\n },\n };\n\n return accessor;\n}\n","/**\n * The repository factory.\n *\n * `getAll` / `getBySlug` / `getFeatured` is the same forty lines of try / fallback / memoize\n * around every content type on every site. Only the transform differs. This holds the forty\n * lines; the site writes the transform.\n */\n\nimport { errorMessage } from './client.js';\nimport { createFieldAccessor, type FieldAccessor } from './fields.js';\nimport { isFullEntry, type MarvinFetcher } from './fetch.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport type { MarvinContentEntry } from './types.js';\n\nexport type RepositoryOptions<T> = {\n /** Collection slug to load from. */\n collection?: string;\n /**\n * Collection slugs tried in order; the first one that yields entries wins. Lets a site work\n * against workspaces that named the same thing `bench-notes`, `journal`, or `blog`.\n */\n collections?: string[];\n /**\n * Fetch each list item as a full entry before transforming.\n *\n * The collection endpoint returns `PublishedEntryListItem`, which has no `assets[]` or\n * `resources[]` (only their slugs) and, on older servers, no `data_json`; the single-entry\n * endpoint returns `PublishedEntryRead`, which has all of it. So if the transform reads asset\n * roles, resources, or (on an older server) any schema field, this must be `true`. The list is\n * requested with `expand=full`, which returns full entries in the one request; a server or SDK\n * without it (SDK < 4.2) costs one more request per entry. `bySlug` always works from full\n * entries either way.\n */\n hydrate?: boolean;\n /** Build the resolved item from an entry. May be async (e.g. to render markdown). */\n transform: (entry: MarvinContentEntry, fields: FieldAccessor) => T | Promise<T>;\n /** Static data to serve when Marvin is unreachable or the collection is empty. */\n fallback?: () => T[] | Promise<T[]>;\n /** Applied to Marvin and fallback results alike, so ordering doesn't depend on the source. */\n sort?: (a: T, b: T) => number;\n /** Applied to Marvin and fallback results alike. */\n filter?: (item: T) => boolean;\n /** The route an entry resolves to; exposed to the transform as `fields.href`. */\n href?: (slug: string, entry: MarvinContentEntry) => string;\n /** How to read an item's slug for `bySlug`. Defaults to `item.slug`. */\n slugOf?: (item: T) => string | undefined;\n /** How to tell whether an item is featured. Defaults to `item.featured`. */\n isFeatured?: (item: T) => boolean;\n};\n\nexport type Repository<T> = {\n /** Every item, resolved once per process. */\n all(): Promise<T[]>;\n /**\n * One item by slug. Served from `all()` once that has loaded; otherwise a direct entry\n * fetch, falling back to a scan of `all()`.\n */\n bySlug(slug: string): Promise<T | undefined>;\n /** The first featured item, or the first item when none is marked. */\n featured(): Promise<T | undefined>;\n /** Every featured item, in `all()` order. */\n allFeatured(): Promise<T[]>;\n /** Drop the memoized results. Next call re-fetches. */\n reset(): void;\n};\n\nexport type RepositoryContext = {\n fetcher: MarvinFetcher;\n renderMarkdown: MarkdownRenderer;\n};\n\nfunction defaultSlugOf<T>(item: T): string | undefined {\n const slug = (item as { slug?: unknown }).slug;\n return typeof slug === 'string' ? slug : undefined;\n}\n\nfunction defaultIsFeatured<T>(item: T): boolean {\n return Boolean((item as { featured?: unknown }).featured);\n}\n\nexport function createRepository<T>(\n context: RepositoryContext,\n options: RepositoryOptions<T>\n): Repository<T> {\n const { fetcher, renderMarkdown } = context;\n const collections = options.collections ?? (options.collection ? [options.collection] : []);\n const slugOf = options.slugOf ?? defaultSlugOf;\n const isFeatured = options.isFeatured ?? defaultIsFeatured;\n\n /** The loaded list, plus the slugs whose items were built from full entries. */\n type Loaded = { items: T[]; fromFullEntry: Set<string> };\n let loadedPromise: Promise<Loaded> | null = null;\n let allPromise: Promise<T[]> | null = null;\n const bySlugCache = new Map<string, Promise<T | undefined>>();\n\n function arrange(items: T[]): T[] {\n const filtered = options.filter ? items.filter(options.filter) : items;\n return options.sort ? [...filtered].sort(options.sort) : filtered;\n }\n\n function transform(entry: MarvinContentEntry, collection?: string): Promise<T> {\n const slug = (entry as { slug?: string }).slug ?? '';\n const fields = createFieldAccessor(entry, {\n renderMarkdown,\n href: options.href?.(slug, entry),\n collection,\n });\n return Promise.resolve(options.transform(entry, fields));\n }\n\n async function useFallback(): Promise<Loaded> {\n return { items: arrange((await options.fallback?.()) ?? []), fromFullEntry: new Set() };\n }\n\n async function load(): Promise<Loaded> {\n if (collections.length > 0 && fetcher.backend.hasBackend()) {\n try {\n const entries = options.hydrate\n ? await fetcher.hydratedCollectionEntriesFallback(collections)\n : await fetcher.collectionEntriesFallback(collections);\n\n if (entries.length > 0) {\n const items = await Promise.all(entries.map((entry) => transform(entry)));\n const fromFullEntry = new Set<string>();\n items.forEach((item, index) => {\n const slug = slugOf(item);\n if (slug !== undefined && isFullEntry(entries[index])) fromFullEntry.add(slug);\n });\n return { items: arrange(items), fromFullEntry };\n }\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(\n `Collection \"${collections[0]}\" failed, using fallback: ${errorMessage(error)}`\n );\n }\n }\n\n return useFallback();\n }\n\n function findInList(items: T[], slug: string): T | undefined {\n return items.find((item) => slugOf(item) === slug);\n }\n\n async function loadBySlug(slug: string): Promise<T | undefined> {\n // Once a hydrated list has loaded, every slug in it is already transformed from a full entry —\n // a per-entry fetch would be a second request for the same content (~1.4s per detail page on\n // a 280-entry site). An item built from a bare list item is NOT served: it has no assets[] or\n // resources[], so a detail page would lose its role-picked images and resource links.\n let listed: T | undefined;\n if (loadedPromise) {\n const loaded = await loadedPromise;\n listed = findInList(loaded.items, slug);\n if (listed && loaded.fromFullEntry.has(slug)) return listed;\n }\n\n if (fetcher.backend.hasBackend()) {\n try {\n const entry = await fetcher.entry(slug);\n if (entry) return await transform(entry);\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(`Entry \"${slug}\" failed, using fallback: ${errorMessage(error)}`);\n }\n }\n\n // No backend, no such entry, or a transform that blew up: fall back to the resolved list,\n // which is either Marvin's or the static data — the caller doesn't need to care which.\n return listed ?? findInList(await all(), slug);\n }\n\n function all(): Promise<T[]> {\n if (!allPromise) {\n loadedPromise = load();\n allPromise = loadedPromise.then((loaded) => loaded.items);\n }\n return allPromise;\n }\n\n return {\n all,\n\n bySlug(slug: string): Promise<T | undefined> {\n let cached = bySlugCache.get(slug);\n if (!cached) {\n cached = loadBySlug(slug);\n bySlugCache.set(slug, cached);\n }\n return cached;\n },\n\n async allFeatured(): Promise<T[]> {\n return (await all()).filter(isFeatured);\n },\n\n async featured(): Promise<T | undefined> {\n const items = await all();\n return items.find(isFeatured) ?? items[0];\n },\n\n reset() {\n allPromise = null;\n loadedPromise = null;\n bySlugCache.clear();\n },\n };\n}\n","import { entryEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { createMarkdownRenderer, preserveSoftBreaks, type MarkdownRenderer } from './markdown.js';\nimport { asString, assetUrl, entryField, selectAssetByRole } from './normalize.js';\nimport type { SectionLanding } from './types.js';\n\nexport type SectionLandingOptions = {\n /** Asset role to use as the hero image. Default `'hero'`. */\n heroRole?: string;\n /** Field holding the intro copy. Default `'body'`, falling back to the entry summary. */\n bodyField?: string;\n};\n\n/**\n * Resolve a section-landing header from a Marvin `page` entry with the given slug.\n *\n * The intro is rendered with authored line breaks preserved — landing copy is written as a few\n * deliberate lines, not a soft-wrapped paragraph, and losing those breaks changes the layout.\n */\nexport async function loadSectionLanding(\n fetcher: MarvinFetcher,\n slug: string,\n renderMarkdown: MarkdownRenderer = createMarkdownRenderer(),\n options: SectionLandingOptions = {}\n): Promise<SectionLanding> {\n const entry = await fetcher.entry(slug);\n if (!entry) return {};\n\n const source =\n asString(entryField(entry, options.bodyField ?? 'body')) ?? asString(entry.summary);\n\n return {\n title: asString(entry.title),\n introHtml: source\n ? await renderMarkdown(preserveSoftBreaks(source), { embeds: entryEmbeds(entry) })\n : undefined,\n hero: assetUrl(selectAssetByRole(entry, options.heroRole ?? 'hero')),\n };\n}\n","/**\n * Site identity + SEO.\n *\n * Marvin's site configuration is optional almost everywhere; a template that had to guard every\n * field would be unreadable. This resolves it once into an `ApiSite` where title, description,\n * siteName, locale and timezone are always present, and where brand asset slugs have already\n * become URLs.\n */\n\nimport type { MarvinSite, SiteConfiguration } from '@inneropen/marvin-sdk';\nimport { siteEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { asRecord, asString, assetUrl } from './normalize.js';\nimport type { ApiSeo, ApiSite } from './types.js';\n\nexport type SiteOptions = {\n /**\n * Static identity to fall back to, field by field, when Marvin is unreachable or leaves a\n * value unset. Anything omitted here falls through to a neutral default.\n */\n fallback?: Partial<ApiSite> | (() => Partial<ApiSite>);\n /** Default locale when neither Marvin nor the fallback sets one. Default `'en-US'`. */\n defaultLocale?: string;\n /** Default timezone when neither Marvin nor the fallback sets one. Default `'UTC'`. */\n defaultTimezone?: string;\n};\n\n/**\n * Build resolved SEO from Marvin's raw SiteSeo block, applying identity fallbacks so title,\n * description and siteName are never empty. Absent → sensible defaults (robots `index,follow`;\n * og type `website`; large-image Twitter card).\n */\nexport function buildSeo(\n raw: Record<string, unknown> | undefined,\n identity: { title: string; description: string; canonicalUrl?: string; siteName: string }\n): ApiSeo {\n const seo = raw ?? {};\n const verification = asRecord(seo.verification);\n\n return {\n title: asString(seo.title) ?? identity.title,\n titleTemplate: asString(seo.titleTemplate),\n description: asString(seo.description) ?? identity.description,\n keywords: Array.isArray(seo.keywords)\n ? seo.keywords.filter((keyword): keyword is string => typeof keyword === 'string')\n : [],\n robots: asString(seo.robots) ?? 'index,follow',\n image: asString(seo.image),\n imageAlt: asString(seo.imageAlt),\n ogType: asString(seo.ogType) ?? 'website',\n siteName: asString(seo.siteName) ?? identity.siteName,\n twitterCard: asString(seo.twitterCard) ?? 'summary_large_image',\n twitterHandle: asString(seo.twitterHandle),\n twitterCreator: asString(seo.twitterCreator),\n fbAppId: asString(seo.fbAppId),\n themeColor: asString(seo.themeColor),\n publisher: asString(seo.publisher),\n canonicalUrl: identity.canonicalUrl,\n verification: {\n google: asString(verification.google),\n bing: asString(verification.bing),\n pinterest: asString(verification.pinterest),\n yandex: asString(verification.yandex),\n },\n };\n}\n\nfunction normalizeSocial(social: unknown): Record<string, string> {\n return Object.fromEntries(\n Object.entries(asRecord(social)).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0\n )\n );\n}\n\n/** The offline site: the caller's fallback, with neutral defaults under it. */\nexport function buildFallbackSite(options: SiteOptions = {}): ApiSite {\n const provided = typeof options.fallback === 'function' ? options.fallback() : options.fallback;\n const fallback = provided ?? {};\n const title = fallback.title ?? fallback.name ?? 'Untitled site';\n const description = fallback.description ?? '';\n\n return {\n ...fallback,\n name: fallback.name ?? title,\n title,\n description,\n locale: fallback.locale ?? options.defaultLocale ?? 'en-US',\n timezone: fallback.timezone ?? options.defaultTimezone ?? 'UTC',\n social: fallback.social ?? {},\n metadata: fallback.metadata ?? {},\n seo:\n fallback.seo ??\n buildSeo(undefined, { title, description, siteName: fallback.name ?? title }),\n };\n}\n\nfunction transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite {\n const config: SiteConfiguration = marvinSite.site;\n const metadata = asRecord(config.metadataJson);\n const title =\n asString(config.title) ?? asString(marvinSite.workspace?.name) ?? fallback.title;\n const description = asString(config.description) ?? fallback.description;\n\n return {\n workspaceSlug: asString(marvinSite.workspace?.slug),\n workspaceName: asString(marvinSite.workspace?.name),\n name: title,\n title,\n tagline: asString(config.tagline) ?? fallback.tagline,\n description,\n canonicalUrl: asString(config.canonicalUrl) ?? fallback.canonicalUrl,\n logo: asString(config.logo) ?? fallback.logo,\n favicon: asString(config.favicon) ?? fallback.favicon,\n locale: asString(config.locale) ?? fallback.locale,\n timezone: asString(config.timezone) ?? fallback.timezone,\n email: asString(config.contactEmail) ?? fallback.email,\n imprint: asString(metadata.imprint) ?? fallback.imprint,\n social: { ...fallback.social, ...normalizeSocial(config.social) },\n // Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older\n // SDKs drop the typed field but pass the blob through untouched, and workspaces configured\n // before the typed field existed still only have the blob.\n seo: buildSeo(asRecord((config as { seo?: unknown }).seo ?? metadata.seo), {\n title,\n description,\n canonicalUrl: asString(config.canonicalUrl),\n siteName: title,\n }),\n // Older servers and SDKs don't send it; `siteEmbeds` returns undefined then.\n embeds: siteEmbeds(\n (config as { embeds?: unknown }).embeds ?? (marvinSite as { embeds?: unknown }).embeds\n ),\n metadata,\n };\n}\n\nfunction looksLikeAssetSlug(value: string): boolean {\n return !/^https?:\\/\\//.test(value) && !value.startsWith('/') && !value.startsWith('data:');\n}\n\nexport type SiteLoader = {\n get(): Promise<ApiSite>;\n reset(): void;\n};\n\nexport function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions = {}): SiteLoader {\n const fallback = buildFallbackSite(options);\n let promise: Promise<ApiSite> | null = null;\n\n async function resolveAssetSlug(slug: string | undefined): Promise<string | undefined> {\n if (!slug) return undefined;\n return assetUrl(await fetcher.asset(slug));\n }\n\n // Resolve a value only when it looks like a bare asset slug; a URL/path/data URI (or a slug with\n // no matching asset) is returned unchanged.\n async function resolveMaybeSlug(value: string | undefined): Promise<string | undefined> {\n if (!value || !looksLikeAssetSlug(value)) return value;\n return (await resolveAssetSlug(value)) ?? value;\n }\n\n async function load(): Promise<ApiSite> {\n const marvinSite = await fetcher.site();\n if (!marvinSite) return fallback;\n\n const site = transformMarvinSite(marvinSite, fallback);\n\n // Brand-asset registry: resolve EVERY `brand.<name>` slug → URL in one pass. A site adds a\n // shared asset with one config line (`brand.paperFold = \"paper-fold\"`) plus an upload;\n // components then read `site.brand.paperFold`. No code change per asset.\n const brandPairs = Object.entries(asRecord(site.metadata.brand)).filter(\n (entry): entry is [string, string] =>\n typeof entry[1] === 'string' && entry[1].trim().length > 0\n );\n const resolved = await Promise.all(\n brandPairs.map(async ([name, slug]) => [name, await resolveAssetSlug(slug)] as const)\n );\n const brand: Record<string, string> = {};\n const brandBySlug = new Map<string, string>();\n for (const [index, [name, url]] of resolved.entries()) {\n if (!url) continue;\n brand[name] = url;\n brandBySlug.set(brandPairs[index][1], url);\n }\n\n // The og:image may be given as a bare asset slug too. It is usually one of the brand slugs\n // (a monogram, a logo), so check what was just resolved before fetching it again.\n const rawImage = site.seo.image;\n const ogImage =\n rawImage && looksLikeAssetSlug(rawImage)\n ? (brandBySlug.get(rawImage) ?? (await resolveAssetSlug(rawImage)))\n : undefined;\n\n // The top-level `site_logo` / `site_favicon` fields may be bare asset slugs too (like brand.*\n // and og:image) — resolve them so they can reference an uploaded asset by slug. The brand-map\n // aliases still win when present.\n const logo = brand.logo ?? (await resolveMaybeSlug(site.logo));\n const favicon = brand.favicon ?? (await resolveMaybeSlug(site.favicon));\n\n return {\n ...site,\n brand,\n // Convenience aliases for common callers; all of these also live in `site.brand`.\n logo,\n favicon,\n seal: brand.seal ?? site.seal,\n seo: { ...site.seo, image: ogImage ?? site.seo.image },\n };\n }\n\n return {\n get() {\n // Memoized: components like a per-card brand stamp render many instances per page, so\n // this keeps brand-slug resolution to once per process rather than once per render.\n promise ??= load();\n return promise;\n },\n reset() {\n promise = null;\n },\n };\n}\n","/**\n * `@inneropen/marvin-astro` — site integration for Marvin CMS.\n *\n * Sits above `@inneropen/marvin-sdk` (transport). This layer is the part every Marvin-backed site\n * rewrites: repositories with a static fallback, site chrome from collections, and payload\n * normalization. Astro components (forms, media embeds, entry-type renderers) live under\n * `@inneropen/marvin-astro/components`.\n *\n * ```ts\n * // src/lib/content.ts\n * export const marvin = createMarvinContent({ site: { fallback: staticSite } });\n *\n * export const posts = marvin.repository({\n * collections: ['bench-notes', 'journal', 'blog'],\n * hydrate: true,\n * href: (slug) => `/bench-notes/${slug}`,\n * fallback: () => staticPosts,\n * transform: async (entry, f) => ({\n * slug: entry.slug,\n * title: entry.title ?? 'Untitled',\n * noteNumber: f.string('noteNumber'),\n * bodyHtml: await f.markdown('body'),\n * image: f.image({ roles: ['hero', 'featured', 'card'] }),\n * }),\n * });\n * ```\n */\n\nimport { createBackend } from './client.js';\nimport { createChromeLoader, type ChromeOptions } from './chrome.js';\nimport { createFetcher } from './fetch.js';\nimport { createMarkdownRenderer } from './markdown.js';\nimport { createRepository, type Repository, type RepositoryOptions } from './repository.js';\nimport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nimport { createSiteLoader, type SiteOptions } from './site.js';\nimport type { MarvinAstroConfig } from './config.js';\nimport type { ApiSite, ApiSiteChrome, SectionLanding } from './types.js';\n\nexport type MarvinContentOptions = MarvinAstroConfig & {\n site?: SiteOptions;\n chrome?: ChromeOptions;\n};\n\nexport type MarvinContent = ReturnType<typeof createMarvinContent>;\n\n/**\n * Build the content layer for one Marvin workspace.\n *\n * Connection settings come from `MARVIN_API_URL`, `MARVIN_SITE_CLIENT_TOKEN` and\n * `MARVIN_WORKSPACE_SLUG` unless passed explicitly. Everything returned degrades to its static\n * fallback when the backend is unreachable, so a site still builds offline.\n */\nexport function createMarvinContent(options: MarvinContentOptions = {}) {\n const backend = createBackend(options);\n const fetcher = createFetcher(backend);\n const renderMarkdown = createMarkdownRenderer(backend.config.markdown);\n const siteLoader = createSiteLoader(fetcher, options.site);\n const chromeLoader = createChromeLoader(fetcher, siteLoader, options.chrome);\n const repositories: Repository<unknown>[] = [];\n\n return {\n /** The resolved configuration, with the token still in place — don't log it. */\n config: backend.config,\n /** Latch control and the raw SDK client. */\n backend,\n /** Guarded, never-throwing wrappers around every published endpoint. */\n fetch: fetcher,\n renderMarkdown,\n\n /** True when Marvin is configured and not currently latched out as unreachable. */\n hasBackend: () => backend.hasBackend(),\n\n /** Resolved site identity, SEO and brand assets. Memoized per process. */\n getSite(): Promise<ApiSite> {\n return siteLoader.get();\n },\n\n /** Navigation, footer, legal, social and inquiry links. Memoized per process. */\n getSiteChrome(): Promise<ApiSiteChrome> {\n return chromeLoader.get();\n },\n\n /** Title/intro/hero for an index page, driven by a `page` entry of the same slug. */\n getSectionLanding(slug: string, sectionOptions?: SectionLandingOptions): Promise<SectionLanding> {\n return loadSectionLanding(fetcher, slug, renderMarkdown, sectionOptions);\n },\n\n /** A collection-backed content repository: `all()` / `bySlug()` / `featured()`. */\n repository<T>(repositoryOptions: RepositoryOptions<T>): Repository<T> {\n const repository = createRepository<T>({ fetcher, renderMarkdown }, repositoryOptions);\n repositories.push(repository as Repository<unknown>);\n return repository;\n },\n\n /** Drop every memoized result and re-open the failure latch. */\n reset() {\n backend.clearLatch();\n siteLoader.reset();\n chromeLoader.reset();\n for (const repository of repositories) repository.reset();\n },\n };\n}\n\nexport {\n createBackend,\n errorMessage,\n isNetworkFailure,\n type MarvinBackend,\n} from './client.js';\nexport {\n DEFAULT_DEV_RETRY_MS,\n DEFAULT_HYDRATE_CONCURRENCY,\n ENV_KEYS,\n describeConfig,\n readEnv,\n resolveConfig,\n type MarvinAstroConfig,\n type MarvinLogger,\n type ResolvedConfig,\n} from './config.js';\nexport { createFetcher, isFullEntry, type HydratedEntry, type MarvinFetcher } from './fetch.js';\nexport {\n createFieldAccessor,\n type FieldAccessor,\n type IconFieldOptions,\n type ImageFieldOptions,\n type MarkdownFieldOptions,\n type ResourceFieldOptions,\n} from './fields.js';\nexport { formatDisplayDate, selectValuesForPage } from './format.js';\nexport {\n createMarkdownRenderer,\n preserveSoftBreaks,\n type MarkdownOptions,\n type MarkdownRenderer,\n type RenderMarkdownOptions,\n} from './markdown.js';\nexport { entryEmbeds, findEmbed, siteEmbeds } from './embeds.js';\nexport * from './normalize.js';\nexport {\n createRepository,\n type Repository,\n type RepositoryContext,\n type RepositoryOptions,\n} from './repository.js';\nexport {\n buildFallbackSite,\n buildSeo,\n createSiteLoader,\n type SiteLoader,\n type SiteOptions,\n} from './site.js';\nexport {\n createChromeLoader,\n defaultResolveHref,\n metadataLink,\n socialLinkFromKey,\n toNavigationLink,\n type ChromeLoader,\n type ChromeOptions,\n type HrefContext,\n type NavigationContext,\n type NavigationLinkInput,\n type ResolveHref,\n type SocialLinkInput,\n} from './chrome.js';\nexport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nexport type * from './types.js';\n"],"mappings":"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","names":["src"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/config.ts","../src/normalize.ts","../src/chrome.ts","../src/fetch.ts","../src/markdown.ts","../src/embeds.ts","../src/format.ts","../src/fields.ts","../src/repository.ts","../src/sections.ts","../src/site.ts","../src/index.ts"],"sourcesContent":["/**\n * SDK client construction and the network-failure latch.\n *\n * The latch exists because a static build asks for content once per path. When the backend is\n * down, that is N failed requests with N timeouts before the build gives up. The latch trips on\n * the first network failure and short-circuits the rest.\n *\n * Unlike the hand-rolled original it is *retryable*: it expires after `retryAfterMs` so a dev\n * server recovers on its own when the backend comes back, instead of serving stale static data\n * until someone restarts it. Production keeps the permanent latch — a build should fail fast and\n * consistently rather than half-succeed.\n */\n\nimport { createMarvinClient } from '@inneropen/marvin-sdk';\nimport type { MarvinClient } from '@inneropen/marvin-sdk';\nimport {\n describeConfig,\n resolveConfig,\n type MarvinAstroConfig,\n type ResolvedConfig,\n} from './config.js';\n\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\n/**\n * A network failure means \"the backend is unreachable\", not \"this request was bad\". Only the\n * former should trip the latch — a 404 on one entry says nothing about the next one.\n */\nexport function isNetworkFailure(error: unknown): boolean {\n const message = errorMessage(error).toLowerCase();\n return (\n message.includes('network error') ||\n message.includes('fetch failed') ||\n message.includes('econnrefused') ||\n message.includes('enotfound') ||\n message.includes('etimedout')\n );\n}\n\nexport type MarvinBackend = {\n readonly config: ResolvedConfig;\n /** True when Marvin is configured AND not currently latched out. */\n hasBackend(): boolean;\n /** The SDK client. Throws when Marvin is not configured. */\n client(): MarvinClient;\n /** Trip the latch if this error indicates the backend is unreachable. */\n remember(error: unknown): void;\n /** True while the latch is closed (backend considered unreachable). */\n isLatched(): boolean;\n /** Manually re-open the latch, e.g. after a deploy. */\n clearLatch(): void;\n warn(message: string): void;\n};\n\nexport function createBackend(options: MarvinAstroConfig = {}): MarvinBackend {\n const config = resolveConfig(options);\n let client: MarvinClient | null = null;\n let latchedAt: number | null = null;\n let envLogged = false;\n\n function isLatched(): boolean {\n if (latchedAt === null) return false;\n if (config.retryAfterMs === Number.POSITIVE_INFINITY) return true;\n if (config.now() - latchedAt < config.retryAfterMs) return true;\n\n // Expired — re-open and let the next call find out whether the backend is back.\n latchedAt = null;\n return false;\n }\n\n function hasBackend(): boolean {\n if (config.debug && !envLogged) {\n envLogged = true;\n config.logger.log('[marvin-astro] env check:', describeConfig(config));\n }\n if (!config.configured) return false;\n return !isLatched();\n }\n\n return {\n config,\n hasBackend,\n isLatched,\n clearLatch() {\n latchedAt = null;\n },\n remember(error: unknown) {\n if (isNetworkFailure(error)) latchedAt = config.now();\n },\n warn(message: string) {\n config.logger.warn(`[marvin-astro] ${message}`);\n },\n client(): MarvinClient {\n if (!config.configured) {\n throw new Error(\n '[marvin-astro] Marvin is not configured. Set MARVIN_API_URL, ' +\n 'MARVIN_SITE_CLIENT_TOKEN and MARVIN_WORKSPACE_SLUG, or pass them to createMarvinContent().'\n );\n }\n client ??= (config.createClient\n ? config.createClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })\n : createMarvinClient({\n apiUrl: config.apiUrl,\n siteClientToken: config.siteClientToken,\n workspaceSlug: config.workspaceSlug,\n debug: config.debug,\n })) as MarvinClient;\n return client;\n },\n };\n}\n","/**\n * Configuration + environment resolution.\n *\n * Precedence for every value: explicit option → `import.meta.env` → `process.env`.\n * Astro exposes server-side env through `import.meta.env`; a plain Node script\n * (a smoke test, a build hook) only has `process.env`. Both work, unchanged.\n */\n\nimport type { MarkdownOptions } from './markdown.js';\n\nexport type MarvinLogger = {\n log: (message: string, ...args: unknown[]) => void;\n warn: (message: string, ...args: unknown[]) => void;\n error: (message: string, ...args: unknown[]) => void;\n};\n\nexport const ENV_KEYS = {\n apiUrl: 'MARVIN_API_URL',\n siteClientToken: 'MARVIN_SITE_CLIENT_TOKEN',\n workspaceSlug: 'MARVIN_WORKSPACE_SLUG',\n debug: 'MARVIN_DEBUG',\n} as const;\n\n/** Dev default for the network-failure latch: retry a downed backend after 30s. */\nexport const DEFAULT_DEV_RETRY_MS = 30_000;\n\n/** How many entry reads `hydrate` keeps in flight at once. */\nexport const DEFAULT_HYDRATE_CONCURRENCY = 6;\n\nexport type MarvinAstroConfig = {\n apiUrl?: string;\n siteClientToken?: string;\n workspaceSlug?: string;\n /** Log the resolved env once at startup, and enable SDK request logging. */\n debug?: boolean;\n /**\n * How long the network-failure latch stays closed before the backend is retried.\n * Defaults to 30s in dev and `Infinity` in production — a build should fail fast and\n * consistently rather than half-succeed with some pages live and some pages static.\n */\n retryAfterMs?: number;\n /**\n * Maximum entry reads in flight at once while hydrating a collection. Firing every read at\n * once makes a large collection time out on itself; the default of 6 keeps a 280-entry\n * hydration well inside the SDK's per-request timeout.\n */\n hydrateConcurrency?: number;\n logger?: MarvinLogger;\n /** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */\n markdown?: MarkdownOptions;\n /** Explicit env source. Highest priority after direct options — mainly a test seam. */\n env?: Record<string, string | undefined>;\n /** Injectable clock, so latch expiry is testable without waiting. */\n now?: () => number;\n /** Injectable SDK client factory — a test seam; defaults to the SDK's `createMarvinClient`. */\n createClient?: (config: MarvinClientOptions) => unknown;\n};\n\nexport type MarvinClientOptions = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n};\n\nexport type ResolvedConfig = {\n apiUrl: string;\n siteClientToken: string;\n workspaceSlug: string;\n debug: boolean;\n retryAfterMs: number;\n hydrateConcurrency: number;\n logger: MarvinLogger;\n markdown?: MarkdownOptions;\n now: () => number;\n createClient?: (config: MarvinClientOptions) => unknown;\n /** True when all three connection values are present. */\n configured: boolean;\n};\n\nfunction metaEnv(): Record<string, unknown> | undefined {\n try {\n return (import.meta as unknown as { env?: Record<string, unknown> }).env;\n } catch {\n return undefined;\n }\n}\n\nfunction processEnv(): Record<string, string | undefined> | undefined {\n try {\n return typeof process !== 'undefined' ? process.env : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction nonEmpty(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\n/** Read one env var, honouring the documented precedence. */\nexport function readEnv(\n key: string,\n override?: Record<string, string | undefined>\n): string | undefined {\n return nonEmpty(override?.[key]) ?? nonEmpty(metaEnv()?.[key]) ?? nonEmpty(processEnv()?.[key]);\n}\n\nfunction isProduction(override?: Record<string, string | undefined>): boolean {\n const prodFlag = metaEnv()?.PROD;\n if (typeof prodFlag === 'boolean') return prodFlag;\n const mode = readEnv('NODE_ENV', override) ?? readEnv('MODE', override);\n return mode === 'production';\n}\n\nexport function resolveConfig(options: MarvinAstroConfig = {}): ResolvedConfig {\n const env = options.env;\n const apiUrl = options.apiUrl ?? readEnv(ENV_KEYS.apiUrl, env) ?? '';\n const siteClientToken = options.siteClientToken ?? readEnv(ENV_KEYS.siteClientToken, env) ?? '';\n const workspaceSlug = options.workspaceSlug ?? readEnv(ENV_KEYS.workspaceSlug, env) ?? '';\n const debug = options.debug ?? readEnv(ENV_KEYS.debug, env) === 'true';\n\n return {\n apiUrl,\n siteClientToken,\n workspaceSlug,\n debug,\n retryAfterMs:\n options.retryAfterMs ??\n (isProduction(env) ? Number.POSITIVE_INFINITY : DEFAULT_DEV_RETRY_MS),\n hydrateConcurrency: Math.max(1, Math.floor(options.hydrateConcurrency ?? DEFAULT_HYDRATE_CONCURRENCY)),\n logger: options.logger ?? console,\n markdown: options.markdown,\n now: options.now ?? (() => Date.now()),\n createClient: options.createClient,\n configured: Boolean(apiUrl && siteClientToken && workspaceSlug),\n };\n}\n\n/** One-line summary of the resolved env, with the token masked. For `debug` logging. */\nexport function describeConfig(config: ResolvedConfig): Record<string, unknown> {\n const token = config.siteClientToken;\n return {\n [ENV_KEYS.apiUrl]: config.apiUrl || 'MISSING',\n [ENV_KEYS.siteClientToken]: token ? `${token.slice(0, 12)}${'*'.repeat(20)}` : 'MISSING',\n [ENV_KEYS.workspaceSlug]: config.workspaceSlug || 'MISSING',\n hasAll: config.configured,\n };\n}\n","/**\n * Reading Marvin payloads.\n *\n * The published API returns the same logical value in several shapes depending on which\n * endpoint produced it (list item vs full read, SDK `Entry` wrapper vs raw JSON, placement\n * metadata vs asset metadata). Every reader here is total: it takes `unknown`, checks, and\n * returns `undefined` rather than throwing.\n */\n\nimport type { AssetSelectOptions, MarvinContentEntry } from './types.js';\n\n/** A non-empty string, or `undefined`. Empty/whitespace strings are treated as absent. */\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined;\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};\n}\n\nexport function asNumber(value: unknown): number | undefined {\n if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;\n if (typeof value === 'string' && value.trim().length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\n\nexport function asStringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n\n const items = value.filter(\n (item): item is string => typeof item === 'string' && item.trim().length > 0\n );\n return items.length > 0 ? items : undefined;\n}\n\n/** `metadata_json` — the free-form blob every entry carries, whatever its schema. */\nexport function entryMetadata(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { metadata?: unknown; metadataJson?: unknown };\n return asRecord(value.metadata ?? value.metadataJson);\n}\n\n/** `data_json` — the schema-defined fields. Absent on list items until hydrated. */\nexport function entryData(entry: MarvinContentEntry): Record<string, unknown> {\n const value = entry as { data?: unknown; dataJson?: unknown };\n return asRecord(value.data ?? value.dataJson);\n}\n\n/**\n * Read a schema field (`data_json`) from an entry, through the SDK's `Entry` accessor when\n * present and off the raw payload otherwise.\n */\nexport function entryField<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const accessor = (entry as { field?: (key: string) => unknown }).field;\n if (typeof accessor === 'function') {\n return accessor.call(entry, key) as T | undefined;\n }\n return entryData(entry)[key] as T | undefined;\n}\n\n/**\n * The precedence rule: structured schema field first (`data_json`), legacy blob second\n * (`metadata_json`).\n *\n * An empty string counts as absent, which matters more than it sounds — an entry type that\n * declares a field the author left blank stores `\"\"`, and without this the legacy value that\n * *is* set would never surface.\n */\nexport function field<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined {\n const value = entryField(entry, key);\n if (value !== undefined && value !== null && value !== '') return value as T;\n const fallback = entryMetadata(entry)[key];\n return (fallback === '' ? undefined : fallback) as T | undefined;\n}\n\n/**\n * Read a value off a published asset placement, checking every level the API might have put it\n * at: the placement itself, its metadata blobs, then the underlying asset and its blobs.\n */\nexport function assetField<T = unknown>(asset: unknown, key: string): T | undefined {\n if (!asset || typeof asset !== 'object') return undefined;\n\n const value = asset as Record<string, unknown>;\n const nested = asRecord(value.asset);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.placementMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/** Same layered read as {@link assetField}, for resource relationships. */\nexport function resourceField<T = unknown>(resource: unknown, key: string): T | undefined {\n if (!resource || typeof resource !== 'object') return undefined;\n\n const value = resource as Record<string, unknown>;\n const nested = asRecord(value.resource);\n return (value[key] ??\n asRecord(value.entryMetadata)[key] ??\n asRecord(value.metadataJson)[key] ??\n asRecord(value.metadata_)[key] ??\n asRecord(value.metadata)[key] ??\n nested[key] ??\n asRecord(nested.metadataJson)[key] ??\n asRecord(nested.metadata_)[key] ??\n asRecord(nested.metadata)[key]) as T | undefined;\n}\n\n/**\n * The entry's collection memberships as slugs. Collections arrive as bare slugs on list items,\n * flat `{slug}` objects, or the SDK's nested `{collection: {slug}}` — all three are read here.\n */\nexport function collectionSlugs(entry: MarvinContentEntry): string[] {\n const direct = (entry as { collectionSlugs?: unknown }).collectionSlugs;\n if (Array.isArray(direct)) {\n return direct.filter((slug): slug is string => typeof slug === 'string');\n }\n\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return [];\n\n return raw\n .map((item) =>\n typeof item === 'string' ? item : (asRecord(asRecord(item).collection).slug ?? asRecord(item).slug)\n )\n .filter((slug): slug is string => typeof slug === 'string');\n}\n\n/**\n * The entry's membership role within one specific collection (e.g. `'legal'` in\n * `footer-navigation`). A hydrated entry carries all of its memberships; this finds the one for\n * the given collection.\n */\nexport function collectionRole(\n entry: MarvinContentEntry,\n collectionSlug: string\n): string | undefined {\n const raw = (entry as { collections?: unknown }).collections;\n if (!Array.isArray(raw)) return undefined;\n\n for (const item of raw) {\n if (typeof item === 'string') continue;\n const record = asRecord(item);\n const slug = asString(record.slug) ?? asString(asRecord(record.collection).slug);\n if (slug === collectionSlug) {\n return asString(asRecord(record.entryMetadata).role) ?? asString(record.role);\n }\n }\n return undefined;\n}\n\n/**\n * A CSS `object-position` value from a focal point. Marvin stores these either pre-formatted\n * (`\"40% 60%\"`) or as `{x, y}` in 0–1 or 0–100 units.\n */\nexport function focalPoint(value: unknown): string | undefined {\n const direct = asString(value);\n if (direct) return direct;\n\n const point = asRecord(value);\n const x = typeof point.x === 'number' ? point.x : undefined;\n const y = typeof point.y === 'number' ? point.y : undefined;\n if (x == null || y == null) return undefined;\n\n return `${x > 1 ? x : x * 100}% ${y > 1 ? y : y * 100}%`;\n}\n\nexport function entryAssets(entry: MarvinContentEntry): Record<string, unknown>[] {\n const assets = (entry as { assets?: unknown }).assets;\n return Array.isArray(assets) ? (assets as Record<string, unknown>[]) : [];\n}\n\nexport function entryResources(entry: MarvinContentEntry): Record<string, unknown>[] {\n const resources = (entry as { resources?: unknown }).resources;\n return Array.isArray(resources) ? (resources as Record<string, unknown>[]) : [];\n}\n\nfunction includesPreferred(actual: string | undefined, exact?: string, any?: string[]): boolean {\n if (!exact && !any?.length) return true;\n if (!actual) return false;\n return actual === exact || Boolean(any?.includes(actual));\n}\n\nfunction assetMatches(asset: Record<string, unknown>, options: AssetSelectOptions): boolean {\n const role = asString(assetField(asset, 'role'));\n const usage = asString(assetField(asset, 'usage'));\n const type = asString(assetField(asset, 'assetType'));\n const mimeType = asString(assetField(asset, 'mimeType'));\n\n const hasRoleCriteria = Boolean(options.role || options.roles?.length);\n const hasUsageCriteria = Boolean(options.usage || options.usages?.length);\n const roleMatches = includesPreferred(role, options.role, options.roles);\n const usageMatches = includesPreferred(usage, options.usage, options.usages);\n const typeMatches = includesPreferred(type, options.type, options.types);\n const mimeMatches = options.mimeType ? mimeType === options.mimeType : true;\n const unroledAllowed = Boolean(options.allowUnroled) && !role && !usage;\n\n // Role and usage are alternatives, not a conjunction: an asset placed with `role: hero` and\n // one placed with `usage: hero` both satisfy a hero request.\n const relationshipMatches =\n hasRoleCriteria || hasUsageCriteria ? roleMatches || usageMatches || unroledAllowed : true;\n\n return relationshipMatches && typeMatches && mimeMatches;\n}\n\nexport function selectEntryAsset(\n entry: MarvinContentEntry,\n options: AssetSelectOptions = {}\n): Record<string, unknown> | undefined {\n return entryAssets(entry).find((asset) => assetMatches(asset, options));\n}\n\n/**\n * Match on role ALONE, exactly. `selectEntryAsset` ORs role against a vacuously-true usage\n * check, so `{roles: ['hero-grade']}` there matches the entry's first asset whatever its role.\n * When you mean \"the asset whose role is exactly this\", use this.\n */\nexport function selectAssetByRole(\n entry: MarvinContentEntry,\n ...roles: string[]\n): Record<string, unknown> | undefined {\n for (const role of roles) {\n const found = entryAssets(entry).find((asset) => asString(assetField(asset, 'role')) === role);\n if (found) return found;\n }\n return undefined;\n}\n\n/** The list-item shorthand: `featuredAsset` is present on collection entries. */\nexport function selectFeaturedAsset(\n entry: MarvinContentEntry\n): Record<string, unknown> | undefined {\n const featured = asRecord((entry as { featuredAsset?: unknown }).featuredAsset);\n return Object.keys(featured).length > 0 ? featured : undefined;\n}\n\nexport function selectImageAsset(\n entry: MarvinContentEntry,\n options: Omit<AssetSelectOptions, 'type' | 'types'> = {}\n): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, { ...options, type: 'image' }) ??\n selectEntryAsset(entry, { ...options, types: ['image'], allowUnroled: true })\n );\n}\n\nexport function selectIconAsset(entry: MarvinContentEntry): Record<string, unknown> | undefined {\n return (\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n mimeType: 'image/svg+xml',\n allowUnroled: true,\n }) ??\n selectEntryAsset(entry, {\n roles: ['icon'],\n types: ['svg', 'image'],\n allowUnroled: true,\n })\n );\n}\n\nexport function assetUrl(asset: unknown): string | undefined {\n return asString(assetField(asset, 'publicUrl'));\n}\n\nexport function assetAlt(asset: unknown, fallback?: string): string | undefined {\n return asString(assetField(asset, 'altText')) ?? fallback;\n}\n\nexport function resourceRole(resource: unknown): string | undefined {\n return asString(resourceField(resource, 'role'));\n}\n\nexport function isExternalHref(href: string): boolean {\n return /^(https?:|mailto:|tel:)/.test(href);\n}\n","/**\n * Site chrome: main nav, footer columns, legal strip, social links, inquiry link.\n *\n * All of it comes from ordinary Marvin collections, so a workspace can reorder its own\n * navigation without a deploy. Every piece has a static fallback for when the backend is down.\n */\n\nimport type { MarvinFetcher, HydratedEntry } from './fetch.js';\nimport {\n asRecord,\n asString,\n collectionRole,\n collectionSlugs,\n entryField,\n isExternalHref,\n} from './normalize.js';\nimport type { SiteLoader } from './site.js';\nimport type {\n ApiNavigationLink,\n ApiSite,\n ApiSiteChrome,\n ApiSocialLink,\n MarvinContentEntry,\n} from './types.js';\n\nexport type NavigationContext = 'main' | 'footer';\n\nexport type HrefContext = {\n /** Where this link is being rendered. */\n context: NavigationContext;\n /** The nav collection the entry was read from. */\n collectionSlug: string;\n slug: string;\n /** Every collection the entry belongs to, nav collections included. */\n collections: string[];\n /** The collection slugs treated as navigation, i.e. not route prefixes. */\n navCollections: Set<string>;\n};\n\nexport type ResolveHref = (entry: MarvinContentEntry, context: HrefContext) => string;\n\n/** A link authored in site code rather than in the CMS. */\nexport type NavigationLinkInput = {\n label: string;\n href: string;\n description?: string;\n role?: string;\n};\n\nexport type SocialLinkInput = NavigationLinkInput & { icon: string };\n\nexport type ChromeOptions = {\n /** Collection holding the main nav. Default `'main-navigation'`. */\n mainCollection?: string;\n /** Collection holding the footer nav. Default `'footer-navigation'`. */\n footerCollection?: string;\n /**\n * Collection holding the social links. Default `'social-links'`. When it has entries, they\n * (label, href, optional `icon` field) are exactly the social links, in order; otherwise the\n * site `social` map is used.\n */\n socialCollection?: string;\n /**\n * Route for a nav entry that carries no explicit `href`/`url`/`path` field.\n *\n * The default prefixes the entry's own (non-navigation) collection: an entry in\n * `workshop-reference` becomes `/workshop-reference/<slug>`, and an entry in no other\n * collection becomes `/<slug>`. Override when routes don't mirror collections.\n */\n resolveHref?: ResolveHref;\n /** Membership role that marks a footer link as legal (Terms, Privacy…). Default `'legal'`. */\n legalRole?: string;\n /** Footer links per column once split. Columns are only created above this count. */\n footerColumnThreshold?: number;\n fallback?: {\n mainNavigation?: NavigationLinkInput[];\n footerNavigation?: NavigationLinkInput[][];\n legalLinks?: NavigationLinkInput[];\n socialLinks?: SocialLinkInput[];\n inquiry?: NavigationLinkInput;\n };\n};\n\nexport function toNavigationLink(link: NavigationLinkInput): ApiNavigationLink {\n return {\n label: link.label,\n href: link.href,\n description: link.description,\n external: isExternalHref(link.href),\n role: link.role,\n };\n}\n\n/** Collection-prefixed routing: `/<owning-collection>/<slug>`, else `/<slug>`. */\nexport const defaultResolveHref: ResolveHref = (_entry, context) => {\n const owning = context.collections.find((slug) => !context.navCollections.has(slug));\n return owning ? `/${owning}/${context.slug}` : `/${context.slug}`;\n};\n\n/** A `{label, href}` object stored loose in site metadata (e.g. `metadata.inquiry`). */\nexport function metadataLink(value: unknown): ApiNavigationLink | undefined {\n const link = asRecord(value);\n const label = asString(link.label);\n const href = asString(link.href);\n if (!label || !href) return undefined;\n\n return {\n label,\n href,\n description: asString(link.description) ?? asString(link.subject),\n external: isExternalHref(href),\n };\n}\n\n/** Title-case a social key: `x_handle` → `X Handle`, `email` → `Email`. */\nfunction socialLabel(key: string): string {\n return key.replace(/[-_]/g, ' ').replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nexport function socialLinkFromKey(key: string, href: string): ApiSocialLink {\n return {\n label: socialLabel(key),\n href,\n icon: key,\n external: isExternalHref(href),\n };\n}\n\n/**\n * Icon key for a social link with no `icon` field: `mailto:` → `email`, else the network's\n * name from the host (`www.instagram.com` → `instagram`, `x.com` → `x`), else the label.\n */\nexport function socialIconFor(href: string, label: string): string {\n if (href.startsWith('mailto:')) return 'email';\n try {\n const host = new URL(href).hostname.replace(/^(www|m)\\./, '');\n return host.split('.')[0] ?? label.toLowerCase();\n } catch {\n return label.toLowerCase();\n }\n}\n\nexport type ChromeLoader = {\n get(): Promise<ApiSiteChrome>;\n reset(): void;\n};\n\nexport function createChromeLoader(\n fetcher: MarvinFetcher,\n siteLoader: SiteLoader,\n options: ChromeOptions = {}\n): ChromeLoader {\n const mainCollection = options.mainCollection ?? 'main-navigation';\n const footerCollection = options.footerCollection ?? 'footer-navigation';\n const socialCollection = options.socialCollection ?? 'social-links';\n const legalRole = options.legalRole ?? 'legal';\n const footerColumnThreshold = options.footerColumnThreshold ?? 4;\n const resolveHref = options.resolveHref ?? defaultResolveHref;\n const navCollections = new Set([mainCollection, footerCollection, socialCollection]);\n const fallback = options.fallback ?? {};\n\n let promise: Promise<ApiSiteChrome> | null = null;\n\n function entryToLink(\n entry: HydratedEntry,\n collectionSlug: string,\n context: NavigationContext\n ): ApiNavigationLink {\n const slug = entry.slug ?? '';\n const href =\n asString(entryField(entry, 'href')) ??\n asString(entryField(entry, 'url')) ??\n asString(entryField(entry, 'path')) ??\n resolveHref(entry, {\n context,\n collectionSlug,\n slug,\n collections: collectionSlugs(entry),\n navCollections,\n });\n const label =\n asString(entryField(entry, 'label')) ??\n asString(asRecord((entry as { metadataJson?: unknown }).metadataJson).label) ??\n asString(entry.title) ??\n href;\n\n return {\n label,\n href,\n description:\n asString(entry.summary) ?? asString((entry as { description?: unknown }).description),\n external: isExternalHref(href),\n role: collectionRole(entry, collectionSlug),\n };\n }\n\n async function navigationCollection(\n slug: string,\n context: NavigationContext,\n online: boolean\n ): Promise<{ links: ApiNavigationLink[]; entries: HydratedEntry[] }> {\n if (!online) return { links: [], entries: [] };\n\n // Hydrated: a nav entry's `href`/`label` overrides are schema fields, absent from list items.\n // Empty, hidden or missing all read as empty: Marvin is up, so its answer is final.\n const entries = await fetcher.hydratedCollectionEntries(slug);\n return { links: entries.map((entry) => entryToLink(entry, slug, context)), entries };\n }\n\n function groupFooterLinks(links: ApiNavigationLink[]): ApiNavigationLink[][] {\n if (links.length <= footerColumnThreshold) return [links];\n\n const midpoint = Math.ceil(links.length / 2);\n return [links.slice(0, midpoint), links.slice(midpoint)];\n }\n\n /** The social-links collection as links, or null when it has no entries. */\n async function socialEntries(online: boolean): Promise<ApiSocialLink[] | null> {\n const social = await navigationCollection(socialCollection, 'footer', online);\n if (social.links.length === 0) return null;\n\n return social.links.map((link, index) => ({\n ...link,\n icon:\n asString(entryField(social.entries[index], 'icon')) ?? socialIconFor(link.href, link.label),\n }));\n }\n\n /** The site `social` map plus the contact email; offline, the static social links if both are empty. */\n function siteSocialLinks(site: ApiSite, online: boolean): ApiSocialLink[] {\n const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));\n if (site.email && !links.some((link) => link.icon === 'email')) {\n links.push(socialLinkFromKey('email', `mailto:${site.email}`));\n }\n if (links.length > 0 || online) return links;\n\n return (fallback.socialLinks ?? []).map((link) => ({\n ...toNavigationLink(link),\n icon: link.icon,\n }));\n }\n\n async function load(): Promise<ApiSiteChrome> {\n const site = await siteLoader.get();\n const staticMain = (fallback.mainNavigation ?? []).map(toNavigationLink);\n const staticFooter = (fallback.footerNavigation ?? []).map((group) =>\n group.map(toNavigationLink)\n );\n\n // The build's one \"is Marvin up?\" check: online, Marvin's content is final everywhere below;\n // offline, every part of the chrome is the static fallback.\n const online = await siteLoader.online();\n const main = await navigationCollection(mainCollection, 'main', online);\n const footer = await navigationCollection(footerCollection, 'footer', online);\n\n const social = await socialEntries(online);\n\n // Split footer links by membership role: legal → the legal strip; everything else → columns.\n const columnLinks = footer.links.filter((link) => link.role !== legalRole);\n const staticLegal = (fallback.legalLinks ?? []).map(toNavigationLink);\n\n return {\n site,\n mainNavigation: online ? main.links : staticMain,\n footerNavigation: online ? groupFooterLinks(columnLinks) : staticFooter,\n legalLinks: online ? footer.links.filter((link) => link.role === legalRole) : staticLegal,\n socialLinks: social ?? siteSocialLinks(site, online),\n inquiry: online\n ? metadataLink(site.metadata.inquiry)\n : fallback.inquiry && toNavigationLink(fallback.inquiry),\n };\n }\n\n return {\n get() {\n promise ??= load();\n return promise;\n },\n reset() {\n promise = null;\n },\n };\n}\n","/**\n * Every call into Marvin, wrapped the same way: skip when there's no backend, catch, remember\n * network failures for the latch, warn once, and return an empty value the caller can fall\n * back from. Nothing here throws.\n */\n\nimport type {\n Collection,\n CollectionEntry,\n Entry,\n MarvinAsset,\n MarvinEntry,\n MarvinResource,\n MarvinSite,\n PublishedCollectionSummary,\n Workspace,\n} from '@inneropen/marvin-sdk';\nimport { errorMessage, type MarvinBackend } from './client.js';\nimport type { MarvinContentEntry } from './types.js';\n\n/** An entry that carries `data_json` — i.e. schema fields are readable. */\nexport type HydratedEntry = MarvinEntry | Entry;\n\nexport type MarvinFetcher = ReturnType<typeof createFetcher>;\n\n/**\n * `collections.entries` as SDK 4.2+ declares it: `{ expand: 'full' }` asks the server for full\n * entries (`?expand=full`) and returns them as SDK `Entry` objects. Older SDKs take only the slug\n * and ignore the option, so the same call works with any SDK in the peer range; their list items\n * are then hydrated one by one.\n */\ntype ExpandingCollectionEntries = (\n slug: string,\n options?: { expand?: 'full' }\n) => Promise<MarvinContentEntry[]>;\n\n/**\n * True for a full entry (`PublishedEntryRead`), false for a list item (`PublishedEntryListItem`).\n *\n * `data` doesn't tell them apart: list items carry it too now. What differs is attachments. A full\n * read has `assets[]` and `resources[]` (placement objects, always present, possibly empty); a list\n * item has only `assetSlugs` / `resourceSlugs` and a `featuredAsset`. The SDK's `Entry` wrapper\n * only ever comes from the single-entry read.\n */\nexport function isFullEntry(item: unknown): boolean {\n if (!item || typeof item !== 'object') return false;\n if (typeof (item as { field?: unknown }).field === 'function') return true;\n const raw = item as Record<string, unknown>;\n if ('assetSlugs' in raw || 'resourceSlugs' in raw) return false;\n return Array.isArray(raw.assets) || Array.isArray(raw.resources);\n}\n\n/** A failed hydration read is retried this many times in total, backing off from this base. */\nexport const HYDRATE_ATTEMPTS = 3;\nexport const HYDRATE_BACKOFF_MS = 500;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `Promise.all(items.map(run))` with at most `limit` calls in flight. Order is preserved. */\nasync function mapWithConcurrency<T, R>(\n items: T[],\n limit: number,\n run: (item: T) => Promise<R>\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n\n async function worker(): Promise<void> {\n while (next < items.length) {\n const index = next++;\n results[index] = await run(items[index]);\n }\n }\n\n const workers = Array.from({ length: Math.min(limit, items.length) }, worker);\n await Promise.all(workers);\n return results;\n}\n\nexport function createFetcher(backend: MarvinBackend) {\n async function guarded<T>(label: string, empty: T, run: () => Promise<T>): Promise<T> {\n if (!backend.hasBackend()) return empty;\n try {\n return await run();\n } catch (error) {\n backend.remember(error);\n backend.warn(`${label} unavailable: ${errorMessage(error)}`);\n return empty;\n }\n }\n\n async function entry(slug: string): Promise<Entry | null> {\n return guarded(`Entry \"${slug}\"`, null, () => backend.client().entry(slug));\n }\n\n /**\n * One entry read that survives a transient failure.\n *\n * `entry()` can't be used here: `guarded` swallows the error and trips the latch, and a\n * tripped latch turns every remaining item into a silent null. So retry with backoff,\n * re-opening the latch first in case a sibling read tripped it, and only remember the\n * failure — and warn — once the attempts are spent.\n */\n async function entryForHydration(slug: string): Promise<Entry | null> {\n if (!backend.hasBackend()) return null;\n\n let failure: unknown;\n for (let attempt = 1; attempt <= HYDRATE_ATTEMPTS; attempt++) {\n if (attempt > 1) {\n await sleep(HYDRATE_BACKOFF_MS * 2 ** (attempt - 2));\n if (backend.isLatched()) backend.clearLatch();\n }\n try {\n return await backend.client().entry(slug);\n } catch (error) {\n failure = error;\n }\n }\n\n backend.remember(failure);\n backend.warn(\n `Entry \"${slug}\" dropped after ${HYDRATE_ATTEMPTS} attempts: ${errorMessage(failure)}`\n );\n return null;\n }\n\n /**\n * Turn list items into full entries.\n *\n * The collection endpoint returns `PublishedEntryListItem`: core fields, `metadata_json` and\n * (on current servers) `data`, but only asset/resource *slugs* — no `assets[]` placements (so\n * no roles: no `hero-grade` image) and no `resources[]`. `PublishedEntryRead` (the single-entry\n * endpoint) has all of it. Only items that are already full entries ({@link isFullEntry}) are\n * passed through; everything else is re-read, even when it carries `data`. A list fetched with\n * `expand=full` ({@link expandedCollectionEntries}) is all full entries, so nothing is re-read.\n *\n * Reads run at most `config.hydrateConcurrency` at a time. Unbounded, a 280-entry collection\n * fired 280 requests at once, most of which timed out and were dropped — and the first\n * timeout latched the backend off for the rest of the build.\n */\n /**\n * A collection's entries as full entries in one request, where the server and SDK support\n * `expand=full` (Marvin core with expand, `@inneropen/marvin-sdk` 4.2+). Otherwise this is\n * `collectionEntries`: list items, which {@link hydrate} reads one at a time.\n */\n function expandedCollectionEntries(slug: string): Promise<MarvinContentEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () => {\n const collections = backend.client().collections;\n const entries = collections.entries as ExpandingCollectionEntries;\n return entries.call(collections, slug, { expand: 'full' });\n });\n }\n\n async function hydrate(entries: MarvinContentEntry[]): Promise<HydratedEntry[]> {\n const hydrated = await mapWithConcurrency(\n entries,\n backend.config.hydrateConcurrency,\n async (item) => {\n if (isFullEntry(item)) return item as HydratedEntry;\n return entryForHydration(item.slug ?? '');\n }\n );\n\n return hydrated.filter((item): item is HydratedEntry => Boolean(item));\n }\n\n return {\n backend,\n\n collections(): Promise<PublishedCollectionSummary[]> {\n return guarded('Collections', [], () => backend.client().collections.list());\n },\n\n async collection(slug: string): Promise<Collection | null> {\n return guarded(`Collection \"${slug}\"`, null, async () => {\n const found = await backend.client().collections.get(slug);\n return Array.isArray(found) ? null : found;\n });\n },\n\n collectionEntries(slug: string): Promise<CollectionEntry[]> {\n return guarded(`Collection entries \"${slug}\"`, [], () =>\n backend.client().collections.entries(slug)\n );\n },\n\n /** Try each collection slug in order; return the first that yields entries. */\n async collectionEntriesFallback(slugs: string[]): Promise<CollectionEntry[]> {\n for (const slug of slugs) {\n const entries = await this.collectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n expandedCollectionEntries,\n\n /** Full entries: one `expand=full` request, or per-item reads where that isn't supported. */\n async hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]> {\n return hydrate(await expandedCollectionEntries(slug));\n },\n\n async hydratedCollectionEntriesFallback(slugs: string[]): Promise<HydratedEntry[]> {\n for (const slug of slugs) {\n const entries = await this.hydratedCollectionEntries(slug);\n if (entries.length > 0) return entries;\n }\n return [];\n },\n\n hydrate,\n entry,\n\n site(): Promise<MarvinSite | null> {\n return guarded('Site', null, () => backend.client().getSite());\n },\n\n workspace(): Promise<Workspace | null> {\n return guarded('Workspace', null, () => backend.client().getWorkspace());\n },\n\n assets(type?: string): Promise<MarvinAsset[]> {\n return guarded('Assets', [], () => backend.client().assets.list({ type }));\n },\n\n asset(slugOrId: string): Promise<MarvinAsset | null> {\n return guarded(`Asset \"${slugOrId}\"`, null, () => backend.client().assets.get(slugOrId));\n },\n\n resources(resourceType?: string): Promise<MarvinResource[]> {\n return guarded('Resources', [], () => backend.client().resources.list({ resourceType }));\n },\n };\n}\n","import { Marked, type MarkedOptions, type Tokens } from 'marked';\nimport { findEmbed, type MarvinEmbeds } from './embeds.js';\n\nexport type MarkdownOptions = MarkedOptions;\n\n/** Per-call options for a {@link MarkdownRenderer}. */\nexport type RenderMarkdownOptions = {\n /**\n * The entry's `embeds` map. A paragraph that is exactly a URL with an entry here renders as\n * that embed's Marvin-built HTML instead of a link.\n */\n embeds?: MarvinEmbeds | null;\n};\n\nexport type MarkdownRenderer = (\n markdown: string | string[] | null | undefined,\n options?: RenderMarkdownOptions\n) => Promise<string>;\n\n// Per-call data rides on marked's parse options: marked merges them over the instance defaults and\n// hands the merged object to the renderer as `this.options` for that parse only. A symbol key\n// keeps it clear of marked's own option names.\nconst EMBEDS = Symbol('marvin.embeds');\ntype ParseOptionsWithEmbeds = MarkedOptions & { [EMBEDS]?: MarvinEmbeds | null };\n\n// One run of non-space characters starting with a scheme. `<url>`, `[text](url)` and a URL inside\n// a sentence all fail this, so they stay links.\nconst BARE_URL = /^https?:\\/\\/\\S+$/i;\n\n/**\n * The embed for a paragraph that is exactly a URL with an `embeds` entry, else `undefined`.\n * Exported for tests; the renderer below is the only caller.\n */\nexport function paragraphEmbedHtml(\n token: Pick<Tokens.Paragraph, 'text'>,\n embeds: MarvinEmbeds | null | undefined\n): string | undefined {\n if (!embeds) return undefined;\n const text = token.text.trim();\n if (!BARE_URL.test(text)) return undefined;\n const html = findEmbed(embeds, text)?.html;\n return typeof html === 'string' && html.trim() ? html : undefined;\n}\n\n/**\n * A markdown renderer with its own `marked` instance — options are per-site, not global, so two\n * `createMarvinContent()` instances in one process can't clobber each other's settings.\n *\n * `breaks: false` is the default because CMS body copy is authored as prose: a soft-wrapped\n * paragraph should render as one paragraph. Where authored line breaks must survive (a landing\n * intro, an address block), pass `breaks: true` or pre-convert with a trailing double space.\n *\n * Pass the entry's `embeds` to turn a bare provider URL on its own line into its player:\n *\n * ```ts\n * await renderMarkdown(body, { embeds: entryEmbeds(entry) });\n * ```\n */\nexport function createMarkdownRenderer(options: MarkdownOptions = {}): MarkdownRenderer {\n const marked = new Marked();\n marked.setOptions({ gfm: true, breaks: false, ...options });\n marked.use({\n renderer: {\n paragraph(token) {\n const embeds = (this.options as ParseOptionsWithEmbeds)[EMBEDS];\n const html = paragraphEmbedHtml(token, embeds);\n // `false` hands the token back to marked's default paragraph renderer.\n return html === undefined ? false : `${html}\\n`;\n },\n },\n });\n\n return async (markdown, renderOptions = {}) => {\n const source = Array.isArray(markdown) ? markdown.join('\\n\\n') : (markdown ?? '');\n if (!source) return '';\n const parseOptions: ParseOptionsWithEmbeds = { [EMBEDS]: renderOptions.embeds ?? null };\n return await marked.parse(source, parseOptions);\n };\n}\n\n/** Turn single newlines into hard breaks, leaving paragraph breaks alone. */\nexport function preserveSoftBreaks(source: string): string {\n return source.replace(/(?<!\\n)\\n(?!\\n)/g, ' \\n');\n}\n","/**\n * Media embeds: the published shapes Marvin hands a site, read tolerantly.\n *\n * Marvin resolves a pasted provider link (YouTube, Vimeo, Spotify, …) on the server and publishes\n * the result next to the entry as `entry.embeds`, keyed by the URL exactly as the author wrote it.\n * Stored markdown is never rewritten: a site that knows about embeds swaps the link for the\n * player; an older site just shows the link.\n *\n * These types are declared here rather than imported from `@inneropen/marvin-sdk` so the package\n * works with SDKs that predate embeds. Kind and status are open-ended so a newer server value\n * doesn't break an older site's types.\n */\n\nimport { asRecord, asString } from './normalize.js';\n\nexport type MarvinEmbedKind = 'video' | 'audio' | 'podcast' | 'playlist';\nexport type MarvinEmbedStatus = 'ok' | 'link' | 'unavailable';\nexport type MarvinEmbedMode = 'direct' | 'click_to_load';\n\n/**\n * A Marvin-built iframe. Every value comes from Marvin's provider registry. Exactly one of\n * `aspectRatio` / `height` is set; the other is `null`.\n */\nexport type MarvinEmbedIframe = {\n src: string;\n title: string;\n allow?: string | null;\n sandbox?: string | null;\n referrerpolicy?: string | null;\n /** CSS aspect ratio for video players, e.g. `\"16/9\"` (`\"9/16\"` for Shorts); `null` otherwise. */\n aspectRatio?: string | null;\n /** Fixed pixel height for audio / podcast / playlist players; `null` for video. */\n height?: number | null;\n};\n\n/** The plain link every embed falls back to. */\nexport type MarvinEmbedLink = {\n href: string;\n title: string;\n providerName: string;\n};\n\n/** Optional fields arrive as `null` rather than omitted; every reader here treats both alike. */\nexport type MarvinEmbed = {\n /** The URL exactly as written in the content. */\n url: string;\n canonicalUrl?: string | null;\n /** Provider key, e.g. `youtube`. */\n provider: string;\n /** Display name, e.g. `YouTube`. */\n providerName: string;\n kind: MarvinEmbedKind | (string & {});\n /** `ok` has an iframe; `link` and `unavailable` render as a link card. */\n status: MarvinEmbedStatus | (string & {});\n title?: string | null;\n authorName?: string | null;\n thumbnailUrl?: string | null;\n iframe?: MarvinEmbedIframe | null;\n link: MarvinEmbedLink;\n /** Marvin-built HTML for the site's embed mode (an iframe, a click-to-load facade, or a link). */\n html: string;\n};\n\n/** `entry.embeds`: every resolved embed on the entry, keyed by the URL as written. */\nexport type MarvinEmbeds = Record<string, MarvinEmbed>;\n\n/**\n * `site.embeds` (next to `site.seo`): the site's embed privacy mode, consent text (`{provider}`\n * already substituted per embed in the published `html`) and the frame sources a CSP must allow.\n */\nexport type MarvinSiteEmbeds = {\n mode: MarvinEmbedMode;\n consentText: string;\n /** Origins/hosts the site's embeds may frame — feed these into a `frame-src` directive. */\n frameSources: string[];\n};\n\nconst EMBED_MODES = new Set<string>(['direct', 'click_to_load']);\n\nfunction readEmbedsField(entry: unknown): unknown {\n if (!entry || typeof entry !== 'object') return undefined;\n const direct = (entry as { embeds?: unknown }).embeds;\n if (direct !== undefined) return direct;\n // The SDK's `Entry` class keeps the payload private; its `toJSON()` hands it back.\n const toJSON = (entry as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === 'function') {\n return asRecord(toJSON.call(entry)).embeds;\n }\n return undefined;\n}\n\n/**\n * The entry's `embeds` map, or `undefined` when it has none (an older server, or no embeds).\n * Values that aren't objects carrying a URL are dropped.\n */\nexport function entryEmbeds(entry: unknown): MarvinEmbeds | undefined {\n const raw = readEmbedsField(entry);\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;\n\n const embeds: MarvinEmbeds = {};\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n embeds[key] = value as MarvinEmbed;\n }\n }\n return Object.keys(embeds).length > 0 ? embeds : undefined;\n}\n\n/** The embed for `url` as written, tolerating surrounding whitespace. */\nexport function findEmbed(\n embeds: MarvinEmbeds | null | undefined,\n url: string | null | undefined\n): MarvinEmbed | undefined {\n if (!embeds || !url) return undefined;\n const key = url.trim();\n if (!key) return undefined;\n return Object.prototype.hasOwnProperty.call(embeds, key) ? embeds[key] : undefined;\n}\n\n/** Normalize `site.embeds`; `undefined` when the server doesn't send it. */\nexport function siteEmbeds(raw: unknown): MarvinSiteEmbeds | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const value = asRecord(raw);\n const mode = asString(value.mode);\n const frameSources = Array.isArray(value.frameSources)\n ? value.frameSources.filter(\n (source): source is string => typeof source === 'string' && source.trim().length > 0\n )\n : [];\n\n return {\n // Unknown or missing mode → the privacy-preserving default.\n mode: mode && EMBED_MODES.has(mode) ? (mode as MarvinEmbedMode) : 'click_to_load',\n consentText: asString(value.consentText) ?? '',\n frameSources,\n };\n}\n","const MONTHS = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\n/**\n * Format a date value for display as \"Mon DD, YYYY\".\n *\n * ISO dates/timestamps (e.g. a raw `publishedAt` like \"2026-07-21T21:22:42.045708Z\") are read in\n * UTC — via the date part only, so there's no off-by-one day shift from the viewer's timezone.\n * Values that are already human-readable (e.g. a hand-entered \"Jul 02, 2024\") pass through\n * unchanged, and empty input returns `undefined` so callers can omit the field.\n */\nexport function formatDisplayDate(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n\n const iso = /^(\\d{4})-(\\d{2})-(\\d{2})/.exec(trimmed);\n if (iso) {\n const [, year, month, day] = iso;\n const name = MONTHS[Number(month) - 1];\n if (name) return `${name} ${day}, ${year}`;\n }\n\n return trimmed;\n}\n\n/**\n * Pick `count` items deterministically from `items`, seeded by `pageSlug`.\n *\n * Same page → same selection on every build, different pages → different selections. Use for\n * rotating pull-quotes, value bands, related links: variety across the site without the\n * hydration mismatch a random pick would cause.\n */\nexport function selectValuesForPage<T>(items: T[], pageSlug: string, count = 4): T[] {\n const seeded = [...items];\n let seed = 0;\n\n for (const character of pageSlug) {\n seed = (seed * 31 + character.charCodeAt(0)) >>> 0;\n }\n\n for (let index = seeded.length - 1; index > 0; index -= 1) {\n seed = (seed * 1664525 + 1013904223) >>> 0;\n\n const target = seed % (index + 1);\n\n [seeded[index], seeded[target]] = [seeded[target], seeded[index]];\n }\n\n return seeded.slice(0, count);\n}\n","/**\n * The field accessor handed to a repository's `transform`.\n *\n * Every reader defaults to the documented precedence — `data_json` first, `metadata_json`\n * second — so a transform reads like a field list instead of a pile of coalescing operators.\n */\n\nimport { entryEmbeds, findEmbed, type MarvinEmbed } from './embeds.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport { preserveSoftBreaks } from './markdown.js';\nimport {\n asNumber,\n asRecord,\n asString,\n asStringArray,\n assetAlt,\n assetField,\n assetUrl,\n collectionRole,\n collectionSlugs,\n entryAssets,\n entryData,\n entryMetadata,\n entryResources,\n field,\n focalPoint,\n resourceField,\n selectAssetByRole,\n selectFeaturedAsset,\n selectIconAsset,\n selectImageAsset,\n} from './normalize.js';\nimport { formatDisplayDate } from './format.js';\nimport type {\n ApiImage,\n ApiResourceLink,\n AssetSelectOptions,\n MarvinContentEntry,\n} from './types.js';\n\nexport type ImageFieldOptions = Omit<AssetSelectOptions, 'type' | 'types'> & {\n /**\n * Exact roles to prefer, in order, before the looser role/usage match. Use for derived\n * variants — e.g. `['hero-grade']` to prefer a colour-graded hero over the raw upload.\n */\n preferRoles?: string[];\n /** `metadata_json` key holding a hand-authored `{src, alt, focalPoint}`. */\n metadataKey?: string | false;\n /** Alt text when the asset carries none. Defaults to the entry title. */\n alt?: string;\n /** Fall back to the list item's `featuredAsset`. Default `true`. */\n fallbackToFeatured?: boolean;\n};\n\nexport type IconFieldOptions = {\n roles?: string[];\n /** Fall back to the list item's `featuredAsset` when no icon asset is attached. */\n fallbackToFeatured?: boolean;\n};\n\nexport type MarkdownFieldOptions = {\n /** Keep authored single newlines as hard breaks. Default `false`. */\n softBreaks?: boolean;\n /**\n * Render a bare provider URL on its own line as its player, using the entry's `embeds`.\n * Default `true`; `false` keeps every URL a plain link.\n */\n embeds?: boolean;\n};\n\nexport type ResourceFieldOptions = {\n /** Keep only these resource types, e.g. `['material', 'construction']`. */\n types?: string[];\n /** Keep only resources attached with this role. */\n role?: string;\n /** Build an href from the resource slug. */\n href?: (slug: string) => string;\n};\n\nconst DEFAULT_IMAGE_ROLES = ['hero', 'featured', 'card'];\n\nexport type FieldAccessor = ReturnType<typeof createFieldAccessor>;\n\nexport type FieldAccessorContext = {\n renderMarkdown: MarkdownRenderer;\n /** The route this entry resolves to, if the repository was given an `href` builder. */\n href?: string;\n /** The collection the entry was loaded from, when it came from one. */\n collection?: string;\n};\n\nexport function createFieldAccessor(\n entry: MarvinContentEntry,\n context: FieldAccessorContext\n) {\n const title = asString((entry as { title?: unknown }).title);\n const embeds = entryEmbeds(entry);\n\n const accessor = {\n /** The entry itself, for anything the accessor doesn't cover. */\n entry,\n /** The route this entry resolves to (from the repository's `href` option). */\n href: context.href,\n collection: context.collection,\n\n /** `data_json` → `metadata_json`, untyped. */\n raw(key: string): unknown {\n return field(entry, key);\n },\n\n string(key: string): string | undefined {\n return asString(field(entry, key));\n },\n\n number(key: string): number | undefined {\n return asNumber(field(entry, key));\n },\n\n /** Booleans authored as strings (`\"true\"`, `\"1\"`, `\"yes\"`) read as booleans. */\n bool(key: string): boolean {\n const value = field(entry, key);\n if (typeof value === 'boolean') return value;\n if (typeof value === 'string') {\n return ['true', '1', 'yes', 'on'].includes(value.trim().toLowerCase());\n }\n if (typeof value === 'number') return value !== 0;\n return Boolean(value);\n },\n\n /** A string array, tolerating a single string authored in place of a list. */\n list(key: string): string[] | undefined {\n const value = field(entry, key);\n const array = asStringArray(value);\n if (array) return array;\n const single = asString(value);\n return single ? [single] : undefined;\n },\n\n /**\n * An enum-guarded read: the value if it's in `allowed`, else `fallback`. Replaces the\n * per-field `normalizeStatus`/`normalizeCategory`/`normalizeTone` guards that every site\n * ends up writing.\n */\n oneOf<T extends string>(key: string, allowed: readonly T[] | Set<T>, fallback: T): T {\n const value = asString(field(entry, key));\n const set = allowed instanceof Set ? allowed : new Set<string>(allowed);\n return value && set.has(value as T) ? (value as T) : fallback;\n },\n\n /** A field read as a display date (\"Mon DD, YYYY\"); already-formatted values pass through. */\n date(key: string): string | undefined {\n return formatDisplayDate(asString(field(entry, key)));\n },\n\n /** The raw ISO publish timestamp, if any. */\n publishedAt(): string | undefined {\n const value = (entry as { publishedAt?: unknown }).publishedAt;\n return value != null ? String(value) : undefined;\n },\n\n /**\n * Render a markdown field to HTML. Falls back to the entry's `contentMarkdown` when the\n * named field is absent. Returns `undefined` when there is nothing to render, so the\n * caller can omit the property rather than emit an empty string.\n */\n async markdown(\n key = 'body',\n options: MarkdownFieldOptions = {}\n ): Promise<string | undefined> {\n const raw =\n field<string | string[]>(entry, key) ??\n (entry as { contentMarkdown?: string | string[] }).contentMarkdown;\n const source = Array.isArray(raw) ? raw.join('\\n\\n') : asString(raw);\n if (!source) return undefined;\n return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source, {\n embeds: options.embeds === false ? undefined : embeds,\n });\n },\n\n /**\n * The resolved embed for an `embed` field (its value is the provider URL), or `undefined`\n * when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from\n * `@inneropen/marvin-astro/components`, or output `embed.html`.\n */\n embed(key: string): MarvinEmbed | undefined {\n return findEmbed(embeds, asString(field(entry, key)));\n },\n\n /**\n * The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,\n * the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the\n * list item's `featuredAsset`.\n */\n image(options: ImageFieldOptions = {}): ApiImage | undefined {\n const {\n preferRoles,\n metadataKey = 'featuredImage',\n alt: altFallback,\n fallbackToFeatured = true,\n roles = DEFAULT_IMAGE_ROLES,\n usages = DEFAULT_IMAGE_ROLES,\n allowUnroled = true,\n ...rest\n } = options;\n const fallbackAlt = altFallback ?? title ?? '';\n\n if (metadataKey) {\n const authored = asRecord(entryMetadata(entry)[metadataKey]);\n const src = asString(authored.src);\n if (src) {\n return {\n src,\n alt: asString(authored.alt) ?? fallbackAlt,\n focalPoint: focalPoint(authored.focalPoint),\n };\n }\n }\n\n const asset =\n (preferRoles?.length ? selectAssetByRole(entry, ...preferRoles) : undefined) ??\n selectImageAsset(entry, { ...rest, roles, usages, allowUnroled }) ??\n (fallbackToFeatured ? selectFeaturedAsset(entry) : undefined);\n\n const src = assetUrl(asset);\n if (!src) return undefined;\n\n return {\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n };\n },\n\n /** URL of the entry's icon asset (`role: icon`, SVG preferred). */\n icon(options: IconFieldOptions = {}): string | undefined {\n const { roles = ['icon'], fallbackToFeatured = false } = options;\n const asset = selectAssetByRole(entry, ...roles) ?? selectIconAsset(entry);\n return (\n assetUrl(asset) ?? (fallbackToFeatured ? assetUrl(selectFeaturedAsset(entry)) : undefined)\n );\n },\n\n /** The raw asset placement matching `options` — when you need more than src/alt/focal. */\n asset(options: AssetSelectOptions = {}): Record<string, unknown> | undefined {\n return selectImageAsset(entry, options) ?? selectFeaturedAsset(entry);\n },\n\n /** The asset whose role is EXACTLY one of `roles`, in preference order. */\n assetByRole(...roles: string[]): Record<string, unknown> | undefined {\n return selectAssetByRole(entry, ...roles);\n },\n\n /** All asset placements on the entry. */\n assets(): Record<string, unknown>[] {\n return entryAssets(entry);\n },\n\n /** Every image asset resolved, filtered by role/usage — support/detail galleries. */\n images(options: ImageFieldOptions = {}): (ApiImage & { usage?: string })[] {\n const wanted = new Set([...(options.roles ?? []), ...(options.usages ?? [])]);\n const fallbackAlt = options.alt ?? title ?? '';\n const images: (ApiImage & { usage?: string })[] = [];\n\n for (const asset of entryAssets(entry)) {\n const usage = asString(assetField(asset, 'usage')) ?? asString(assetField(asset, 'role'));\n const src = assetUrl(asset);\n if (!src) continue;\n if (wanted.size > 0 && (!usage || !wanted.has(usage))) continue;\n\n images.push({\n src,\n alt: assetAlt(asset, fallbackAlt) ?? fallbackAlt,\n focalPoint: focalPoint(assetField(asset, 'focalPoint')),\n usage,\n });\n }\n\n return images;\n },\n\n /** Attached resources, normalized to name/type/role/href. */\n resources(options: ResourceFieldOptions = {}): ApiResourceLink[] {\n const types = options.types?.length ? new Set(options.types) : undefined;\n\n return entryResources(entry)\n .map((relationship) => {\n const nested = asRecord(relationship.resource);\n const source = Object.keys(nested).length > 0 ? nested : relationship;\n const slug = asString(resourceField(source, 'slug'));\n return {\n name: asString(resourceField(source, 'name')) ?? '',\n type: asString(resourceField(source, 'resourceType')) ?? '',\n role: asString(assetField(relationship, 'role')),\n slug,\n href: slug && options.href ? options.href(slug) : undefined,\n };\n })\n .filter((resource) => Boolean(resource.name))\n .filter((resource) => !types || types.has(resource.type))\n .filter((resource) => !options.role || resource.role === options.role);\n },\n\n /** The first attached resource matching `options`. */\n resource(options: ResourceFieldOptions = {}): ApiResourceLink | undefined {\n return accessor.resources(options)[0];\n },\n\n /** The `metadata_json` blob. */\n metadata(): Record<string, unknown> {\n return entryMetadata(entry);\n },\n\n /** The `data_json` blob. Empty on a list item that was not hydrated. */\n data(): Record<string, unknown> {\n return entryData(entry);\n },\n\n /** Slugs of every collection the entry belongs to. */\n collections(): string[] {\n return collectionSlugs(entry);\n },\n\n /** The entry's membership role within `collectionSlug`. */\n role(collectionSlug: string): string | undefined {\n return collectionRole(entry, collectionSlug);\n },\n };\n\n return accessor;\n}\n","/**\n * The repository factory.\n *\n * `getAll` / `getBySlug` / `getFeatured` is the same forty lines of try / fallback / memoize\n * around every content type on every site. Only the transform differs. This holds the forty\n * lines; the site writes the transform.\n */\n\nimport { errorMessage } from './client.js';\nimport { createFieldAccessor, type FieldAccessor } from './fields.js';\nimport { isFullEntry, type MarvinFetcher } from './fetch.js';\nimport type { MarkdownRenderer } from './markdown.js';\nimport type { MarvinContentEntry } from './types.js';\n\nexport type RepositoryOptions<T> = {\n /** Collection slug to load from. */\n collection?: string;\n /**\n * Collection slugs tried in order; the first one that yields entries wins. Lets a site work\n * against workspaces that named the same thing `bench-notes`, `journal`, or `blog`.\n */\n collections?: string[];\n /**\n * Fetch each list item as a full entry before transforming.\n *\n * The collection endpoint returns `PublishedEntryListItem`, which has no `assets[]` or\n * `resources[]` (only their slugs) and, on older servers, no `data_json`; the single-entry\n * endpoint returns `PublishedEntryRead`, which has all of it. So if the transform reads asset\n * roles, resources, or (on an older server) any schema field, this must be `true`. The list is\n * requested with `expand=full`, which returns full entries in the one request; a server or SDK\n * without it (SDK < 4.2) costs one more request per entry. `bySlug` always works from full\n * entries either way.\n */\n hydrate?: boolean;\n /** Build the resolved item from an entry. May be async (e.g. to render markdown). */\n transform: (entry: MarvinContentEntry, fields: FieldAccessor) => T | Promise<T>;\n /** Static data to serve when Marvin is unreachable or the collection is empty. */\n fallback?: () => T[] | Promise<T[]>;\n /** Applied to Marvin and fallback results alike, so ordering doesn't depend on the source. */\n sort?: (a: T, b: T) => number;\n /** Applied to Marvin and fallback results alike. */\n filter?: (item: T) => boolean;\n /** The route an entry resolves to; exposed to the transform as `fields.href`. */\n href?: (slug: string, entry: MarvinContentEntry) => string;\n /** How to read an item's slug for `bySlug`. Defaults to `item.slug`. */\n slugOf?: (item: T) => string | undefined;\n /** How to tell whether an item is featured. Defaults to `item.featured`. */\n isFeatured?: (item: T) => boolean;\n};\n\nexport type Repository<T> = {\n /** Every item, resolved once per process. */\n all(): Promise<T[]>;\n /**\n * One item by slug. Served from `all()` once that has loaded; otherwise a direct entry\n * fetch, falling back to a scan of `all()`.\n */\n bySlug(slug: string): Promise<T | undefined>;\n /** The first featured item, or the first item when none is marked. */\n featured(): Promise<T | undefined>;\n /** Every featured item, in `all()` order. */\n allFeatured(): Promise<T[]>;\n /** Drop the memoized results. Next call re-fetches. */\n reset(): void;\n};\n\nexport type RepositoryContext = {\n fetcher: MarvinFetcher;\n renderMarkdown: MarkdownRenderer;\n};\n\nfunction defaultSlugOf<T>(item: T): string | undefined {\n const slug = (item as { slug?: unknown }).slug;\n return typeof slug === 'string' ? slug : undefined;\n}\n\nfunction defaultIsFeatured<T>(item: T): boolean {\n return Boolean((item as { featured?: unknown }).featured);\n}\n\nexport function createRepository<T>(\n context: RepositoryContext,\n options: RepositoryOptions<T>\n): Repository<T> {\n const { fetcher, renderMarkdown } = context;\n const collections = options.collections ?? (options.collection ? [options.collection] : []);\n const slugOf = options.slugOf ?? defaultSlugOf;\n const isFeatured = options.isFeatured ?? defaultIsFeatured;\n\n /** The loaded list, plus the slugs whose items were built from full entries. */\n type Loaded = { items: T[]; fromFullEntry: Set<string> };\n let loadedPromise: Promise<Loaded> | null = null;\n let allPromise: Promise<T[]> | null = null;\n const bySlugCache = new Map<string, Promise<T | undefined>>();\n\n function arrange(items: T[]): T[] {\n const filtered = options.filter ? items.filter(options.filter) : items;\n return options.sort ? [...filtered].sort(options.sort) : filtered;\n }\n\n function transform(entry: MarvinContentEntry, collection?: string): Promise<T> {\n const slug = (entry as { slug?: string }).slug ?? '';\n const fields = createFieldAccessor(entry, {\n renderMarkdown,\n href: options.href?.(slug, entry),\n collection,\n });\n return Promise.resolve(options.transform(entry, fields));\n }\n\n async function useFallback(): Promise<Loaded> {\n return { items: arrange((await options.fallback?.()) ?? []), fromFullEntry: new Set() };\n }\n\n async function load(): Promise<Loaded> {\n if (collections.length > 0 && fetcher.backend.hasBackend()) {\n try {\n const entries = options.hydrate\n ? await fetcher.hydratedCollectionEntriesFallback(collections)\n : await fetcher.collectionEntriesFallback(collections);\n\n if (entries.length > 0) {\n const items = await Promise.all(entries.map((entry) => transform(entry)));\n const fromFullEntry = new Set<string>();\n items.forEach((item, index) => {\n const slug = slugOf(item);\n if (slug !== undefined && isFullEntry(entries[index])) fromFullEntry.add(slug);\n });\n return { items: arrange(items), fromFullEntry };\n }\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(\n `Collection \"${collections[0]}\" failed, using fallback: ${errorMessage(error)}`\n );\n }\n }\n\n return useFallback();\n }\n\n function findInList(items: T[], slug: string): T | undefined {\n return items.find((item) => slugOf(item) === slug);\n }\n\n async function loadBySlug(slug: string): Promise<T | undefined> {\n // Once a hydrated list has loaded, every slug in it is already transformed from a full entry —\n // a per-entry fetch would be a second request for the same content (~1.4s per detail page on\n // a 280-entry site). An item built from a bare list item is NOT served: it has no assets[] or\n // resources[], so a detail page would lose its role-picked images and resource links.\n let listed: T | undefined;\n if (loadedPromise) {\n const loaded = await loadedPromise;\n listed = findInList(loaded.items, slug);\n if (listed && loaded.fromFullEntry.has(slug)) return listed;\n }\n\n if (fetcher.backend.hasBackend()) {\n try {\n const entry = await fetcher.entry(slug);\n if (entry) return await transform(entry);\n } catch (error) {\n fetcher.backend.remember(error);\n fetcher.backend.warn(`Entry \"${slug}\" failed, using fallback: ${errorMessage(error)}`);\n }\n }\n\n // No backend, no such entry, or a transform that blew up: fall back to the resolved list,\n // which is either Marvin's or the static data — the caller doesn't need to care which.\n return listed ?? findInList(await all(), slug);\n }\n\n function all(): Promise<T[]> {\n if (!allPromise) {\n loadedPromise = load();\n allPromise = loadedPromise.then((loaded) => loaded.items);\n }\n return allPromise;\n }\n\n return {\n all,\n\n bySlug(slug: string): Promise<T | undefined> {\n let cached = bySlugCache.get(slug);\n if (!cached) {\n cached = loadBySlug(slug);\n bySlugCache.set(slug, cached);\n }\n return cached;\n },\n\n async allFeatured(): Promise<T[]> {\n return (await all()).filter(isFeatured);\n },\n\n async featured(): Promise<T | undefined> {\n const items = await all();\n return items.find(isFeatured) ?? items[0];\n },\n\n reset() {\n allPromise = null;\n loadedPromise = null;\n bySlugCache.clear();\n },\n };\n}\n","import { entryEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { createMarkdownRenderer, preserveSoftBreaks, type MarkdownRenderer } from './markdown.js';\nimport { asString, assetUrl, entryField, selectAssetByRole } from './normalize.js';\nimport type { SectionLanding } from './types.js';\n\nexport type SectionLandingOptions = {\n /** Asset role to use as the hero image. Default `'hero'`. */\n heroRole?: string;\n /** Field holding the intro copy. Default `'body'`, falling back to the entry summary. */\n bodyField?: string;\n};\n\n/**\n * Resolve a section-landing header from a Marvin `page` entry with the given slug.\n *\n * The intro is rendered with authored line breaks preserved — landing copy is written as a few\n * deliberate lines, not a soft-wrapped paragraph, and losing those breaks changes the layout.\n */\nexport async function loadSectionLanding(\n fetcher: MarvinFetcher,\n slug: string,\n renderMarkdown: MarkdownRenderer = createMarkdownRenderer(),\n options: SectionLandingOptions = {}\n): Promise<SectionLanding> {\n const entry = await fetcher.entry(slug);\n if (!entry) return {};\n\n const source =\n asString(entryField(entry, options.bodyField ?? 'body')) ?? asString(entry.summary);\n\n return {\n title: asString(entry.title),\n introHtml: source\n ? await renderMarkdown(preserveSoftBreaks(source), { embeds: entryEmbeds(entry) })\n : undefined,\n hero: assetUrl(selectAssetByRole(entry, options.heroRole ?? 'hero')),\n };\n}\n","/**\n * Site identity + SEO.\n *\n * Marvin's site configuration is optional almost everywhere; a template that had to guard every\n * field would be unreadable. This resolves it once into an `ApiSite` where title, description,\n * siteName, locale and timezone are always present, and where brand asset slugs have already\n * become URLs.\n */\n\nimport type { MarvinSite, SiteConfiguration } from '@inneropen/marvin-sdk';\nimport { siteEmbeds } from './embeds.js';\nimport type { MarvinFetcher } from './fetch.js';\nimport { asRecord, asString, assetUrl } from './normalize.js';\nimport type { ApiSeo, ApiSite } from './types.js';\n\nexport type SiteOptions = {\n /**\n * Static identity to fall back to, field by field, when Marvin is unreachable or leaves a\n * value unset. Anything omitted here falls through to a neutral default.\n */\n fallback?: Partial<ApiSite> | (() => Partial<ApiSite>);\n /** Default locale when neither Marvin nor the fallback sets one. Default `'en-US'`. */\n defaultLocale?: string;\n /** Default timezone when neither Marvin nor the fallback sets one. Default `'UTC'`. */\n defaultTimezone?: string;\n};\n\n/**\n * Build resolved SEO from Marvin's raw SiteSeo block, applying identity fallbacks so title,\n * description and siteName are never empty. Absent → sensible defaults (robots `index,follow`;\n * og type `website`; large-image Twitter card).\n */\nexport function buildSeo(\n raw: Record<string, unknown> | undefined,\n identity: { title: string; description: string; canonicalUrl?: string; siteName: string }\n): ApiSeo {\n const seo = raw ?? {};\n const verification = asRecord(seo.verification);\n\n return {\n title: asString(seo.title) ?? identity.title,\n titleTemplate: asString(seo.titleTemplate),\n description: asString(seo.description) ?? identity.description,\n keywords: Array.isArray(seo.keywords)\n ? seo.keywords.filter((keyword): keyword is string => typeof keyword === 'string')\n : [],\n robots: asString(seo.robots) ?? 'index,follow',\n image: asString(seo.image),\n imageAlt: asString(seo.imageAlt),\n ogType: asString(seo.ogType) ?? 'website',\n siteName: asString(seo.siteName) ?? identity.siteName,\n twitterCard: asString(seo.twitterCard) ?? 'summary_large_image',\n twitterHandle: asString(seo.twitterHandle),\n twitterCreator: asString(seo.twitterCreator),\n fbAppId: asString(seo.fbAppId),\n themeColor: asString(seo.themeColor),\n publisher: asString(seo.publisher),\n canonicalUrl: identity.canonicalUrl,\n verification: {\n google: asString(verification.google),\n bing: asString(verification.bing),\n pinterest: asString(verification.pinterest),\n yandex: asString(verification.yandex),\n },\n };\n}\n\nfunction normalizeSocial(social: unknown): Record<string, string> {\n return Object.fromEntries(\n Object.entries(asRecord(social)).filter(\n (entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0\n )\n );\n}\n\n/** The offline site: the caller's fallback, with neutral defaults under it. */\nexport function buildFallbackSite(options: SiteOptions = {}): ApiSite {\n const provided = typeof options.fallback === 'function' ? options.fallback() : options.fallback;\n const fallback = provided ?? {};\n const title = fallback.title ?? fallback.name ?? 'Untitled site';\n const description = fallback.description ?? '';\n\n return {\n ...fallback,\n name: fallback.name ?? title,\n title,\n description,\n locale: fallback.locale ?? options.defaultLocale ?? 'en-US',\n timezone: fallback.timezone ?? options.defaultTimezone ?? 'UTC',\n social: fallback.social ?? {},\n metadata: fallback.metadata ?? {},\n seo:\n fallback.seo ??\n buildSeo(undefined, { title, description, siteName: fallback.name ?? title }),\n };\n}\n\nfunction transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite {\n const config: SiteConfiguration = marvinSite.site;\n const metadata = asRecord(config.metadataJson);\n const title =\n asString(config.title) ?? asString(marvinSite.workspace?.name) ?? fallback.title;\n const description = asString(config.description) ?? fallback.description;\n\n return {\n workspaceSlug: asString(marvinSite.workspace?.slug),\n workspaceName: asString(marvinSite.workspace?.name),\n name: title,\n title,\n tagline: asString(config.tagline) ?? fallback.tagline,\n description,\n canonicalUrl: asString(config.canonicalUrl) ?? fallback.canonicalUrl,\n logo: asString(config.logo) ?? fallback.logo,\n favicon: asString(config.favicon) ?? fallback.favicon,\n locale: asString(config.locale) ?? fallback.locale,\n timezone: asString(config.timezone) ?? fallback.timezone,\n email: asString(config.contactEmail) ?? fallback.email,\n imprint: asString(metadata.imprint) ?? fallback.imprint,\n // Marvin's social map only: once Marvin answers, the static one must not leak back in.\n social: normalizeSocial(config.social),\n // Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older\n // SDKs drop the typed field but pass the blob through untouched, and workspaces configured\n // before the typed field existed still only have the blob.\n seo: buildSeo(asRecord((config as { seo?: unknown }).seo ?? metadata.seo), {\n title,\n description,\n canonicalUrl: asString(config.canonicalUrl),\n siteName: title,\n }),\n // Older servers and SDKs don't send it; `siteEmbeds` returns undefined then.\n embeds: siteEmbeds(\n (config as { embeds?: unknown }).embeds ?? (marvinSite as { embeds?: unknown }).embeds\n ),\n metadata,\n };\n}\n\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 /**\n * The build's one \"is Marvin up?\" answer: true when the site configuration came from Marvin.\n * When it did, Marvin's content is final (an empty or hidden collection shows nothing); the\n * static fallbacks stand in only when it didn't.\n */\n online(): Promise<boolean>;\n reset(): void;\n};\n\nexport function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions = {}): SiteLoader {\n const fallback = buildFallbackSite(options);\n let promise: Promise<ApiSite> | null = null;\n let fromMarvin = false;\n\n async function 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 fromMarvin = marvinSite !== null;\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 async online() {\n await this.get();\n return fromMarvin;\n },\n reset() {\n promise = null;\n fromMarvin = false;\n },\n };\n}\n","/**\n * `@inneropen/marvin-astro` — site integration for Marvin CMS.\n *\n * Sits above `@inneropen/marvin-sdk` (transport). This layer is the part every Marvin-backed site\n * rewrites: repositories with a static fallback, site chrome from collections, and payload\n * normalization. Astro components (forms, media embeds, entry-type renderers) live under\n * `@inneropen/marvin-astro/components`.\n *\n * ```ts\n * // src/lib/content.ts\n * export const marvin = createMarvinContent({ site: { fallback: staticSite } });\n *\n * export const posts = marvin.repository({\n * collections: ['bench-notes', 'journal', 'blog'],\n * hydrate: true,\n * href: (slug) => `/bench-notes/${slug}`,\n * fallback: () => staticPosts,\n * transform: async (entry, f) => ({\n * slug: entry.slug,\n * title: entry.title ?? 'Untitled',\n * noteNumber: f.string('noteNumber'),\n * bodyHtml: await f.markdown('body'),\n * image: f.image({ roles: ['hero', 'featured', 'card'] }),\n * }),\n * });\n * ```\n */\n\nimport { createBackend } from './client.js';\nimport { createChromeLoader, type ChromeOptions } from './chrome.js';\nimport { createFetcher } from './fetch.js';\nimport { createMarkdownRenderer } from './markdown.js';\nimport { createRepository, type Repository, type RepositoryOptions } from './repository.js';\nimport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nimport { createSiteLoader, type SiteOptions } from './site.js';\nimport type { MarvinAstroConfig } from './config.js';\nimport type { ApiSite, ApiSiteChrome, SectionLanding } from './types.js';\n\nexport type MarvinContentOptions = MarvinAstroConfig & {\n site?: SiteOptions;\n chrome?: ChromeOptions;\n};\n\nexport type MarvinContent = ReturnType<typeof createMarvinContent>;\n\n/**\n * Build the content layer for one Marvin workspace.\n *\n * Connection settings come from `MARVIN_API_URL`, `MARVIN_SITE_CLIENT_TOKEN` and\n * `MARVIN_WORKSPACE_SLUG` unless passed explicitly. Everything returned degrades to its static\n * fallback when the backend is unreachable, so a site still builds offline.\n */\nexport function createMarvinContent(options: MarvinContentOptions = {}) {\n const backend = createBackend(options);\n const fetcher = createFetcher(backend);\n const renderMarkdown = createMarkdownRenderer(backend.config.markdown);\n const siteLoader = createSiteLoader(fetcher, options.site);\n const chromeLoader = createChromeLoader(fetcher, siteLoader, options.chrome);\n const repositories: Repository<unknown>[] = [];\n\n return {\n /** The resolved configuration, with the token still in place — don't log it. */\n config: backend.config,\n /** Latch control and the raw SDK client. */\n backend,\n /** Guarded, never-throwing wrappers around every published endpoint. */\n fetch: fetcher,\n renderMarkdown,\n\n /** True when Marvin is configured and not currently latched out as unreachable. */\n hasBackend: () => backend.hasBackend(),\n\n /** Resolved site identity, SEO and brand assets. Memoized per process. */\n getSite(): Promise<ApiSite> {\n return siteLoader.get();\n },\n\n /** Navigation, footer, legal, social and inquiry links. Memoized per process. */\n getSiteChrome(): Promise<ApiSiteChrome> {\n return chromeLoader.get();\n },\n\n /** Title/intro/hero for an index page, driven by a `page` entry of the same slug. */\n getSectionLanding(slug: string, sectionOptions?: SectionLandingOptions): Promise<SectionLanding> {\n return loadSectionLanding(fetcher, slug, renderMarkdown, sectionOptions);\n },\n\n /** A collection-backed content repository: `all()` / `bySlug()` / `featured()`. */\n repository<T>(repositoryOptions: RepositoryOptions<T>): Repository<T> {\n const repository = createRepository<T>({ fetcher, renderMarkdown }, repositoryOptions);\n repositories.push(repository as Repository<unknown>);\n return repository;\n },\n\n /** Drop every memoized result and re-open the failure latch. */\n reset() {\n backend.clearLatch();\n siteLoader.reset();\n chromeLoader.reset();\n for (const repository of repositories) repository.reset();\n },\n };\n}\n\nexport {\n createBackend,\n errorMessage,\n isNetworkFailure,\n type MarvinBackend,\n} from './client.js';\nexport {\n DEFAULT_DEV_RETRY_MS,\n DEFAULT_HYDRATE_CONCURRENCY,\n ENV_KEYS,\n describeConfig,\n readEnv,\n resolveConfig,\n type MarvinAstroConfig,\n type MarvinLogger,\n type ResolvedConfig,\n} from './config.js';\nexport { createFetcher, isFullEntry, type HydratedEntry, type MarvinFetcher } from './fetch.js';\nexport {\n createFieldAccessor,\n type FieldAccessor,\n type IconFieldOptions,\n type ImageFieldOptions,\n type MarkdownFieldOptions,\n type ResourceFieldOptions,\n} from './fields.js';\nexport { formatDisplayDate, selectValuesForPage } from './format.js';\nexport {\n createMarkdownRenderer,\n preserveSoftBreaks,\n type MarkdownOptions,\n type MarkdownRenderer,\n type RenderMarkdownOptions,\n} from './markdown.js';\nexport { entryEmbeds, findEmbed, siteEmbeds } from './embeds.js';\nexport * from './normalize.js';\nexport {\n createRepository,\n type Repository,\n type RepositoryContext,\n type RepositoryOptions,\n} from './repository.js';\nexport {\n buildFallbackSite,\n buildSeo,\n createSiteLoader,\n type SiteLoader,\n type SiteOptions,\n} from './site.js';\nexport {\n createChromeLoader,\n defaultResolveHref,\n metadataLink,\n socialLinkFromKey,\n toNavigationLink,\n type ChromeLoader,\n type ChromeOptions,\n type HrefContext,\n type NavigationContext,\n type NavigationLinkInput,\n type ResolveHref,\n type SocialLinkInput,\n} from './chrome.js';\nexport { loadSectionLanding, type SectionLandingOptions } from './sections.js';\nexport type * from './types.js';\n"],"mappings":"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","names":["src"]}
|
package/package.json
CHANGED
package/src/chrome.ts
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
isExternalHref,
|
|
16
16
|
} from './normalize.js';
|
|
17
17
|
import type { SiteLoader } from './site.js';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
ApiNavigationLink,
|
|
20
|
+
ApiSite,
|
|
21
|
+
ApiSiteChrome,
|
|
22
|
+
ApiSocialLink,
|
|
23
|
+
MarvinContentEntry,
|
|
24
|
+
} from './types.js';
|
|
19
25
|
|
|
20
26
|
export type NavigationContext = 'main' | 'footer';
|
|
21
27
|
|
|
@@ -48,6 +54,12 @@ export type ChromeOptions = {
|
|
|
48
54
|
mainCollection?: string;
|
|
49
55
|
/** Collection holding the footer nav. Default `'footer-navigation'`. */
|
|
50
56
|
footerCollection?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Collection holding the social links. Default `'social-links'`. When it has entries, they
|
|
59
|
+
* (label, href, optional `icon` field) are exactly the social links, in order; otherwise the
|
|
60
|
+
* site `social` map is used.
|
|
61
|
+
*/
|
|
62
|
+
socialCollection?: string;
|
|
51
63
|
/**
|
|
52
64
|
* Route for a nav entry that carries no explicit `href`/`url`/`path` field.
|
|
53
65
|
*
|
|
@@ -114,6 +126,20 @@ export function socialLinkFromKey(key: string, href: string): ApiSocialLink {
|
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Icon key for a social link with no `icon` field: `mailto:` → `email`, else the network's
|
|
131
|
+
* name from the host (`www.instagram.com` → `instagram`, `x.com` → `x`), else the label.
|
|
132
|
+
*/
|
|
133
|
+
export function socialIconFor(href: string, label: string): string {
|
|
134
|
+
if (href.startsWith('mailto:')) return 'email';
|
|
135
|
+
try {
|
|
136
|
+
const host = new URL(href).hostname.replace(/^(www|m)\./, '');
|
|
137
|
+
return host.split('.')[0] ?? label.toLowerCase();
|
|
138
|
+
} catch {
|
|
139
|
+
return label.toLowerCase();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
export type ChromeLoader = {
|
|
118
144
|
get(): Promise<ApiSiteChrome>;
|
|
119
145
|
reset(): void;
|
|
@@ -126,10 +152,11 @@ export function createChromeLoader(
|
|
|
126
152
|
): ChromeLoader {
|
|
127
153
|
const mainCollection = options.mainCollection ?? 'main-navigation';
|
|
128
154
|
const footerCollection = options.footerCollection ?? 'footer-navigation';
|
|
155
|
+
const socialCollection = options.socialCollection ?? 'social-links';
|
|
129
156
|
const legalRole = options.legalRole ?? 'legal';
|
|
130
157
|
const footerColumnThreshold = options.footerColumnThreshold ?? 4;
|
|
131
158
|
const resolveHref = options.resolveHref ?? defaultResolveHref;
|
|
132
|
-
const navCollections = new Set([mainCollection, footerCollection]);
|
|
159
|
+
const navCollections = new Set([mainCollection, footerCollection, socialCollection]);
|
|
133
160
|
const fallback = options.fallback ?? {};
|
|
134
161
|
|
|
135
162
|
let promise: Promise<ApiSiteChrome> | null = null;
|
|
@@ -170,15 +197,14 @@ export function createChromeLoader(
|
|
|
170
197
|
async function navigationCollection(
|
|
171
198
|
slug: string,
|
|
172
199
|
context: NavigationContext,
|
|
173
|
-
|
|
174
|
-
): Promise<{ links: ApiNavigationLink[];
|
|
175
|
-
if (!
|
|
200
|
+
online: boolean
|
|
201
|
+
): Promise<{ links: ApiNavigationLink[]; entries: HydratedEntry[] }> {
|
|
202
|
+
if (!online) return { links: [], entries: [] };
|
|
176
203
|
|
|
177
204
|
// Hydrated: a nav entry's `href`/`label` overrides are schema fields, absent from list items.
|
|
205
|
+
// Empty, hidden or missing all read as empty: Marvin is up, so its answer is final.
|
|
178
206
|
const entries = await fetcher.hydratedCollectionEntries(slug);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return { links: entries.map((entry) => entryToLink(entry, slug, context)), fromBackend: true };
|
|
207
|
+
return { links: entries.map((entry) => entryToLink(entry, slug, context)), entries };
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
function groupFooterLinks(links: ApiNavigationLink[]): ApiNavigationLink[][] {
|
|
@@ -188,6 +214,32 @@ export function createChromeLoader(
|
|
|
188
214
|
return [links.slice(0, midpoint), links.slice(midpoint)];
|
|
189
215
|
}
|
|
190
216
|
|
|
217
|
+
/** The social-links collection as links, or null when it has no entries. */
|
|
218
|
+
async function socialEntries(online: boolean): Promise<ApiSocialLink[] | null> {
|
|
219
|
+
const social = await navigationCollection(socialCollection, 'footer', online);
|
|
220
|
+
if (social.links.length === 0) return null;
|
|
221
|
+
|
|
222
|
+
return social.links.map((link, index) => ({
|
|
223
|
+
...link,
|
|
224
|
+
icon:
|
|
225
|
+
asString(entryField(social.entries[index], 'icon')) ?? socialIconFor(link.href, link.label),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The site `social` map plus the contact email; offline, the static social links if both are empty. */
|
|
230
|
+
function siteSocialLinks(site: ApiSite, online: boolean): ApiSocialLink[] {
|
|
231
|
+
const links = Object.entries(site.social).map(([key, href]) => socialLinkFromKey(key, href));
|
|
232
|
+
if (site.email && !links.some((link) => link.icon === 'email')) {
|
|
233
|
+
links.push(socialLinkFromKey('email', `mailto:${site.email}`));
|
|
234
|
+
}
|
|
235
|
+
if (links.length > 0 || online) return links;
|
|
236
|
+
|
|
237
|
+
return (fallback.socialLinks ?? []).map((link) => ({
|
|
238
|
+
...toNavigationLink(link),
|
|
239
|
+
icon: link.icon,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
191
243
|
async function load(): Promise<ApiSiteChrome> {
|
|
192
244
|
const site = await siteLoader.get();
|
|
193
245
|
const staticMain = (fallback.mainNavigation ?? []).map(toNavigationLink);
|
|
@@ -195,38 +247,27 @@ export function createChromeLoader(
|
|
|
195
247
|
group.map(toNavigationLink)
|
|
196
248
|
);
|
|
197
249
|
|
|
198
|
-
|
|
199
|
-
|
|
250
|
+
// The build's one "is Marvin up?" check: online, Marvin's content is final everywhere below;
|
|
251
|
+
// offline, every part of the chrome is the static fallback.
|
|
252
|
+
const online = await siteLoader.online();
|
|
253
|
+
const main = await navigationCollection(mainCollection, 'main', online);
|
|
254
|
+
const footer = await navigationCollection(footerCollection, 'footer', online);
|
|
200
255
|
|
|
201
|
-
const
|
|
202
|
-
socialLinkFromKey(key, href)
|
|
203
|
-
);
|
|
204
|
-
if (site.email && !socialLinks.some((link) => link.icon === 'email')) {
|
|
205
|
-
socialLinks.push(socialLinkFromKey('email', `mailto:${site.email}`));
|
|
206
|
-
}
|
|
256
|
+
const social = await socialEntries(online);
|
|
207
257
|
|
|
208
258
|
// Split footer links by membership role: legal → the legal strip; everything else → columns.
|
|
209
|
-
const legalLinks = footer.fromBackend
|
|
210
|
-
? footer.links.filter((link) => link.role === legalRole)
|
|
211
|
-
: [];
|
|
212
259
|
const columnLinks = footer.links.filter((link) => link.role !== legalRole);
|
|
213
260
|
const staticLegal = (fallback.legalLinks ?? []).map(toNavigationLink);
|
|
214
261
|
|
|
215
262
|
return {
|
|
216
263
|
site,
|
|
217
|
-
mainNavigation: main.links,
|
|
218
|
-
footerNavigation:
|
|
219
|
-
legalLinks:
|
|
220
|
-
socialLinks:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
...toNavigationLink(link),
|
|
225
|
-
icon: link.icon,
|
|
226
|
-
})),
|
|
227
|
-
inquiry:
|
|
228
|
-
metadataLink(site.metadata.inquiry) ??
|
|
229
|
-
(fallback.inquiry ? toNavigationLink(fallback.inquiry) : undefined),
|
|
264
|
+
mainNavigation: online ? main.links : staticMain,
|
|
265
|
+
footerNavigation: online ? groupFooterLinks(columnLinks) : staticFooter,
|
|
266
|
+
legalLinks: online ? footer.links.filter((link) => link.role === legalRole) : staticLegal,
|
|
267
|
+
socialLinks: social ?? siteSocialLinks(site, online),
|
|
268
|
+
inquiry: online
|
|
269
|
+
? metadataLink(site.metadata.inquiry)
|
|
270
|
+
: fallback.inquiry && toNavigationLink(fallback.inquiry),
|
|
230
271
|
};
|
|
231
272
|
}
|
|
232
273
|
|
package/src/site.ts
CHANGED
|
@@ -116,7 +116,8 @@ function transformMarvinSite(marvinSite: MarvinSite, fallback: ApiSite): ApiSite
|
|
|
116
116
|
timezone: asString(config.timezone) ?? fallback.timezone,
|
|
117
117
|
email: asString(config.contactEmail) ?? fallback.email,
|
|
118
118
|
imprint: asString(metadata.imprint) ?? fallback.imprint,
|
|
119
|
-
social:
|
|
119
|
+
// Marvin's social map only: once Marvin answers, the static one must not leak back in.
|
|
120
|
+
social: normalizeSocial(config.social),
|
|
120
121
|
// Typed `config.seo` when the SDK declares it, else the raw `metadata.seo` blob — older
|
|
121
122
|
// SDKs drop the typed field but pass the blob through untouched, and workspaces configured
|
|
122
123
|
// before the typed field existed still only have the blob.
|
|
@@ -140,12 +141,19 @@ function looksLikeAssetSlug(value: string): boolean {
|
|
|
140
141
|
|
|
141
142
|
export type SiteLoader = {
|
|
142
143
|
get(): Promise<ApiSite>;
|
|
144
|
+
/**
|
|
145
|
+
* The build's one "is Marvin up?" answer: true when the site configuration came from Marvin.
|
|
146
|
+
* When it did, Marvin's content is final (an empty or hidden collection shows nothing); the
|
|
147
|
+
* static fallbacks stand in only when it didn't.
|
|
148
|
+
*/
|
|
149
|
+
online(): Promise<boolean>;
|
|
143
150
|
reset(): void;
|
|
144
151
|
};
|
|
145
152
|
|
|
146
153
|
export function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions = {}): SiteLoader {
|
|
147
154
|
const fallback = buildFallbackSite(options);
|
|
148
155
|
let promise: Promise<ApiSite> | null = null;
|
|
156
|
+
let fromMarvin = false;
|
|
149
157
|
|
|
150
158
|
async function resolveAssetSlug(slug: string | undefined): Promise<string | undefined> {
|
|
151
159
|
if (!slug) return undefined;
|
|
@@ -161,6 +169,7 @@ export function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions =
|
|
|
161
169
|
|
|
162
170
|
async function load(): Promise<ApiSite> {
|
|
163
171
|
const marvinSite = await fetcher.site();
|
|
172
|
+
fromMarvin = marvinSite !== null;
|
|
164
173
|
if (!marvinSite) return fallback;
|
|
165
174
|
|
|
166
175
|
const site = transformMarvinSite(marvinSite, fallback);
|
|
@@ -215,8 +224,13 @@ export function createSiteLoader(fetcher: MarvinFetcher, options: SiteOptions =
|
|
|
215
224
|
promise ??= load();
|
|
216
225
|
return promise;
|
|
217
226
|
},
|
|
227
|
+
async online() {
|
|
228
|
+
await this.get();
|
|
229
|
+
return fromMarvin;
|
|
230
|
+
},
|
|
218
231
|
reset() {
|
|
219
232
|
promise = null;
|
|
233
|
+
fromMarvin = false;
|
|
220
234
|
},
|
|
221
235
|
};
|
|
222
236
|
}
|