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