@inneropen/marvin-astro 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +175 -9
  2. package/dist/embeds-BTNryQA6.d.ts +79 -0
  3. package/dist/index.d.ts +42 -3
  4. package/dist/index.js +143 -9
  5. package/dist/index.js.map +1 -1
  6. package/dist/renderers/index.d.ts +119 -0
  7. package/dist/renderers/index.js +251 -0
  8. package/dist/renderers/index.js.map +1 -0
  9. package/dist/types.d.ts +8 -1
  10. package/package.json +16 -6
  11. package/src/chrome.ts +242 -0
  12. package/src/client.ts +119 -0
  13. package/src/components/Embed.astro +119 -0
  14. package/src/components/EmbedLoader.astro +24 -0
  15. package/src/components/EntryRenderer.astro +40 -0
  16. package/src/components/FormRenderer.astro +188 -0
  17. package/src/components/LinkCard.astro +42 -0
  18. package/src/components/embed-frame.ts +168 -0
  19. package/src/components/embeds.css +137 -0
  20. package/src/components/index.ts +29 -0
  21. package/src/components/integration.ts +62 -0
  22. package/src/components/markdown.ts +4 -0
  23. package/src/components/registry.ts +34 -0
  24. package/src/components/renderers/ArticleRenderer.astro +56 -0
  25. package/src/components/renderers/FaqRenderer.astro +26 -0
  26. package/src/components/renderers/NavigationRenderer.astro +27 -0
  27. package/src/components/renderers/PageRenderer.astro +51 -0
  28. package/src/config.ts +149 -0
  29. package/src/embeds.ts +137 -0
  30. package/src/env.d.ts +5 -0
  31. package/src/fetch.ts +198 -0
  32. package/src/fields.ts +330 -0
  33. package/src/format.ts +63 -0
  34. package/src/index.ts +169 -0
  35. package/src/markdown.ts +84 -0
  36. package/src/normalize.ts +284 -0
  37. package/src/renderers/debug.ts +6 -0
  38. package/src/renderers/index.ts +48 -0
  39. package/src/renderers/registry.ts +133 -0
  40. package/src/renderers/resolve.ts +134 -0
  41. package/src/renderers/types.ts +71 -0
  42. package/src/renderers/validation-types.ts +23 -0
  43. package/src/renderers/validation.ts +55 -0
  44. package/src/repository.ts +190 -0
  45. package/src/sections.ts +39 -0
  46. package/src/site.ts +222 -0
  47. package/src/types.ts +188 -0
