@inneropen/marvin-astro 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -14
- package/dist/embeds-BTNryQA6.d.ts +79 -0
- package/dist/index.d.ts +43 -6
- package/dist/index.js +116 -14
- package/dist/index.js.map +1 -1
- package/dist/renderers/index.d.ts +119 -0
- package/dist/renderers/index.js +251 -0
- package/dist/renderers/index.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/package.json +16 -6
- package/src/chrome.ts +242 -0
- package/src/client.ts +119 -0
- package/src/components/Embed.astro +119 -0
- package/src/components/EmbedLoader.astro +24 -0
- package/src/components/EntryRenderer.astro +40 -0
- package/src/components/FormRenderer.astro +188 -0
- package/src/components/LinkCard.astro +42 -0
- package/src/components/embed-frame.ts +168 -0
- package/src/components/embeds.css +137 -0
- package/src/components/index.ts +29 -0
- package/src/components/integration.ts +62 -0
- package/src/components/markdown.ts +4 -0
- package/src/components/registry.ts +34 -0
- package/src/components/renderers/ArticleRenderer.astro +56 -0
- package/src/components/renderers/FaqRenderer.astro +26 -0
- package/src/components/renderers/NavigationRenderer.astro +27 -0
- package/src/components/renderers/PageRenderer.astro +51 -0
- package/src/config.ts +149 -0
- package/src/embeds.ts +137 -0
- package/src/env.d.ts +5 -0
- package/src/fetch.ts +208 -0
- package/src/fields.ts +330 -0
- package/src/format.ts +63 -0
- package/src/index.ts +169 -0
- package/src/markdown.ts +84 -0
- package/src/normalize.ts +284 -0
- package/src/renderers/debug.ts +6 -0
- package/src/renderers/index.ts +48 -0
- package/src/renderers/registry.ts +133 -0
- package/src/renderers/resolve.ts +134 -0
- package/src/renderers/types.ts +71 -0
- package/src/renderers/validation-types.ts +23 -0
- package/src/renderers/validation.ts +55 -0
- package/src/repository.ts +206 -0
- package/src/sections.ts +39 -0
- package/src/site.ts +222 -0
- 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
|
-
|
|
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
|
-
|
|
|
12
|
-
|
|
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.
|
|
@@ -78,12 +80,16 @@ page — and only fetches the entry directly when nothing is loaded yet or the s
|
|
|
78
80
|
|
|
79
81
|
## Why `hydrate`
|
|
80
82
|
|
|
81
|
-
The collection endpoint returns `PublishedEntryListItem
|
|
82
|
-
`
|
|
83
|
-
|
|
83
|
+
The collection endpoint returns `PublishedEntryListItem`: core fields, `metadata_json` and (on
|
|
84
|
+
current servers) `data`, but only the *slugs* of its assets and resources — no `assets[]`
|
|
85
|
+
placements (so no roles, no `hero-grade` image) and no `resources[]`. The single-entry endpoint
|
|
86
|
+
returns `PublishedEntryRead`, which has all of it.
|
|
84
87
|
|
|
85
|
-
So if a transform reads
|
|
86
|
-
`hydrate: true` is required
|
|
88
|
+
So if a transform reads asset roles, resources, or (on an older server, whose list items lack
|
|
89
|
+
`data`) any schema field, `hydrate: true` is required. It costs one request per entry. Items are
|
|
90
|
+
re-read unless they're already full entries (`isFullEntry`: `assets[]`/`resources[]` present, no
|
|
91
|
+
`assetSlugs`) — carrying `data` isn't enough. `bySlug()` serves a detail page from the loaded list
|
|
92
|
+
only when that item came from a full entry; otherwise it fetches the entry.
|
|
87
93
|
|
|
88
94
|
Those requests run at most `hydrateConcurrency` at a time (default 6) rather than all at once, and
|
|
89
95
|
a read that fails is retried with backoff (3 attempts) before the entry is dropped, so a single
|
|
@@ -104,7 +110,8 @@ fall-through a legacy value that *is* set would never surface.
|
|
|
104
110
|
| `f.string(key)` `f.number(key)` `f.bool(key)` `f.list(key)` | scalars; `bool` reads `"true"`/`"1"`/`"yes"` |
|
|
105
111
|
| `f.oneOf(key, allowed, fallback)` | enum guard — replaces per-field `normalizeStatus`-style helpers |
|
|
106
112
|
| `f.raw(key)` `f.data()` `f.metadata()` | untyped escape hatches |
|
|
107
|
-
| `f.markdown(key?, { softBreaks })` | renders to HTML; `undefined` when there is nothing to render |
|
|
113
|
+
| `f.markdown(key?, { softBreaks, embeds })` | renders to HTML, with [media embeds](#media-embeds); `undefined` when there is nothing to render |
|
|
114
|
+
| `f.embed(key)` | the resolved embed for an `embed` field — render with `<Embed>` |
|
|
108
115
|
| `f.date(key)` `f.publishedAt()` | display date ("Mon DD, YYYY") / raw ISO stamp |
|
|
109
116
|
| `f.image(options)` `f.images(options)` `f.icon(options)` | resolved `{src, alt, focalPoint}` |
|
|
110
117
|
| `f.asset(options)` `f.assetByRole(...roles)` `f.assets()` | raw asset placements |
|
|
@@ -143,6 +150,153 @@ The default prefixes the entry's own non-navigation collection: an entry in `wor
|
|
|
143
150
|
becomes `/workshop-reference/<slug>`, an entry in no other collection becomes `/<slug>`. Override
|
|
144
151
|
when routes don't mirror collections.
|
|
145
152
|
|
|
153
|
+
## Media embeds
|
|
154
|
+
|
|
155
|
+
Paste a YouTube, Vimeo, Spotify, SoundCloud, Apple Music/Podcasts, Tidal or podcast link on its
|
|
156
|
+
own line in a markdown field and Marvin resolves it on the server. The published entry then
|
|
157
|
+
carries `embeds`, keyed by the URL exactly as written, each with Marvin-built `html` for the
|
|
158
|
+
site's embed mode and the structured fields behind it. The stored markdown never changes, so a
|
|
159
|
+
site that doesn't render embeds just shows the link.
|
|
160
|
+
|
|
161
|
+
**Markdown.** `f.markdown()` passes the entry's embeds automatically. A paragraph that is exactly
|
|
162
|
+
a URL with an `embeds` entry renders as that embed's `html`; `<url>`, `[text](url)`, a URL inside
|
|
163
|
+
a sentence and anything in code stay as they are. Opt a field out with `f.markdown('body',
|
|
164
|
+
{ embeds: false })`. Where you call the renderer yourself, pass the embeds:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { entryEmbeds } from '@inneropen/marvin-astro';
|
|
168
|
+
|
|
169
|
+
const bodyHtml = await marvin.renderMarkdown(body, { embeds: entryEmbeds(entry) });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**`embed` fields.** An `embed` field's value is the provider URL; `f.embed(key)` returns its
|
|
173
|
+
resolved embed. Render it with `<Embed>`, which builds the player, facade or link card from the
|
|
174
|
+
structured fields:
|
|
175
|
+
|
|
176
|
+
```astro
|
|
177
|
+
---
|
|
178
|
+
import { Embed } from '@inneropen/marvin-astro/components';
|
|
179
|
+
const site = await marvin.getSite();
|
|
180
|
+
---
|
|
181
|
+
<Embed
|
|
182
|
+
embed={post.video}
|
|
183
|
+
mode={site.embeds?.mode}
|
|
184
|
+
consentText={site.embeds?.consentText}
|
|
185
|
+
frameSources={site.embeds?.frameSources}
|
|
186
|
+
/>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**The loader and CSS.** In `click_to_load` mode (Marvin's default) an embed is a facade — a
|
|
190
|
+
button and a plain link — and nothing loads from the provider until the visitor clicks. Put
|
|
191
|
+
`<EmbedLoader />` once in your layout:
|
|
192
|
+
|
|
193
|
+
```astro
|
|
194
|
+
---
|
|
195
|
+
import { EmbedLoader } from '@inneropen/marvin-astro/components';
|
|
196
|
+
---
|
|
197
|
+
<body>
|
|
198
|
+
<slot />
|
|
199
|
+
<EmbedLoader />
|
|
200
|
+
</body>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
On click (or Enter/Space) it swaps the button for the iframe. It builds the iframe only from the
|
|
204
|
+
facade's `data-marvin-embed-attrs`, keeps only `src`, `title`, `allow`, `sandbox`,
|
|
205
|
+
`referrerpolicy`, `loading` and `style`, refuses any `src` that isn't https on a host in
|
|
206
|
+
`data-marvin-embed-hosts`, and never parses HTML. It also brings the base `.marvin-embed` CSS:
|
|
207
|
+
responsive aspect ratio for video, fixed height for audio, facade and link-card styles. The
|
|
208
|
+
rules have zero specificity, so any site rule wins; theme through custom properties:
|
|
209
|
+
|
|
210
|
+
```css
|
|
211
|
+
.marvin-embed, .marvin-embed-link {
|
|
212
|
+
--marvin-embed-radius: 0;
|
|
213
|
+
--marvin-embed-bg: var(--ink);
|
|
214
|
+
--marvin-embed-fg: var(--paper);
|
|
215
|
+
--marvin-embed-max-width: 48rem;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The full list (`--marvin-embed-aspect`, `-height`, `-margin`, `-muted`, `-border`, `-focus`, …)
|
|
220
|
+
is at the top of `src/components/embeds.css`. For CSS without the loader (a `direct`-mode site),
|
|
221
|
+
`import '@inneropen/marvin-astro/components/embeds.css'`.
|
|
222
|
+
|
|
223
|
+
**CSP.** `site.embeds.frameSources` lists every origin the site's embeds may frame. Use it for a
|
|
224
|
+
`frame-src` directive:
|
|
225
|
+
|
|
226
|
+
```astro
|
|
227
|
+
---
|
|
228
|
+
const site = await marvin.getSite();
|
|
229
|
+
const frameSrc = ["'self'", ...(site.embeds?.frameSources ?? [])].join(' ');
|
|
230
|
+
---
|
|
231
|
+
<meta http-equiv="Content-Security-Policy" content={`frame-src ${frameSrc}`} />
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The loader script is small enough that Astro inlines it; if your CSP forbids inline scripts, hash
|
|
235
|
+
it or set `vite.build.assetsInlineLimit: 0` so Astro emits it as a file.
|
|
236
|
+
|
|
237
|
+
The embed types (`MarvinEmbed`, `MarvinSiteEmbeds`, …) are declared in this package, so none of
|
|
238
|
+
this needs a newer SDK.
|
|
239
|
+
|
|
240
|
+
## Components
|
|
241
|
+
|
|
242
|
+
`@inneropen/marvin-astro/components` ships Astro source, compiled by your site's build.
|
|
243
|
+
|
|
244
|
+
| Component | Use |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `FormRenderer` | a public form from a submittable entry type's `formSchema`; unstyled, progressive-enhancement submit |
|
|
247
|
+
| `Embed`, `LinkCard`, `EmbedLoader` | media embeds (above) |
|
|
248
|
+
| `EntryRenderer` | picks the renderer for an entry from its entry type's `rendering` |
|
|
249
|
+
| `PageRenderer`, `ArticleRenderer`, `FaqRenderer`, `NavigationRenderer` | unstyled, semantic core renderers with `data-renderer`/`data-role` hooks |
|
|
250
|
+
|
|
251
|
+
Page and Article render the markdown body (with embeds); pass `renderMarkdown={marvin.renderMarkdown}`
|
|
252
|
+
to use your site's markdown options.
|
|
253
|
+
|
|
254
|
+
The registry and logic: `getRenderer`, `astroRegistry`, `coreRendererPackage` from
|
|
255
|
+
`@inneropen/marvin-astro/components` (or `/components/registry`), and the framework-agnostic
|
|
256
|
+
helpers — `resolveRendererName`, `resolveRendererConfig`, `extractBody`, `extractField`,
|
|
257
|
+
`getFeaturedAsset`, `createRegistry`, `createPackageRegistry`, `createRendererPackage`,
|
|
258
|
+
`validateRenderers`, … — from `@inneropen/marvin-astro/renderers`.
|
|
259
|
+
|
|
260
|
+
Build-time check that every rendered entry type has a renderer:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
// astro.config.mjs
|
|
264
|
+
import { marvinIntegration } from '@inneropen/marvin-astro/components/integration';
|
|
265
|
+
import { astroRegistry } from '@inneropen/marvin-astro/components/registry';
|
|
266
|
+
|
|
267
|
+
export default defineConfig({
|
|
268
|
+
integrations: [marvinIntegration({ registry: astroRegistry })],
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Options: `registry` (required), `apiUrl`, `siteToken` (default to the `MARVIN_*` env vars),
|
|
273
|
+
`strict` (throw instead of warn), `ignore` (entry-type slugs to skip).
|
|
274
|
+
|
|
275
|
+
Marvin's system entry types name their renderer package `@inneropen/marvin-renderers-core`; the
|
|
276
|
+
core renderers still answer to that name (and to `@inneropen/marvin-astro`).
|
|
277
|
+
|
|
278
|
+
## Migrating from @inneropen/marvin-renderers-core
|
|
279
|
+
|
|
280
|
+
The components and logic moved here with their names unchanged. Swap the import paths, then
|
|
281
|
+
remove `@inneropen/marvin-renderers-core` from `package.json`:
|
|
282
|
+
|
|
283
|
+
| Before (`@inneropen/marvin-renderers-core…`) | After (`@inneropen/marvin-astro…`) |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `/astro` | `/components` |
|
|
286
|
+
| `/astro/registry` | `/components/registry` |
|
|
287
|
+
| `/astro/integration` | `/components/integration` |
|
|
288
|
+
| `/astro/renderers/PageRenderer.astro` (etc.) | `/components/renderers/PageRenderer.astro` |
|
|
289
|
+
| `/logic` | `/renderers` |
|
|
290
|
+
|
|
291
|
+
```diff
|
|
292
|
+
-import { FormRenderer } from '@inneropen/marvin-renderers-core/astro';
|
|
293
|
+
+import { FormRenderer } from '@inneropen/marvin-astro/components';
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Behaviour changes: Page/Article now render the body as markdown instead of injecting it raw, and
|
|
297
|
+
`EntryRenderer` renders core entry types without a custom registry (it silently rendered only
|
|
298
|
+
its slot before).
|
|
299
|
+
|
|
146
300
|
## The failure latch
|
|
147
301
|
|
|
148
302
|
A static build asks for content once per path. When the backend is down that means N failed
|
|
@@ -162,8 +316,8 @@ marvin.backend.clearLatch();
|
|
|
162
316
|
|
|
163
317
|
## SEO head (optional)
|
|
164
318
|
|
|
165
|
-
|
|
166
|
-
Astro stays an optional peer dependency.
|
|
319
|
+
`SeoHead` ships behind its own export path (as do the [components](#components)), so the core
|
|
320
|
+
package stays pure TypeScript and Astro stays an optional peer dependency.
|
|
167
321
|
|
|
168
322
|
```astro
|
|
169
323
|
---
|
|
@@ -187,18 +341,23 @@ verification tags. No styling, no site coupling.
|
|
|
187
341
|
| `@inneropen/marvin-astro` | `createMarvinContent` and every helper below it |
|
|
188
342
|
| `@inneropen/marvin-astro/types` | resolved types only (`ApiSite`, `ApiSeo`, `ApiSiteChrome`, …) |
|
|
189
343
|
| `@inneropen/marvin-astro/astro` | `SeoHead` |
|
|
344
|
+
| `@inneropen/marvin-astro/components` | `FormRenderer`, `Embed`, `LinkCard`, `EmbedLoader`, `EntryRenderer`, the core renderers, registry, `marvinIntegration` |
|
|
345
|
+
| `@inneropen/marvin-astro/components/registry` | `getRenderer`, `astroRegistry`, `coreRendererPackage` |
|
|
346
|
+
| `@inneropen/marvin-astro/components/integration` | `marvinIntegration` (for `astro.config.mjs`) |
|
|
347
|
+
| `@inneropen/marvin-astro/components/*` | individual components, `renderers/*.astro`, `embeds.css` |
|
|
348
|
+
| `@inneropen/marvin-astro/renderers` | renderer logic (compiled): resolve, registry, validation |
|
|
190
349
|
|
|
191
350
|
Beyond `createMarvinContent`, the pieces are usable on their own: `createBackend`,
|
|
192
351
|
`createFetcher`, `createRepository`, `createSiteLoader`, `createChromeLoader`,
|
|
193
|
-
`createFieldAccessor`, `createMarkdownRenderer`, `
|
|
194
|
-
the whole `normalize` surface.
|
|
352
|
+
`createFieldAccessor`, `createMarkdownRenderer`, `entryEmbeds`, `findEmbed`, `siteEmbeds`,
|
|
353
|
+
`formatDisplayDate`, `selectValuesForPage`, and the whole `normalize` surface.
|
|
195
354
|
|
|
196
355
|
## Development
|
|
197
356
|
|
|
198
357
|
```bash
|
|
199
358
|
npm install
|
|
200
359
|
npm run typecheck
|
|
201
|
-
npm test # vitest, fixture-driven, no network
|
|
360
|
+
npm test # vitest, fixture-driven, no network; .astro components via Astro's Container API
|
|
202
361
|
npm run build # tsup → dist/ with .d.ts
|
|
203
362
|
```
|
|
204
363
|
|
|
@@ -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
|
-
|
|
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. */
|
|
@@ -139,6 +155,15 @@ declare function createBackend(options?: MarvinAstroConfig): MarvinBackend;
|
|
|
139
155
|
/** An entry that carries `data_json` — i.e. schema fields are readable. */
|
|
140
156
|
type HydratedEntry = MarvinEntry | Entry;
|
|
141
157
|
type MarvinFetcher = ReturnType<typeof createFetcher>;
|
|
158
|
+
/**
|
|
159
|
+
* True for a full entry (`PublishedEntryRead`), false for a list item (`PublishedEntryListItem`).
|
|
160
|
+
*
|
|
161
|
+
* `data` doesn't tell them apart: list items carry it too now. What differs is attachments. A full
|
|
162
|
+
* read has `assets[]` and `resources[]` (placement objects, always present, possibly empty); a list
|
|
163
|
+
* item has only `assetSlugs` / `resourceSlugs` and a `featuredAsset`. The SDK's `Entry` wrapper
|
|
164
|
+
* only ever comes from the single-entry read.
|
|
165
|
+
*/
|
|
166
|
+
declare function isFullEntry(item: unknown): boolean;
|
|
142
167
|
declare function createFetcher(backend: MarvinBackend): {
|
|
143
168
|
backend: MarvinBackend;
|
|
144
169
|
collections(): Promise<PublishedCollectionSummary[]>;
|
|
@@ -291,6 +316,11 @@ type IconFieldOptions = {
|
|
|
291
316
|
type MarkdownFieldOptions = {
|
|
292
317
|
/** Keep authored single newlines as hard breaks. Default `false`. */
|
|
293
318
|
softBreaks?: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* Render a bare provider URL on its own line as its player, using the entry's `embeds`.
|
|
321
|
+
* Default `true`; `false` keeps every URL a plain link.
|
|
322
|
+
*/
|
|
323
|
+
embeds?: boolean;
|
|
294
324
|
};
|
|
295
325
|
type ResourceFieldOptions = {
|
|
296
326
|
/** Keep only these resource types, e.g. `['material', 'construction']`. */
|
|
@@ -338,6 +368,12 @@ declare function createFieldAccessor(entry: MarvinContentEntry, context: FieldAc
|
|
|
338
368
|
* caller can omit the property rather than emit an empty string.
|
|
339
369
|
*/
|
|
340
370
|
markdown(key?: string, options?: MarkdownFieldOptions): Promise<string | undefined>;
|
|
371
|
+
/**
|
|
372
|
+
* The resolved embed for an `embed` field (its value is the provider URL), or `undefined`
|
|
373
|
+
* when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from
|
|
374
|
+
* `@inneropen/marvin-astro/components`, or output `embed.html`.
|
|
375
|
+
*/
|
|
376
|
+
embed(key: string): MarvinEmbed | undefined;
|
|
341
377
|
/**
|
|
342
378
|
* The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,
|
|
343
379
|
* the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the
|
|
@@ -389,10 +425,11 @@ type RepositoryOptions<T> = {
|
|
|
389
425
|
/**
|
|
390
426
|
* Fetch each list item as a full entry before transforming.
|
|
391
427
|
*
|
|
392
|
-
* The collection endpoint returns `PublishedEntryListItem`, which
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
428
|
+
* The collection endpoint returns `PublishedEntryListItem`, which has no `assets[]` or
|
|
429
|
+
* `resources[]` (only their slugs) and, on older servers, no `data_json`; the single-entry
|
|
430
|
+
* endpoint returns `PublishedEntryRead`, which has all of it. So if the transform reads asset
|
|
431
|
+
* roles, resources, or (on an older server) any schema field, this must be `true`. It costs one
|
|
432
|
+
* request per entry. `bySlug` always works from full entries either way.
|
|
396
433
|
*/
|
|
397
434
|
hydrate?: boolean;
|
|
398
435
|
/** Build the resolved item from an entry. May be async (e.g. to render markdown). */
|
|
@@ -585,4 +622,4 @@ declare function createMarvinContent(options?: MarvinContentOptions): {
|
|
|
585
622
|
reset(): void;
|
|
586
623
|
};
|
|
587
624
|
|
|
588
|
-
export { ApiImage, ApiNavigationLink, ApiResourceLink, ApiSeo, ApiSite, ApiSiteChrome, ApiSocialLink, AssetSelectOptions, type ChromeLoader, type ChromeOptions, DEFAULT_DEV_RETRY_MS, DEFAULT_HYDRATE_CONCURRENCY, ENV_KEYS, type FieldAccessor, type HrefContext, type HydratedEntry, type IconFieldOptions, type ImageFieldOptions, type MarkdownFieldOptions, type MarkdownOptions, type MarkdownRenderer, type MarvinAstroConfig, type MarvinBackend, type MarvinContent, MarvinContentEntry, type MarvinContentOptions, type MarvinFetcher, type MarvinLogger, type NavigationContext, type NavigationLinkInput, type Repository, type RepositoryContext, type RepositoryOptions, type ResolveHref, type ResolvedConfig, type ResourceFieldOptions, SectionLanding, type SectionLandingOptions, type SiteLoader, type SiteOptions, type SocialLinkInput, asNumber, asRecord, asString, asStringArray, assetAlt, assetField, assetUrl, buildFallbackSite, buildSeo, collectionRole, collectionSlugs, createBackend, createChromeLoader, createFetcher, createFieldAccessor, createMarkdownRenderer, createMarvinContent, createRepository, createSiteLoader, defaultResolveHref, describeConfig, entryAssets, entryData, entryField, entryMetadata, entryResources, errorMessage, field, focalPoint, formatDisplayDate, isExternalHref, isNetworkFailure, loadSectionLanding, metadataLink, preserveSoftBreaks, readEnv, resolveConfig, resourceField, resourceRole, selectAssetByRole, selectEntryAsset, selectFeaturedAsset, selectIconAsset, selectImageAsset, selectValuesForPage, socialLinkFromKey, toNavigationLink };
|
|
625
|
+
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, isFullEntry, isNetworkFailure, loadSectionLanding, metadataLink, preserveSoftBreaks, readEnv, resolveConfig, resourceField, resourceRole, selectAssetByRole, selectEntryAsset, selectFeaturedAsset, selectIconAsset, selectImageAsset, selectValuesForPage, socialLinkFromKey, toNavigationLink };
|
package/dist/index.js
CHANGED
|
@@ -404,6 +404,13 @@ function createChromeLoader(fetcher, siteLoader, options = {}) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
// src/fetch.ts
|
|
407
|
+
function isFullEntry(item) {
|
|
408
|
+
if (!item || typeof item !== "object") return false;
|
|
409
|
+
if (typeof item.field === "function") return true;
|
|
410
|
+
const raw = item;
|
|
411
|
+
if ("assetSlugs" in raw || "resourceSlugs" in raw) return false;
|
|
412
|
+
return Array.isArray(raw.assets) || Array.isArray(raw.resources);
|
|
413
|
+
}
|
|
407
414
|
var HYDRATE_ATTEMPTS = 3;
|
|
408
415
|
var HYDRATE_BACKOFF_MS = 500;
|
|
409
416
|
function sleep(ms) {
|
|
@@ -461,9 +468,7 @@ function createFetcher(backend) {
|
|
|
461
468
|
entries,
|
|
462
469
|
backend.config.hydrateConcurrency,
|
|
463
470
|
async (item) => {
|
|
464
|
-
if (
|
|
465
|
-
return item;
|
|
466
|
-
}
|
|
471
|
+
if (isFullEntry(item)) return item;
|
|
467
472
|
return entryForHydration(item.slug ?? "");
|
|
468
473
|
}
|
|
469
474
|
);
|
|
@@ -527,13 +532,79 @@ function createFetcher(backend) {
|
|
|
527
532
|
|
|
528
533
|
// src/markdown.ts
|
|
529
534
|
import { Marked } from "marked";
|
|
535
|
+
|
|
536
|
+
// src/embeds.ts
|
|
537
|
+
var EMBED_MODES = /* @__PURE__ */ new Set(["direct", "click_to_load"]);
|
|
538
|
+
function readEmbedsField(entry) {
|
|
539
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
540
|
+
const direct = entry.embeds;
|
|
541
|
+
if (direct !== void 0) return direct;
|
|
542
|
+
const toJSON = entry.toJSON;
|
|
543
|
+
if (typeof toJSON === "function") {
|
|
544
|
+
return asRecord(toJSON.call(entry)).embeds;
|
|
545
|
+
}
|
|
546
|
+
return void 0;
|
|
547
|
+
}
|
|
548
|
+
function entryEmbeds(entry) {
|
|
549
|
+
const raw = readEmbedsField(entry);
|
|
550
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
551
|
+
const embeds = {};
|
|
552
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
553
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
554
|
+
embeds[key] = value;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return Object.keys(embeds).length > 0 ? embeds : void 0;
|
|
558
|
+
}
|
|
559
|
+
function findEmbed(embeds, url) {
|
|
560
|
+
if (!embeds || !url) return void 0;
|
|
561
|
+
const key = url.trim();
|
|
562
|
+
if (!key) return void 0;
|
|
563
|
+
return Object.prototype.hasOwnProperty.call(embeds, key) ? embeds[key] : void 0;
|
|
564
|
+
}
|
|
565
|
+
function siteEmbeds(raw) {
|
|
566
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
567
|
+
const value = asRecord(raw);
|
|
568
|
+
const mode = asString(value.mode);
|
|
569
|
+
const frameSources = Array.isArray(value.frameSources) ? value.frameSources.filter(
|
|
570
|
+
(source) => typeof source === "string" && source.trim().length > 0
|
|
571
|
+
) : [];
|
|
572
|
+
return {
|
|
573
|
+
// Unknown or missing mode → the privacy-preserving default.
|
|
574
|
+
mode: mode && EMBED_MODES.has(mode) ? mode : "click_to_load",
|
|
575
|
+
consentText: asString(value.consentText) ?? "",
|
|
576
|
+
frameSources
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/markdown.ts
|
|
581
|
+
var EMBEDS = /* @__PURE__ */ Symbol("marvin.embeds");
|
|
582
|
+
var BARE_URL = /^https?:\/\/\S+$/i;
|
|
583
|
+
function paragraphEmbedHtml(token, embeds) {
|
|
584
|
+
if (!embeds) return void 0;
|
|
585
|
+
const text = token.text.trim();
|
|
586
|
+
if (!BARE_URL.test(text)) return void 0;
|
|
587
|
+
const html = findEmbed(embeds, text)?.html;
|
|
588
|
+
return typeof html === "string" && html.trim() ? html : void 0;
|
|
589
|
+
}
|
|
530
590
|
function createMarkdownRenderer(options = {}) {
|
|
531
591
|
const marked = new Marked();
|
|
532
592
|
marked.setOptions({ gfm: true, breaks: false, ...options });
|
|
533
|
-
|
|
593
|
+
marked.use({
|
|
594
|
+
renderer: {
|
|
595
|
+
paragraph(token) {
|
|
596
|
+
const embeds = this.options[EMBEDS];
|
|
597
|
+
const html = paragraphEmbedHtml(token, embeds);
|
|
598
|
+
return html === void 0 ? false : `${html}
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
return async (markdown, renderOptions = {}) => {
|
|
534
604
|
const source = Array.isArray(markdown) ? markdown.join("\n\n") : markdown ?? "";
|
|
535
605
|
if (!source) return "";
|
|
536
|
-
|
|
606
|
+
const parseOptions = { [EMBEDS]: renderOptions.embeds ?? null };
|
|
607
|
+
return await marked.parse(source, parseOptions);
|
|
537
608
|
};
|
|
538
609
|
}
|
|
539
610
|
function preserveSoftBreaks(source) {
|
|
@@ -585,6 +656,7 @@ function selectValuesForPage(items, pageSlug, count = 4) {
|
|
|
585
656
|
var DEFAULT_IMAGE_ROLES = ["hero", "featured", "card"];
|
|
586
657
|
function createFieldAccessor(entry, context) {
|
|
587
658
|
const title = asString(entry.title);
|
|
659
|
+
const embeds = entryEmbeds(entry);
|
|
588
660
|
const accessor = {
|
|
589
661
|
/** The entry itself, for anything the accessor doesn't cover. */
|
|
590
662
|
entry,
|
|
@@ -647,7 +719,17 @@ function createFieldAccessor(entry, context) {
|
|
|
647
719
|
const raw = field(entry, key) ?? entry.contentMarkdown;
|
|
648
720
|
const source = Array.isArray(raw) ? raw.join("\n\n") : asString(raw);
|
|
649
721
|
if (!source) return void 0;
|
|
650
|
-
return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source
|
|
722
|
+
return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source, {
|
|
723
|
+
embeds: options.embeds === false ? void 0 : embeds
|
|
724
|
+
});
|
|
725
|
+
},
|
|
726
|
+
/**
|
|
727
|
+
* The resolved embed for an `embed` field (its value is the provider URL), or `undefined`
|
|
728
|
+
* when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from
|
|
729
|
+
* `@inneropen/marvin-astro/components`, or output `embed.html`.
|
|
730
|
+
*/
|
|
731
|
+
embed(key) {
|
|
732
|
+
return findEmbed(embeds, asString(field(entry, key)));
|
|
651
733
|
},
|
|
652
734
|
/**
|
|
653
735
|
* The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,
|
|
@@ -776,6 +858,7 @@ function createRepository(context, options) {
|
|
|
776
858
|
const collections = options.collections ?? (options.collection ? [options.collection] : []);
|
|
777
859
|
const slugOf = options.slugOf ?? defaultSlugOf;
|
|
778
860
|
const isFeatured = options.isFeatured ?? defaultIsFeatured;
|
|
861
|
+
let loadedPromise = null;
|
|
779
862
|
let allPromise = null;
|
|
780
863
|
const bySlugCache = /* @__PURE__ */ new Map();
|
|
781
864
|
function arrange(items) {
|
|
@@ -792,7 +875,7 @@ function createRepository(context, options) {
|
|
|
792
875
|
return Promise.resolve(options.transform(entry, fields));
|
|
793
876
|
}
|
|
794
877
|
async function useFallback() {
|
|
795
|
-
return arrange(await options.fallback?.() ?? []);
|
|
878
|
+
return { items: arrange(await options.fallback?.() ?? []), fromFullEntry: /* @__PURE__ */ new Set() };
|
|
796
879
|
}
|
|
797
880
|
async function load() {
|
|
798
881
|
if (collections.length > 0 && fetcher.backend.hasBackend()) {
|
|
@@ -800,7 +883,12 @@ function createRepository(context, options) {
|
|
|
800
883
|
const entries = options.hydrate ? await fetcher.hydratedCollectionEntriesFallback(collections) : await fetcher.collectionEntriesFallback(collections);
|
|
801
884
|
if (entries.length > 0) {
|
|
802
885
|
const items = await Promise.all(entries.map((entry) => transform(entry)));
|
|
803
|
-
|
|
886
|
+
const fromFullEntry = /* @__PURE__ */ new Set();
|
|
887
|
+
items.forEach((item, index) => {
|
|
888
|
+
const slug = slugOf(item);
|
|
889
|
+
if (slug !== void 0 && isFullEntry(entries[index])) fromFullEntry.add(slug);
|
|
890
|
+
});
|
|
891
|
+
return { items: arrange(items), fromFullEntry };
|
|
804
892
|
}
|
|
805
893
|
} catch (error) {
|
|
806
894
|
fetcher.backend.remember(error);
|
|
@@ -815,9 +903,11 @@ function createRepository(context, options) {
|
|
|
815
903
|
return items.find((item) => slugOf(item) === slug);
|
|
816
904
|
}
|
|
817
905
|
async function loadBySlug(slug) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
906
|
+
let listed;
|
|
907
|
+
if (loadedPromise) {
|
|
908
|
+
const loaded = await loadedPromise;
|
|
909
|
+
listed = findInList(loaded.items, slug);
|
|
910
|
+
if (listed && loaded.fromFullEntry.has(slug)) return listed;
|
|
821
911
|
}
|
|
822
912
|
if (fetcher.backend.hasBackend()) {
|
|
823
913
|
try {
|
|
@@ -828,10 +918,13 @@ function createRepository(context, options) {
|
|
|
828
918
|
fetcher.backend.warn(`Entry "${slug}" failed, using fallback: ${errorMessage(error)}`);
|
|
829
919
|
}
|
|
830
920
|
}
|
|
831
|
-
return findInList(await all(), slug);
|
|
921
|
+
return listed ?? findInList(await all(), slug);
|
|
832
922
|
}
|
|
833
923
|
function all() {
|
|
834
|
-
|
|
924
|
+
if (!allPromise) {
|
|
925
|
+
loadedPromise = load();
|
|
926
|
+
allPromise = loadedPromise.then((loaded) => loaded.items);
|
|
927
|
+
}
|
|
835
928
|
return allPromise;
|
|
836
929
|
}
|
|
837
930
|
return {
|
|
@@ -853,6 +946,7 @@ function createRepository(context, options) {
|
|
|
853
946
|
},
|
|
854
947
|
reset() {
|
|
855
948
|
allPromise = null;
|
|
949
|
+
loadedPromise = null;
|
|
856
950
|
bySlugCache.clear();
|
|
857
951
|
}
|
|
858
952
|
};
|
|
@@ -865,7 +959,7 @@ async function loadSectionLanding(fetcher, slug, renderMarkdown = createMarkdown
|
|
|
865
959
|
const source = asString(entryField(entry, options.bodyField ?? "body")) ?? asString(entry.summary);
|
|
866
960
|
return {
|
|
867
961
|
title: asString(entry.title),
|
|
868
|
-
introHtml: source ? await renderMarkdown(preserveSoftBreaks(source)) : void 0,
|
|
962
|
+
introHtml: source ? await renderMarkdown(preserveSoftBreaks(source), { embeds: entryEmbeds(entry) }) : void 0,
|
|
869
963
|
hero: assetUrl(selectAssetByRole(entry, options.heroRole ?? "hero"))
|
|
870
964
|
};
|
|
871
965
|
}
|
|
@@ -952,6 +1046,10 @@ function transformMarvinSite(marvinSite, fallback) {
|
|
|
952
1046
|
canonicalUrl: asString(config.canonicalUrl),
|
|
953
1047
|
siteName: title
|
|
954
1048
|
}),
|
|
1049
|
+
// Older servers and SDKs don't send it; `siteEmbeds` returns undefined then.
|
|
1050
|
+
embeds: siteEmbeds(
|
|
1051
|
+
config.embeds ?? marvinSite.embeds
|
|
1052
|
+
),
|
|
955
1053
|
metadata
|
|
956
1054
|
};
|
|
957
1055
|
}
|
|
@@ -1083,14 +1181,17 @@ export {
|
|
|
1083
1181
|
describeConfig,
|
|
1084
1182
|
entryAssets,
|
|
1085
1183
|
entryData,
|
|
1184
|
+
entryEmbeds,
|
|
1086
1185
|
entryField,
|
|
1087
1186
|
entryMetadata,
|
|
1088
1187
|
entryResources,
|
|
1089
1188
|
errorMessage,
|
|
1090
1189
|
field,
|
|
1190
|
+
findEmbed,
|
|
1091
1191
|
focalPoint,
|
|
1092
1192
|
formatDisplayDate,
|
|
1093
1193
|
isExternalHref,
|
|
1194
|
+
isFullEntry,
|
|
1094
1195
|
isNetworkFailure,
|
|
1095
1196
|
loadSectionLanding,
|
|
1096
1197
|
metadataLink,
|
|
@@ -1105,6 +1206,7 @@ export {
|
|
|
1105
1206
|
selectIconAsset,
|
|
1106
1207
|
selectImageAsset,
|
|
1107
1208
|
selectValuesForPage,
|
|
1209
|
+
siteEmbeds,
|
|
1108
1210
|
socialLinkFromKey,
|
|
1109
1211
|
toNavigationLink
|
|
1110
1212
|
};
|