package/README.md CHANGED
@@ -3,13 +3,15 @@
3
3
  Site integration for [Marvin CMS](https://github.com/inneropen) on Astro: content repositories
4
4
  with static fallbacks, site chrome from collections, and payload normalization.
5
5
 
6
- Three packages, three concerns:
6
+ Two packages, two concerns:
7
7
 
8
8
  | Package | Concern |
9
9
  |---|---|
10
10
  | `@inneropen/marvin-sdk` | transport — HTTP client, entries, collections, assets |
11
- | `@inneropen/marvin-renderers-core` | entry-type → Astro renderer component mapping |
12
- | **`@inneropen/marvin-astro`** | **site integration — repositories, chrome, normalization** |
11
+ | **`@inneropen/marvin-astro`** | **site integration — repositories, chrome, normalization, and the Astro components (forms, media embeds, entry-type renderers)** |
12
+
13
+ `@inneropen/marvin-renderers-core` has been folded in here — see
14
+ [Migrating from @inneropen/marvin-renderers-core](#migrating-from-inneropenmarvin-renderers-core).
13
15
 
14
16
  A new site wires up to a Marvin workspace by installing one package, setting three env vars, and
15
17
  writing only its own transform functions.
@@ -73,6 +75,9 @@ const { site, mainNavigation } = await marvin.getSiteChrome();
73
75
 
74
76
  `posts.all()` · `posts.bySlug(slug)` · `posts.featured()` · `posts.allFeatured()` · `posts.reset()`
75
77
 
78
+ `bySlug` serves from the loaded list once `all()` has resolved — no extra request per detail
79
+ page — and only fetches the entry directly when nothing is loaded yet or the slug isn't in it.
80
+
76
81
  ## Why `hydrate`
77
82
 
78
83
  The collection endpoint returns `PublishedEntryListItem`, which carries core fields and
@@ -82,6 +87,14 @@ which includes it.
82
87
  So if a transform reads any schema-defined field — anything beyond title/slug/summary/metadata —
83
88
  `hydrate: true` is required or those fields come back `undefined`. It costs one request per entry.
84
89
 
90
+ Those requests run at most `hydrateConcurrency` at a time (default 6) rather than all at once, and
91
+ a read that fails is retried with backoff (3 attempts) before the entry is dropped, so a single
92
+ transient failure neither loses an item nor latches the backend off for the rest of the build.
93
+
94
+ ```ts
95
+ createMarvinContent({ hydrateConcurrency: 4 });
96
+ ```
97
+
85
98
  ## Field precedence
86
99
 
87
100
  Every reader on `f` resolves `data_json` first and `metadata_json` second. An empty string counts
@@ -93,7 +106,8 @@ fall-through a legacy value that *is* set would never surface.
93
106
  | `f.string(key)` `f.number(key)` `f.bool(key)` `f.list(key)` | scalars; `bool` reads `"true"`/`"1"`/`"yes"` |
94
107
  | `f.oneOf(key, allowed, fallback)` | enum guard — replaces per-field `normalizeStatus`-style helpers |
95
108
  | `f.raw(key)` `f.data()` `f.metadata()` | untyped escape hatches |
96
- | `f.markdown(key?, { softBreaks })` | renders to HTML; `undefined` when there is nothing to render |
109
+ | `f.markdown(key?, { softBreaks, embeds })` | renders to HTML, with [media embeds](#media-embeds); `undefined` when there is nothing to render |
110
+ | `f.embed(key)` | the resolved embed for an `embed` field — render with `<Embed>` |
97
111
  | `f.date(key)` `f.publishedAt()` | display date ("Mon DD, YYYY") / raw ISO stamp |
98
112
  | `f.image(options)` `f.images(options)` `f.icon(options)` | resolved `{src, alt, focalPoint}` |
99
113
  | `f.asset(options)` `f.assetByRole(...roles)` `f.assets()` | raw asset placements |
@@ -132,6 +146,153 @@ The default prefixes the entry's own non-navigation collection: an entry in `wor
132
146
  becomes `/workshop-reference/<slug>`, an entry in no other collection becomes `/<slug>`. Override
133
147
  when routes don't mirror collections.
134
148
 
149
+ ## Media embeds
150
+
151
+ Paste a YouTube, Vimeo, Spotify, SoundCloud, Apple Music/Podcasts, Tidal or podcast link on its
152
+ own line in a markdown field and Marvin resolves it on the server. The published entry then
153
+ carries `embeds`, keyed by the URL exactly as written, each with Marvin-built `html` for the
154
+ site's embed mode and the structured fields behind it. The stored markdown never changes, so a
155
+ site that doesn't render embeds just shows the link.
156
+
157
+ **Markdown.** `f.markdown()` passes the entry's embeds automatically. A paragraph that is exactly
158
+ a URL with an `embeds` entry renders as that embed's `html`; `<url>`, `[text](url)`, a URL inside
159
+ a sentence and anything in code stay as they are. Opt a field out with `f.markdown('body',
160
+ { embeds: false })`. Where you call the renderer yourself, pass the embeds:
161
+
162
+ ```ts
163
+ import { entryEmbeds } from '@inneropen/marvin-astro';
164
+
165
+ const bodyHtml = await marvin.renderMarkdown(body, { embeds: entryEmbeds(entry) });
166
+ ```
167
+
168
+ **`embed` fields.** An `embed` field's value is the provider URL; `f.embed(key)` returns its
169
+ resolved embed. Render it with `<Embed>`, which builds the player, facade or link card from the
170
+ structured fields:
171
+
172
+ ```astro
173
+ ---
174
+ import { Embed } from '@inneropen/marvin-astro/components';
175
+ const site = await marvin.getSite();
176
+ ---
177
+ <Embed
178
+ embed={post.video}
179
+ mode={site.embeds?.mode}
180
+ consentText={site.embeds?.consentText}
181
+ frameSources={site.embeds?.frameSources}
182
+ />
183
+ ```
184
+
185
+ **The loader and CSS.** In `click_to_load` mode (Marvin's default) an embed is a facade — a
186
+ button and a plain link — and nothing loads from the provider until the visitor clicks. Put
187
+ `<EmbedLoader />` once in your layout:
188
+
189
+ ```astro
190
+ ---
191
+ import { EmbedLoader } from '@inneropen/marvin-astro/components';
192
+ ---
193
+ <body>
194
+ <slot />
195
+ <EmbedLoader />
196
+ </body>
197
+ ```
198
+
199
+ On click (or Enter/Space) it swaps the button for the iframe. It builds the iframe only from the
200
+ facade's `data-marvin-embed-attrs`, keeps only `src`, `title`, `allow`, `sandbox`,
201
+ `referrerpolicy`, `loading` and `style`, refuses any `src` that isn't https on a host in
202
+ `data-marvin-embed-hosts`, and never parses HTML. It also brings the base `.marvin-embed` CSS:
203
+ responsive aspect ratio for video, fixed height for audio, facade and link-card styles. The
204
+ rules have zero specificity, so any site rule wins; theme through custom properties:
205
+
206
+ ```css
207
+ .marvin-embed, .marvin-embed-link {
208
+ --marvin-embed-radius: 0;
209
+ --marvin-embed-bg: var(--ink);
210
+ --marvin-embed-fg: var(--paper);
211
+ --marvin-embed-max-width: 48rem;
212
+ }
213
+ ```
214
+
215
+ The full list (`--marvin-embed-aspect`, `-height`, `-margin`, `-muted`, `-border`, `-focus`, …)
216
+ is at the top of `src/components/embeds.css`. For CSS without the loader (a `direct`-mode site),
217
+ `import '@inneropen/marvin-astro/components/embeds.css'`.
218
+
219
+ **CSP.** `site.embeds.frameSources` lists every origin the site's embeds may frame. Use it for a
220
+ `frame-src` directive:
221
+
222
+ ```astro
223
+ ---
224
+ const site = await marvin.getSite();
225
+ const frameSrc = ["'self'", ...(site.embeds?.frameSources ?? [])].join(' ');
226
+ ---
227
+ <meta http-equiv="Content-Security-Policy" content={`frame-src ${frameSrc}`} />
228
+ ```
229
+
230
+ The loader script is small enough that Astro inlines it; if your CSP forbids inline scripts, hash
231
+ it or set `vite.build.assetsInlineLimit: 0` so Astro emits it as a file.
232
+
233
+ The embed types (`MarvinEmbed`, `MarvinSiteEmbeds`, …) are declared in this package, so none of
234
+ this needs a newer SDK.
235
+
236
+ ## Components
237
+
238
+ `@inneropen/marvin-astro/components` ships Astro source, compiled by your site's build.
239
+
240
+ | Component | Use |
241
+ |---|---|
242
+ | `FormRenderer` | a public form from a submittable entry type's `formSchema`; unstyled, progressive-enhancement submit |
243
+ | `Embed`, `LinkCard`, `EmbedLoader` | media embeds (above) |
244
+ | `EntryRenderer` | picks the renderer for an entry from its entry type's `rendering` |
245
+ | `PageRenderer`, `ArticleRenderer`, `FaqRenderer`, `NavigationRenderer` | unstyled, semantic core renderers with `data-renderer`/`data-role` hooks |
246
+
247
+ Page and Article render the markdown body (with embeds); pass `renderMarkdown={marvin.renderMarkdown}`
248
+ to use your site's markdown options.
249
+
250
+ The registry and logic: `getRenderer`, `astroRegistry`, `coreRendererPackage` from
251
+ `@inneropen/marvin-astro/components` (or `/components/registry`), and the framework-agnostic
252
+ helpers — `resolveRendererName`, `resolveRendererConfig`, `extractBody`, `extractField`,
253
+ `getFeaturedAsset`, `createRegistry`, `createPackageRegistry`, `createRendererPackage`,
254
+ `validateRenderers`, … — from `@inneropen/marvin-astro/renderers`.
255
+
256
+ Build-time check that every rendered entry type has a renderer:
257
+
258
+ ```js
259
+ // astro.config.mjs
260
+ import { marvinIntegration } from '@inneropen/marvin-astro/components/integration';
261
+ import { astroRegistry } from '@inneropen/marvin-astro/components/registry';
262
+
263
+ export default defineConfig({
264
+ integrations: [marvinIntegration({ registry: astroRegistry })],
265
+ });
266
+ ```
267
+
268
+ Options: `registry` (required), `apiUrl`, `siteToken` (default to the `MARVIN_*` env vars),
269
+ `strict` (throw instead of warn), `ignore` (entry-type slugs to skip).
270
+
271
+ Marvin's system entry types name their renderer package `@inneropen/marvin-renderers-core`; the
272
+ core renderers still answer to that name (and to `@inneropen/marvin-astro`).
273
+
274
+ ## Migrating from @inneropen/marvin-renderers-core
275
+
276
+ The components and logic moved here with their names unchanged. Swap the import paths, then
277
+ remove `@inneropen/marvin-renderers-core` from `package.json`:
278
+
279
+ | Before (`@inneropen/marvin-renderers-core…`) | After (`@inneropen/marvin-astro…`) |
280
+ |---|---|
281
+ | `/astro` | `/components` |
282
+ | `/astro/registry` | `/components/registry` |
283
+ | `/astro/integration` | `/components/integration` |
284
+ | `/astro/renderers/PageRenderer.astro` (etc.) | `/components/renderers/PageRenderer.astro` |
285
+ | `/logic` | `/renderers` |
286
+
287
+ ```diff
288
+ -import { FormRenderer } from '@inneropen/marvin-renderers-core/astro';
289
+ +import { FormRenderer } from '@inneropen/marvin-astro/components';
290
+ ```
291
+
292
+ Behaviour changes: Page/Article now render the body as markdown instead of injecting it raw, and
293
+ `EntryRenderer` renders core entry types without a custom registry (it silently rendered only
294
+ its slot before).
295
+
135
296
  ## The failure latch
136
297
 
137
298
  A static build asks for content once per path. When the backend is down that means N failed
@@ -151,8 +312,8 @@ marvin.backend.clearLatch();
151
312
 
152
313
  ## SEO head (optional)
153
314
 
154
- One component ships, behind its own export path, so the core package stays pure TypeScript and
155
- Astro stays an optional peer dependency.
315
+ `SeoHead` ships behind its own export path (as do the [components](#components)), so the core
316
+ package stays pure TypeScript and Astro stays an optional peer dependency.
156
317
 
157
318
  ```astro
158
319
  ---
@@ -176,18 +337,23 @@ verification tags. No styling, no site coupling.
176
337
  | `@inneropen/marvin-astro` | `createMarvinContent` and every helper below it |
177
338
  | `@inneropen/marvin-astro/types` | resolved types only (`ApiSite`, `ApiSeo`, `ApiSiteChrome`, …) |
178
339
  | `@inneropen/marvin-astro/astro` | `SeoHead` |
340
+ | `@inneropen/marvin-astro/components` | `FormRenderer`, `Embed`, `LinkCard`, `EmbedLoader`, `EntryRenderer`, the core renderers, registry, `marvinIntegration` |
341
+ | `@inneropen/marvin-astro/components/registry` | `getRenderer`, `astroRegistry`, `coreRendererPackage` |
342
+ | `@inneropen/marvin-astro/components/integration` | `marvinIntegration` (for `astro.config.mjs`) |
343
+ | `@inneropen/marvin-astro/components/*` | individual components, `renderers/*.astro`, `embeds.css` |
344
+ | `@inneropen/marvin-astro/renderers` | renderer logic (compiled): resolve, registry, validation |
179
345
 
180
346
  Beyond `createMarvinContent`, the pieces are usable on their own: `createBackend`,
181
347
  `createFetcher`, `createRepository`, `createSiteLoader`, `createChromeLoader`,
182
- `createFieldAccessor`, `createMarkdownRenderer`, `formatDisplayDate`, `selectValuesForPage`, and
183
- the whole `normalize` surface.
348
+ `createFieldAccessor`, `createMarkdownRenderer`, `entryEmbeds`, `findEmbed`, `siteEmbeds`,
349
+ `formatDisplayDate`, `selectValuesForPage`, and the whole `normalize` surface.
184
350
 
185
351
  ## Development
186
352
 
187
353
  ```bash
188
354
  npm install
189
355
  npm run typecheck
190
- npm test # vitest, fixture-driven, no network
356
+ npm test # vitest, fixture-driven, no network; .astro components via Astro's Container API
191
357
  npm run build # tsup → dist/ with .d.ts
192
358
  ```
193
359
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Media embeds: the published shapes Marvin hands a site, read tolerantly.
3
+ *
4
+ * Marvin resolves a pasted provider link (YouTube, Vimeo, Spotify, …) on the server and publishes
5
+ * the result next to the entry as `entry.embeds`, keyed by the URL exactly as the author wrote it.
6
+ * Stored markdown is never rewritten: a site that knows about embeds swaps the link for the
7
+ * player; an older site just shows the link.
8
+ *
9
+ * These types are declared here rather than imported from `@inneropen/marvin-sdk` so the package
10
+ * works with SDKs that predate embeds. Kind and status are open-ended so a newer server value
11
+ * doesn't break an older site's types.
12
+ */
13
+ type MarvinEmbedKind = 'video' | 'audio' | 'podcast' | 'playlist';
14
+ type MarvinEmbedStatus = 'ok' | 'link' | 'unavailable';
15
+ type MarvinEmbedMode = 'direct' | 'click_to_load';
16
+ /**
17
+ * A Marvin-built iframe. Every value comes from Marvin's provider registry. Exactly one of
18
+ * `aspectRatio` / `height` is set; the other is `null`.
19
+ */
20
+ type MarvinEmbedIframe = {
21
+ src: string;
22
+ title: string;
23
+ allow?: string | null;
24
+ sandbox?: string | null;
25
+ referrerpolicy?: string | null;
26
+ /** CSS aspect ratio for video players, e.g. `"16/9"` (`"9/16"` for Shorts); `null` otherwise. */
27
+ aspectRatio?: string | null;
28
+ /** Fixed pixel height for audio / podcast / playlist players; `null` for video. */
29
+ height?: number | null;
30
+ };
31
+ /** The plain link every embed falls back to. */
32
+ type MarvinEmbedLink = {
33
+ href: string;
34
+ title: string;
35
+ providerName: string;
36
+ };
37
+ /** Optional fields arrive as `null` rather than omitted; every reader here treats both alike. */
38
+ type MarvinEmbed = {
39
+ /** The URL exactly as written in the content. */
40
+ url: string;
41
+ canonicalUrl?: string | null;
42
+ /** Provider key, e.g. `youtube`. */
43
+ provider: string;
44
+ /** Display name, e.g. `YouTube`. */
45
+ providerName: string;
46
+ kind: MarvinEmbedKind | (string & {});
47
+ /** `ok` has an iframe; `link` and `unavailable` render as a link card. */
48
+ status: MarvinEmbedStatus | (string & {});
49
+ title?: string | null;
50
+ authorName?: string | null;
51
+ thumbnailUrl?: string | null;
52
+ iframe?: MarvinEmbedIframe | null;
53
+ link: MarvinEmbedLink;
54
+ /** Marvin-built HTML for the site's embed mode (an iframe, a click-to-load facade, or a link). */
55
+ html: string;
56
+ };
57
+ /** `entry.embeds`: every resolved embed on the entry, keyed by the URL as written. */
58
+ type MarvinEmbeds = Record<string, MarvinEmbed>;
59
+ /**
60
+ * `site.embeds` (next to `site.seo`): the site's embed privacy mode, consent text (`{provider}`
61
+ * already substituted per embed in the published `html`) and the frame sources a CSP must allow.
62
+ */
63
+ type MarvinSiteEmbeds = {
64
+ mode: MarvinEmbedMode;
65
+ consentText: string;
66
+ /** Origins/hosts the site's embeds may frame — feed these into a `frame-src` directive. */
67
+ frameSources: string[];
68
+ };
69
+ /**
70
+ * The entry's `embeds` map, or `undefined` when it has none (an older server, or no embeds).
71
+ * Values that aren't objects carrying a URL are dropped.
72
+ */
73
+ declare function entryEmbeds(entry: unknown): MarvinEmbeds | undefined;
74
+ /** The embed for `url` as written, tolerating surrounding whitespace. */
75
+ declare function findEmbed(embeds: MarvinEmbeds | null | undefined, url: string | null | undefined): MarvinEmbed | undefined;
76
+ /** Normalize `site.embeds`; `undefined` when the server doesn't send it. */
77
+ declare function siteEmbeds(raw: unknown): MarvinSiteEmbeds | undefined;
78
+
79
+ export { type MarvinEmbeds as M, type MarvinEmbed as a, type MarvinEmbedIframe as b, type MarvinEmbedKind as c, type MarvinEmbedLink as d, type MarvinEmbedMode as e, type MarvinEmbedStatus as f, type MarvinSiteEmbeds as g, entryEmbeds as h, findEmbed as i, siteEmbeds as s };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  import { MarkedOptions } from 'marked';
2
+ import { M as MarvinEmbeds, a as MarvinEmbed } from './embeds-BTNryQA6.js';
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';
2
4
  import { MarvinContentEntry, ApiSite, ApiSeo, ApiSiteChrome, ApiNavigationLink, ApiSocialLink, AssetSelectOptions, ApiImage, ApiResourceLink, SectionLanding } from './types.js';
3
5
  export { ApiActionVariant, ApiHero, ApiHeroAction, ApiPageAction } from './types.js';
4
6
  import * as _inneropen_marvin_sdk from '@inneropen/marvin-sdk';
5
7
  import { MarvinClient, MarvinEntry, Entry, PublishedCollectionSummary, Collection, CollectionEntry, MarvinSite, Workspace, MarvinAsset, MarvinResource } from '@inneropen/marvin-sdk';
6
8
 
7
9
  type MarkdownOptions = MarkedOptions;
8
- type MarkdownRenderer = (markdown: string | string[] | null | undefined) => Promise<string>;
10
+ /** Per-call options for a {@link MarkdownRenderer}. */
11
+ type RenderMarkdownOptions = {
12
+ /**
13
+ * The entry's `embeds` map. A paragraph that is exactly a URL with an entry here renders as
14
+ * that embed's Marvin-built HTML instead of a link.
15
+ */
16
+ embeds?: MarvinEmbeds | null;
17
+ };
18
+ type MarkdownRenderer = (markdown: string | string[] | null | undefined, options?: RenderMarkdownOptions) => Promise<string>;
9
19
  /**
10
20
  * A markdown renderer with its own `marked` instance — options are per-site, not global, so two
11
21
  * `createMarvinContent()` instances in one process can't clobber each other's settings.
@@ -13,6 +23,12 @@ type MarkdownRenderer = (markdown: string | string[] | null | undefined) => Prom
13
23
  * `breaks: false` is the default because CMS body copy is authored as prose: a soft-wrapped
14
24
  * paragraph should render as one paragraph. Where authored line breaks must survive (a landing
15
25
  * intro, an address block), pass `breaks: true` or pre-convert with a trailing double space.
26
+ *
27
+ * Pass the entry's `embeds` to turn a bare provider URL on its own line into its player:
28
+ *
29
+ * ```ts
30
+ * await renderMarkdown(body, { embeds: entryEmbeds(entry) });
31
+ * ```
16
32
  */
17
33
  declare function createMarkdownRenderer(options?: MarkdownOptions): MarkdownRenderer;
18
34
  /** Turn single newlines into hard breaks, leaving paragraph breaks alone. */
@@ -39,6 +55,8 @@ declare const ENV_KEYS: {
39
55
  };
40
56
  /** Dev default for the network-failure latch: retry a downed backend after 30s. */
41
57
  declare const DEFAULT_DEV_RETRY_MS = 30000;
58
+ /** How many entry reads `hydrate` keeps in flight at once. */
59
+ declare const DEFAULT_HYDRATE_CONCURRENCY = 6;
42
60
  type MarvinAstroConfig = {
43
61
  apiUrl?: string;
44
62
  siteClientToken?: string;
@@ -51,6 +69,12 @@ type MarvinAstroConfig = {
51
69
  * consistently rather than half-succeed with some pages live and some pages static.
52
70
  */
53
71
  retryAfterMs?: number;
72
+ /**
73
+ * Maximum entry reads in flight at once while hydrating a collection. Firing every read at
74
+ * once makes a large collection time out on itself; the default of 6 keeps a 280-entry
75
+ * hydration well inside the SDK's per-request timeout.
76
+ */
77
+ hydrateConcurrency?: number;
54
78
  logger?: MarvinLogger;
55
79
  /** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */
56
80
  markdown?: MarkdownOptions;
@@ -73,6 +97,7 @@ type ResolvedConfig = {
73
97
  workspaceSlug: string;
74
98
  debug: boolean;
75
99
  retryAfterMs: number;
100
+ hydrateConcurrency: number;
76
101
  logger: MarvinLogger;
77
102
  markdown?: MarkdownOptions;
78
103
  now: () => number;
@@ -282,6 +307,11 @@ type IconFieldOptions = {
282
307
  type MarkdownFieldOptions = {
283
308
  /** Keep authored single newlines as hard breaks. Default `false`. */
284
309
  softBreaks?: boolean;
310
+ /**
311
+ * Render a bare provider URL on its own line as its player, using the entry's `embeds`.
312
+ * Default `true`; `false` keeps every URL a plain link.
313
+ */
314
+ embeds?: boolean;
285
315
  };
286
316
  type ResourceFieldOptions = {
287
317
  /** Keep only these resource types, e.g. `['material', 'construction']`. */
@@ -329,6 +359,12 @@ declare function createFieldAccessor(entry: MarvinContentEntry, context: FieldAc
329
359
  * caller can omit the property rather than emit an empty string.
330
360
  */
331
361
  markdown(key?: string, options?: MarkdownFieldOptions): Promise<string | undefined>;
362
+ /**
363
+ * The resolved embed for an `embed` field (its value is the provider URL), or `undefined`
364
+ * when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from
365
+ * `@inneropen/marvin-astro/components`, or output `embed.html`.
366
+ */
367
+ embed(key: string): MarvinEmbed | undefined;
332
368
  /**
333
369
  * The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,
334
370
  * the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the
@@ -404,7 +440,10 @@ type RepositoryOptions<T> = {
404
440
  type Repository<T> = {
405
441
  /** Every item, resolved once per process. */
406
442
  all(): Promise<T[]>;
407
- /** One item by slug — a direct entry fetch, falling back to a scan of `all()`. */
443
+ /**
444
+ * One item by slug. Served from `all()` once that has loaded; otherwise a direct entry
445
+ * fetch, falling back to a scan of `all()`.
446
+ */
408
447
  bySlug(slug: string): Promise<T | undefined>;
409
448
  /** The first featured item, or the first item when none is marked. */
410
449
  featured(): Promise<T | undefined>;
@@ -573,4 +612,4 @@ declare function createMarvinContent(options?: MarvinContentOptions): {
573
612
  reset(): void;
574
613
  };
575
614
 
576
- export { ApiImage, ApiNavigationLink, ApiResourceLink, ApiSeo, ApiSite, ApiSiteChrome, ApiSocialLink, AssetSelectOptions, type ChromeLoader, type ChromeOptions, DEFAULT_DEV_RETRY_MS, ENV_KEYS, type FieldAccessor, type HrefContext, type HydratedEntry, type IconFieldOptions, type ImageFieldOptions, type MarkdownFieldOptions, type MarkdownOptions, type MarkdownRenderer, type MarvinAstroConfig, type MarvinBackend, type MarvinContent, MarvinContentEntry, type MarvinContentOptions, type MarvinFetcher, type MarvinLogger, type NavigationContext, type NavigationLinkInput, type Repository, type RepositoryContext, type RepositoryOptions, type ResolveHref, type ResolvedConfig, type ResourceFieldOptions, SectionLanding, type SectionLandingOptions, type SiteLoader, type SiteOptions, type SocialLinkInput, asNumber, asRecord, asString, asStringArray, assetAlt, assetField, assetUrl, buildFallbackSite, buildSeo, collectionRole, collectionSlugs, createBackend, createChromeLoader, createFetcher, createFieldAccessor, createMarkdownRenderer, createMarvinContent, createRepository, createSiteLoader, defaultResolveHref, describeConfig, entryAssets, entryData, entryField, entryMetadata, entryResources, errorMessage, field, focalPoint, formatDisplayDate, isExternalHref, isNetworkFailure, loadSectionLanding, metadataLink, preserveSoftBreaks, readEnv, resolveConfig, resourceField, resourceRole, selectAssetByRole, selectEntryAsset, selectFeaturedAsset, selectIconAsset, selectImageAsset, selectValuesForPage, socialLinkFromKey, toNavigationLink };
615
+ export { ApiImage, ApiNavigationLink, ApiResourceLink, ApiSeo, ApiSite, ApiSiteChrome, ApiSocialLink, AssetSelectOptions, type ChromeLoader, type ChromeOptions, DEFAULT_DEV_RETRY_MS, DEFAULT_HYDRATE_CONCURRENCY, ENV_KEYS, type FieldAccessor, type HrefContext, type HydratedEntry, type IconFieldOptions, type ImageFieldOptions, type MarkdownFieldOptions, type MarkdownOptions, type MarkdownRenderer, type MarvinAstroConfig, type MarvinBackend, type MarvinContent, MarvinContentEntry, type MarvinContentOptions, MarvinEmbed, MarvinEmbeds, type MarvinFetcher, type MarvinLogger, type NavigationContext, type NavigationLinkInput, type RenderMarkdownOptions, type Repository, type RepositoryContext, type RepositoryOptions, type ResolveHref, type ResolvedConfig, type ResourceFieldOptions, SectionLanding, type SectionLandingOptions, type SiteLoader, type SiteOptions, type SocialLinkInput, asNumber, asRecord, asString, asStringArray, assetAlt, assetField, assetUrl, buildFallbackSite, buildSeo, collectionRole, collectionSlugs, createBackend, createChromeLoader, createFetcher, createFieldAccessor, createMarkdownRenderer, createMarvinContent, createRepository, createSiteLoader, defaultResolveHref, describeConfig, entryAssets, entryData, entryField, entryMetadata, entryResources, errorMessage, field, focalPoint, formatDisplayDate, isExternalHref, isNetworkFailure, loadSectionLanding, metadataLink, preserveSoftBreaks, readEnv, resolveConfig, resourceField, resourceRole, selectAssetByRole, selectEntryAsset, selectFeaturedAsset, selectIconAsset, selectImageAsset, selectValuesForPage, socialLinkFromKey, toNavigationLink };