@inneropen/marvin-astro 1.0.0-next.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 +209 -0
- package/dist/index.d.ts +576 -0
- package/dist/index.js +1061 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/src/astro/SeoHead.astro +73 -0
- package/src/astro/index.ts +8 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { MarkedOptions } from 'marked';
|
|
2
|
+
import { MarvinContentEntry, ApiSite, ApiSeo, ApiSiteChrome, ApiNavigationLink, ApiSocialLink, AssetSelectOptions, ApiImage, ApiResourceLink, SectionLanding } from './types.js';
|
|
3
|
+
export { ApiActionVariant, ApiHero, ApiHeroAction, ApiPageAction } from './types.js';
|
|
4
|
+
import * as _inneropen_marvin_sdk from '@inneropen/marvin-sdk';
|
|
5
|
+
import { MarvinClient, MarvinEntry, Entry, PublishedCollectionSummary, Collection, CollectionEntry, MarvinSite, Workspace, MarvinAsset, MarvinResource } from '@inneropen/marvin-sdk';
|
|
6
|
+
|
|
7
|
+
type MarkdownOptions = MarkedOptions;
|
|
8
|
+
type MarkdownRenderer = (markdown: string | string[] | null | undefined) => Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* A markdown renderer with its own `marked` instance — options are per-site, not global, so two
|
|
11
|
+
* `createMarvinContent()` instances in one process can't clobber each other's settings.
|
|
12
|
+
*
|
|
13
|
+
* `breaks: false` is the default because CMS body copy is authored as prose: a soft-wrapped
|
|
14
|
+
* paragraph should render as one paragraph. Where authored line breaks must survive (a landing
|
|
15
|
+
* intro, an address block), pass `breaks: true` or pre-convert with a trailing double space.
|
|
16
|
+
*/
|
|
17
|
+
declare function createMarkdownRenderer(options?: MarkdownOptions): MarkdownRenderer;
|
|
18
|
+
/** Turn single newlines into hard breaks, leaving paragraph breaks alone. */
|
|
19
|
+
declare function preserveSoftBreaks(source: string): string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration + environment resolution.
|
|
23
|
+
*
|
|
24
|
+
* Precedence for every value: explicit option → `import.meta.env` → `process.env`.
|
|
25
|
+
* Astro exposes server-side env through `import.meta.env`; a plain Node script
|
|
26
|
+
* (a smoke test, a build hook) only has `process.env`. Both work, unchanged.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
type MarvinLogger = {
|
|
30
|
+
log: (message: string, ...args: unknown[]) => void;
|
|
31
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
32
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
33
|
+
};
|
|
34
|
+
declare const ENV_KEYS: {
|
|
35
|
+
readonly apiUrl: "MARVIN_API_URL";
|
|
36
|
+
readonly siteClientToken: "MARVIN_SITE_CLIENT_TOKEN";
|
|
37
|
+
readonly workspaceSlug: "MARVIN_WORKSPACE_SLUG";
|
|
38
|
+
readonly debug: "MARVIN_DEBUG";
|
|
39
|
+
};
|
|
40
|
+
/** Dev default for the network-failure latch: retry a downed backend after 30s. */
|
|
41
|
+
declare const DEFAULT_DEV_RETRY_MS = 30000;
|
|
42
|
+
type MarvinAstroConfig = {
|
|
43
|
+
apiUrl?: string;
|
|
44
|
+
siteClientToken?: string;
|
|
45
|
+
workspaceSlug?: string;
|
|
46
|
+
/** Log the resolved env once at startup, and enable SDK request logging. */
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* How long the network-failure latch stays closed before the backend is retried.
|
|
50
|
+
* Defaults to 30s in dev and `Infinity` in production — a build should fail fast and
|
|
51
|
+
* consistently rather than half-succeed with some pages live and some pages static.
|
|
52
|
+
*/
|
|
53
|
+
retryAfterMs?: number;
|
|
54
|
+
logger?: MarvinLogger;
|
|
55
|
+
/** Markdown rendering options; defaults to `{ gfm: true, breaks: false }`. */
|
|
56
|
+
markdown?: MarkdownOptions;
|
|
57
|
+
/** Explicit env source. Highest priority after direct options — mainly a test seam. */
|
|
58
|
+
env?: Record<string, string | undefined>;
|
|
59
|
+
/** Injectable clock, so latch expiry is testable without waiting. */
|
|
60
|
+
now?: () => number;
|
|
61
|
+
/** Injectable SDK client factory — a test seam; defaults to the SDK's `createMarvinClient`. */
|
|
62
|
+
createClient?: (config: MarvinClientOptions) => unknown;
|
|
63
|
+
};
|
|
64
|
+
type MarvinClientOptions = {
|
|
65
|
+
apiUrl: string;
|
|
66
|
+
siteClientToken: string;
|
|
67
|
+
workspaceSlug: string;
|
|
68
|
+
debug: boolean;
|
|
69
|
+
};
|
|
70
|
+
type ResolvedConfig = {
|
|
71
|
+
apiUrl: string;
|
|
72
|
+
siteClientToken: string;
|
|
73
|
+
workspaceSlug: string;
|
|
74
|
+
debug: boolean;
|
|
75
|
+
retryAfterMs: number;
|
|
76
|
+
logger: MarvinLogger;
|
|
77
|
+
markdown?: MarkdownOptions;
|
|
78
|
+
now: () => number;
|
|
79
|
+
createClient?: (config: MarvinClientOptions) => unknown;
|
|
80
|
+
/** True when all three connection values are present. */
|
|
81
|
+
configured: boolean;
|
|
82
|
+
};
|
|
83
|
+
/** Read one env var, honouring the documented precedence. */
|
|
84
|
+
declare function readEnv(key: string, override?: Record<string, string | undefined>): string | undefined;
|
|
85
|
+
declare function resolveConfig(options?: MarvinAstroConfig): ResolvedConfig;
|
|
86
|
+
/** One-line summary of the resolved env, with the token masked. For `debug` logging. */
|
|
87
|
+
declare function describeConfig(config: ResolvedConfig): Record<string, unknown>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SDK client construction and the network-failure latch.
|
|
91
|
+
*
|
|
92
|
+
* The latch exists because a static build asks for content once per path. When the backend is
|
|
93
|
+
* down, that is N failed requests with N timeouts before the build gives up. The latch trips on
|
|
94
|
+
* the first network failure and short-circuits the rest.
|
|
95
|
+
*
|
|
96
|
+
* Unlike the hand-rolled original it is *retryable*: it expires after `retryAfterMs` so a dev
|
|
97
|
+
* server recovers on its own when the backend comes back, instead of serving stale static data
|
|
98
|
+
* until someone restarts it. Production keeps the permanent latch — a build should fail fast and
|
|
99
|
+
* consistently rather than half-succeed.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
declare function errorMessage(error: unknown): string;
|
|
103
|
+
/**
|
|
104
|
+
* A network failure means "the backend is unreachable", not "this request was bad". Only the
|
|
105
|
+
* former should trip the latch — a 404 on one entry says nothing about the next one.
|
|
106
|
+
*/
|
|
107
|
+
declare function isNetworkFailure(error: unknown): boolean;
|
|
108
|
+
type MarvinBackend = {
|
|
109
|
+
readonly config: ResolvedConfig;
|
|
110
|
+
/** True when Marvin is configured AND not currently latched out. */
|
|
111
|
+
hasBackend(): boolean;
|
|
112
|
+
/** The SDK client. Throws when Marvin is not configured. */
|
|
113
|
+
client(): MarvinClient;
|
|
114
|
+
/** Trip the latch if this error indicates the backend is unreachable. */
|
|
115
|
+
remember(error: unknown): void;
|
|
116
|
+
/** True while the latch is closed (backend considered unreachable). */
|
|
117
|
+
isLatched(): boolean;
|
|
118
|
+
/** Manually re-open the latch, e.g. after a deploy. */
|
|
119
|
+
clearLatch(): void;
|
|
120
|
+
warn(message: string): void;
|
|
121
|
+
};
|
|
122
|
+
declare function createBackend(options?: MarvinAstroConfig): MarvinBackend;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Every call into Marvin, wrapped the same way: skip when there's no backend, catch, remember
|
|
126
|
+
* network failures for the latch, warn once, and return an empty value the caller can fall
|
|
127
|
+
* back from. Nothing here throws.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/** An entry that carries `data_json` — i.e. schema fields are readable. */
|
|
131
|
+
type HydratedEntry = MarvinEntry | Entry;
|
|
132
|
+
type MarvinFetcher = ReturnType<typeof createFetcher>;
|
|
133
|
+
declare function createFetcher(backend: MarvinBackend): {
|
|
134
|
+
backend: MarvinBackend;
|
|
135
|
+
collections(): Promise<PublishedCollectionSummary[]>;
|
|
136
|
+
collection(slug: string): Promise<Collection | null>;
|
|
137
|
+
collectionEntries(slug: string): Promise<CollectionEntry[]>;
|
|
138
|
+
/** Try each collection slug in order; return the first that yields entries. */
|
|
139
|
+
collectionEntriesFallback(slugs: string[]): Promise<CollectionEntry[]>;
|
|
140
|
+
hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]>;
|
|
141
|
+
hydratedCollectionEntriesFallback(slugs: string[]): Promise<HydratedEntry[]>;
|
|
142
|
+
hydrate: (entries: MarvinContentEntry[]) => Promise<HydratedEntry[]>;
|
|
143
|
+
entry: (slug: string) => Promise<Entry | null>;
|
|
144
|
+
site(): Promise<MarvinSite | null>;
|
|
145
|
+
workspace(): Promise<Workspace | null>;
|
|
146
|
+
assets(type?: string): Promise<MarvinAsset[]>;
|
|
147
|
+
asset(slugOrId: string): Promise<MarvinAsset | null>;
|
|
148
|
+
resources(resourceType?: string): Promise<MarvinResource[]>;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Site identity + SEO.
|
|
153
|
+
*
|
|
154
|
+
* Marvin's site configuration is optional almost everywhere; a template that had to guard every
|
|
155
|
+
* field would be unreadable. This resolves it once into an `ApiSite` where title, description,
|
|
156
|
+
* siteName, locale and timezone are always present, and where brand asset slugs have already
|
|
157
|
+
* become URLs.
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
type SiteOptions = {
|
|
161
|
+
/**
|
|
162
|
+
* Static identity to fall back to, field by field, when Marvin is unreachable or leaves a
|
|
163
|
+
* value unset. Anything omitted here falls through to a neutral default.
|
|
164
|
+
*/
|
|
165
|
+
fallback?: Partial<ApiSite> | (() => Partial<ApiSite>);
|
|
166
|
+
/** Default locale when neither Marvin nor the fallback sets one. Default `'en-US'`. */
|
|
167
|
+
defaultLocale?: string;
|
|
168
|
+
/** Default timezone when neither Marvin nor the fallback sets one. Default `'UTC'`. */
|
|
169
|
+
defaultTimezone?: string;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Build resolved SEO from Marvin's raw SiteSeo block, applying identity fallbacks so title,
|
|
173
|
+
* description and siteName are never empty. Absent → sensible defaults (robots `index,follow`;
|
|
174
|
+
* og type `website`; large-image Twitter card).
|
|
175
|
+
*/
|
|
176
|
+
declare function buildSeo(raw: Record<string, unknown> | undefined, identity: {
|
|
177
|
+
title: string;
|
|
178
|
+
description: string;
|
|
179
|
+
canonicalUrl?: string;
|
|
180
|
+
siteName: string;
|
|
181
|
+
}): ApiSeo;
|
|
182
|
+
/** The offline site: the caller's fallback, with neutral defaults under it. */
|
|
183
|
+
declare function buildFallbackSite(options?: SiteOptions): ApiSite;
|
|
184
|
+
type SiteLoader = {
|
|
185
|
+
get(): Promise<ApiSite>;
|
|
186
|
+
reset(): void;
|
|
187
|
+
};
|
|
188
|
+
declare function createSiteLoader(fetcher: MarvinFetcher, options?: SiteOptions): SiteLoader;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Site chrome: main nav, footer columns, legal strip, social links, inquiry link.
|
|
192
|
+
*
|
|
193
|
+
* All of it comes from ordinary Marvin collections, so a workspace can reorder its own
|
|
194
|
+
* navigation without a deploy. Every piece has a static fallback for when the backend is down.
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
type NavigationContext = 'main' | 'footer';
|
|
198
|
+
type HrefContext = {
|
|
199
|
+
/** Where this link is being rendered. */
|
|
200
|
+
context: NavigationContext;
|
|
201
|
+
/** The nav collection the entry was read from. */
|
|
202
|
+
collectionSlug: string;
|
|
203
|
+
slug: string;
|
|
204
|
+
/** Every collection the entry belongs to, nav collections included. */
|
|
205
|
+
collections: string[];
|
|
206
|
+
/** The collection slugs treated as navigation, i.e. not route prefixes. */
|
|
207
|
+
navCollections: Set<string>;
|
|
208
|
+
};
|
|
209
|
+
type ResolveHref = (entry: MarvinContentEntry, context: HrefContext) => string;
|
|
210
|
+
/** A link authored in site code rather than in the CMS. */
|
|
211
|
+
type NavigationLinkInput = {
|
|
212
|
+
label: string;
|
|
213
|
+
href: string;
|
|
214
|
+
description?: string;
|
|
215
|
+
role?: string;
|
|
216
|
+
};
|
|
217
|
+
type SocialLinkInput = NavigationLinkInput & {
|
|
218
|
+
icon: string;
|
|
219
|
+
};
|
|
220
|
+
type ChromeOptions = {
|
|
221
|
+
/** Collection holding the main nav. Default `'main-navigation'`. */
|
|
222
|
+
mainCollection?: string;
|
|
223
|
+
/** Collection holding the footer nav. Default `'footer-navigation'`. */
|
|
224
|
+
footerCollection?: string;
|
|
225
|
+
/**
|
|
226
|
+
* Route for a nav entry that carries no explicit `href`/`url`/`path` field.
|
|
227
|
+
*
|
|
228
|
+
* The default prefixes the entry's own (non-navigation) collection: an entry in
|
|
229
|
+
* `workshop-reference` becomes `/workshop-reference/<slug>`, and an entry in no other
|
|
230
|
+
* collection becomes `/<slug>`. Override when routes don't mirror collections.
|
|
231
|
+
*/
|
|
232
|
+
resolveHref?: ResolveHref;
|
|
233
|
+
/** Membership role that marks a footer link as legal (Terms, Privacy…). Default `'legal'`. */
|
|
234
|
+
legalRole?: string;
|
|
235
|
+
/** Footer links per column once split. Columns are only created above this count. */
|
|
236
|
+
footerColumnThreshold?: number;
|
|
237
|
+
fallback?: {
|
|
238
|
+
mainNavigation?: NavigationLinkInput[];
|
|
239
|
+
footerNavigation?: NavigationLinkInput[][];
|
|
240
|
+
legalLinks?: NavigationLinkInput[];
|
|
241
|
+
socialLinks?: SocialLinkInput[];
|
|
242
|
+
inquiry?: NavigationLinkInput;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
declare function toNavigationLink(link: NavigationLinkInput): ApiNavigationLink;
|
|
246
|
+
/** Collection-prefixed routing: `/<owning-collection>/<slug>`, else `/<slug>`. */
|
|
247
|
+
declare const defaultResolveHref: ResolveHref;
|
|
248
|
+
/** A `{label, href}` object stored loose in site metadata (e.g. `metadata.inquiry`). */
|
|
249
|
+
declare function metadataLink(value: unknown): ApiNavigationLink | undefined;
|
|
250
|
+
declare function socialLinkFromKey(key: string, href: string): ApiSocialLink;
|
|
251
|
+
type ChromeLoader = {
|
|
252
|
+
get(): Promise<ApiSiteChrome>;
|
|
253
|
+
reset(): void;
|
|
254
|
+
};
|
|
255
|
+
declare function createChromeLoader(fetcher: MarvinFetcher, siteLoader: SiteLoader, options?: ChromeOptions): ChromeLoader;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The field accessor handed to a repository's `transform`.
|
|
259
|
+
*
|
|
260
|
+
* Every reader defaults to the documented precedence — `data_json` first, `metadata_json`
|
|
261
|
+
* second — so a transform reads like a field list instead of a pile of coalescing operators.
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
type ImageFieldOptions = Omit<AssetSelectOptions, 'type' | 'types'> & {
|
|
265
|
+
/**
|
|
266
|
+
* Exact roles to prefer, in order, before the looser role/usage match. Use for derived
|
|
267
|
+
* variants — e.g. `['hero-grade']` to prefer a colour-graded hero over the raw upload.
|
|
268
|
+
*/
|
|
269
|
+
preferRoles?: string[];
|
|
270
|
+
/** `metadata_json` key holding a hand-authored `{src, alt, focalPoint}`. */
|
|
271
|
+
metadataKey?: string | false;
|
|
272
|
+
/** Alt text when the asset carries none. Defaults to the entry title. */
|
|
273
|
+
alt?: string;
|
|
274
|
+
/** Fall back to the list item's `featuredAsset`. Default `true`. */
|
|
275
|
+
fallbackToFeatured?: boolean;
|
|
276
|
+
};
|
|
277
|
+
type IconFieldOptions = {
|
|
278
|
+
roles?: string[];
|
|
279
|
+
/** Fall back to the list item's `featuredAsset` when no icon asset is attached. */
|
|
280
|
+
fallbackToFeatured?: boolean;
|
|
281
|
+
};
|
|
282
|
+
type MarkdownFieldOptions = {
|
|
283
|
+
/** Keep authored single newlines as hard breaks. Default `false`. */
|
|
284
|
+
softBreaks?: boolean;
|
|
285
|
+
};
|
|
286
|
+
type ResourceFieldOptions = {
|
|
287
|
+
/** Keep only these resource types, e.g. `['material', 'construction']`. */
|
|
288
|
+
types?: string[];
|
|
289
|
+
/** Keep only resources attached with this role. */
|
|
290
|
+
role?: string;
|
|
291
|
+
/** Build an href from the resource slug. */
|
|
292
|
+
href?: (slug: string) => string;
|
|
293
|
+
};
|
|
294
|
+
type FieldAccessor = ReturnType<typeof createFieldAccessor>;
|
|
295
|
+
type FieldAccessorContext = {
|
|
296
|
+
renderMarkdown: MarkdownRenderer;
|
|
297
|
+
/** The route this entry resolves to, if the repository was given an `href` builder. */
|
|
298
|
+
href?: string;
|
|
299
|
+
/** The collection the entry was loaded from, when it came from one. */
|
|
300
|
+
collection?: string;
|
|
301
|
+
};
|
|
302
|
+
declare function createFieldAccessor(entry: MarvinContentEntry, context: FieldAccessorContext): {
|
|
303
|
+
/** The entry itself, for anything the accessor doesn't cover. */
|
|
304
|
+
entry: MarvinContentEntry;
|
|
305
|
+
/** The route this entry resolves to (from the repository's `href` option). */
|
|
306
|
+
href: string | undefined;
|
|
307
|
+
collection: string | undefined;
|
|
308
|
+
/** `data_json` → `metadata_json`, untyped. */
|
|
309
|
+
raw(key: string): unknown;
|
|
310
|
+
string(key: string): string | undefined;
|
|
311
|
+
number(key: string): number | undefined;
|
|
312
|
+
/** Booleans authored as strings (`"true"`, `"1"`, `"yes"`) read as booleans. */
|
|
313
|
+
bool(key: string): boolean;
|
|
314
|
+
/** A string array, tolerating a single string authored in place of a list. */
|
|
315
|
+
list(key: string): string[] | undefined;
|
|
316
|
+
/**
|
|
317
|
+
* An enum-guarded read: the value if it's in `allowed`, else `fallback`. Replaces the
|
|
318
|
+
* per-field `normalizeStatus`/`normalizeCategory`/`normalizeTone` guards that every site
|
|
319
|
+
* ends up writing.
|
|
320
|
+
*/
|
|
321
|
+
oneOf<T extends string>(key: string, allowed: readonly T[] | Set<T>, fallback: T): T;
|
|
322
|
+
/** A field read as a display date ("Mon DD, YYYY"); already-formatted values pass through. */
|
|
323
|
+
date(key: string): string | undefined;
|
|
324
|
+
/** The raw ISO publish timestamp, if any. */
|
|
325
|
+
publishedAt(): string | undefined;
|
|
326
|
+
/**
|
|
327
|
+
* Render a markdown field to HTML. Falls back to the entry's `contentMarkdown` when the
|
|
328
|
+
* named field is absent. Returns `undefined` when there is nothing to render, so the
|
|
329
|
+
* caller can omit the property rather than emit an empty string.
|
|
330
|
+
*/
|
|
331
|
+
markdown(key?: string, options?: MarkdownFieldOptions): Promise<string | undefined>;
|
|
332
|
+
/**
|
|
333
|
+
* The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,
|
|
334
|
+
* the exact `preferRoles`, a role/usage match over the entry's image assets, and finally the
|
|
335
|
+
* list item's `featuredAsset`.
|
|
336
|
+
*/
|
|
337
|
+
image(options?: ImageFieldOptions): ApiImage | undefined;
|
|
338
|
+
/** URL of the entry's icon asset (`role: icon`, SVG preferred). */
|
|
339
|
+
icon(options?: IconFieldOptions): string | undefined;
|
|
340
|
+
/** The raw asset placement matching `options` — when you need more than src/alt/focal. */
|
|
341
|
+
asset(options?: AssetSelectOptions): Record<string, unknown> | undefined;
|
|
342
|
+
/** The asset whose role is EXACTLY one of `roles`, in preference order. */
|
|
343
|
+
assetByRole(...roles: string[]): Record<string, unknown> | undefined;
|
|
344
|
+
/** All asset placements on the entry. */
|
|
345
|
+
assets(): Record<string, unknown>[];
|
|
346
|
+
/** Every image asset resolved, filtered by role/usage — support/detail galleries. */
|
|
347
|
+
images(options?: ImageFieldOptions): (ApiImage & {
|
|
348
|
+
usage?: string;
|
|
349
|
+
})[];
|
|
350
|
+
/** Attached resources, normalized to name/type/role/href. */
|
|
351
|
+
resources(options?: ResourceFieldOptions): ApiResourceLink[];
|
|
352
|
+
/** The first attached resource matching `options`. */
|
|
353
|
+
resource(options?: ResourceFieldOptions): ApiResourceLink | undefined;
|
|
354
|
+
/** The `metadata_json` blob. */
|
|
355
|
+
metadata(): Record<string, unknown>;
|
|
356
|
+
/** The `data_json` blob. Empty on a list item that was not hydrated. */
|
|
357
|
+
data(): Record<string, unknown>;
|
|
358
|
+
/** Slugs of every collection the entry belongs to. */
|
|
359
|
+
collections(): string[];
|
|
360
|
+
/** The entry's membership role within `collectionSlug`. */
|
|
361
|
+
role(collectionSlug: string): string | undefined;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The repository factory.
|
|
366
|
+
*
|
|
367
|
+
* `getAll` / `getBySlug` / `getFeatured` is the same forty lines of try / fallback / memoize
|
|
368
|
+
* around every content type on every site. Only the transform differs. This holds the forty
|
|
369
|
+
* lines; the site writes the transform.
|
|
370
|
+
*/
|
|
371
|
+
|
|
372
|
+
type RepositoryOptions<T> = {
|
|
373
|
+
/** Collection slug to load from. */
|
|
374
|
+
collection?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Collection slugs tried in order; the first one that yields entries wins. Lets a site work
|
|
377
|
+
* against workspaces that named the same thing `bench-notes`, `journal`, or `blog`.
|
|
378
|
+
*/
|
|
379
|
+
collections?: string[];
|
|
380
|
+
/**
|
|
381
|
+
* Fetch each list item as a full entry before transforming.
|
|
382
|
+
*
|
|
383
|
+
* The collection endpoint returns `PublishedEntryListItem`, which omits `data_json`; the
|
|
384
|
+
* single-entry endpoint returns `PublishedEntryRead`, which includes it. So if the transform
|
|
385
|
+
* reads ANY schema-defined field — anything not title/slug/summary/metadata — this must be
|
|
386
|
+
* `true` or those fields come back `undefined`. It costs one request per entry.
|
|
387
|
+
*/
|
|
388
|
+
hydrate?: boolean;
|
|
389
|
+
/** Build the resolved item from an entry. May be async (e.g. to render markdown). */
|
|
390
|
+
transform: (entry: MarvinContentEntry, fields: FieldAccessor) => T | Promise<T>;
|
|
391
|
+
/** Static data to serve when Marvin is unreachable or the collection is empty. */
|
|
392
|
+
fallback?: () => T[] | Promise<T[]>;
|
|
393
|
+
/** Applied to Marvin and fallback results alike, so ordering doesn't depend on the source. */
|
|
394
|
+
sort?: (a: T, b: T) => number;
|
|
395
|
+
/** Applied to Marvin and fallback results alike. */
|
|
396
|
+
filter?: (item: T) => boolean;
|
|
397
|
+
/** The route an entry resolves to; exposed to the transform as `fields.href`. */
|
|
398
|
+
href?: (slug: string, entry: MarvinContentEntry) => string;
|
|
399
|
+
/** How to read an item's slug for `bySlug`. Defaults to `item.slug`. */
|
|
400
|
+
slugOf?: (item: T) => string | undefined;
|
|
401
|
+
/** How to tell whether an item is featured. Defaults to `item.featured`. */
|
|
402
|
+
isFeatured?: (item: T) => boolean;
|
|
403
|
+
};
|
|
404
|
+
type Repository<T> = {
|
|
405
|
+
/** Every item, resolved once per process. */
|
|
406
|
+
all(): Promise<T[]>;
|
|
407
|
+
/** One item by slug — a direct entry fetch, falling back to a scan of `all()`. */
|
|
408
|
+
bySlug(slug: string): Promise<T | undefined>;
|
|
409
|
+
/** The first featured item, or the first item when none is marked. */
|
|
410
|
+
featured(): Promise<T | undefined>;
|
|
411
|
+
/** Every featured item, in `all()` order. */
|
|
412
|
+
allFeatured(): Promise<T[]>;
|
|
413
|
+
/** Drop the memoized results. Next call re-fetches. */
|
|
414
|
+
reset(): void;
|
|
415
|
+
};
|
|
416
|
+
type RepositoryContext = {
|
|
417
|
+
fetcher: MarvinFetcher;
|
|
418
|
+
renderMarkdown: MarkdownRenderer;
|
|
419
|
+
};
|
|
420
|
+
declare function createRepository<T>(context: RepositoryContext, options: RepositoryOptions<T>): Repository<T>;
|
|
421
|
+
|
|
422
|
+
type SectionLandingOptions = {
|
|
423
|
+
/** Asset role to use as the hero image. Default `'hero'`. */
|
|
424
|
+
heroRole?: string;
|
|
425
|
+
/** Field holding the intro copy. Default `'body'`, falling back to the entry summary. */
|
|
426
|
+
bodyField?: string;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Resolve a section-landing header from a Marvin `page` entry with the given slug.
|
|
430
|
+
*
|
|
431
|
+
* The intro is rendered with authored line breaks preserved — landing copy is written as a few
|
|
432
|
+
* deliberate lines, not a soft-wrapped paragraph, and losing those breaks changes the layout.
|
|
433
|
+
*/
|
|
434
|
+
declare function loadSectionLanding(fetcher: MarvinFetcher, slug: string, renderMarkdown?: MarkdownRenderer, options?: SectionLandingOptions): Promise<SectionLanding>;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Format a date value for display as "Mon DD, YYYY".
|
|
438
|
+
*
|
|
439
|
+
* ISO dates/timestamps (e.g. a raw `publishedAt` like "2026-07-21T21:22:42.045708Z") are read in
|
|
440
|
+
* UTC — via the date part only, so there's no off-by-one day shift from the viewer's timezone.
|
|
441
|
+
* Values that are already human-readable (e.g. a hand-entered "Jul 02, 2024") pass through
|
|
442
|
+
* unchanged, and empty input returns `undefined` so callers can omit the field.
|
|
443
|
+
*/
|
|
444
|
+
declare function formatDisplayDate(value: string | null | undefined): string | undefined;
|
|
445
|
+
/**
|
|
446
|
+
* Pick `count` items deterministically from `items`, seeded by `pageSlug`.
|
|
447
|
+
*
|
|
448
|
+
* Same page → same selection on every build, different pages → different selections. Use for
|
|
449
|
+
* rotating pull-quotes, value bands, related links: variety across the site without the
|
|
450
|
+
* hydration mismatch a random pick would cause.
|
|
451
|
+
*/
|
|
452
|
+
declare function selectValuesForPage<T>(items: T[], pageSlug: string, count?: number): T[];
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Reading Marvin payloads.
|
|
456
|
+
*
|
|
457
|
+
* The published API returns the same logical value in several shapes depending on which
|
|
458
|
+
* endpoint produced it (list item vs full read, SDK `Entry` wrapper vs raw JSON, placement
|
|
459
|
+
* metadata vs asset metadata). Every reader here is total: it takes `unknown`, checks, and
|
|
460
|
+
* returns `undefined` rather than throwing.
|
|
461
|
+
*/
|
|
462
|
+
|
|
463
|
+
/** A non-empty string, or `undefined`. Empty/whitespace strings are treated as absent. */
|
|
464
|
+
declare function asString(value: unknown): string | undefined;
|
|
465
|
+
declare function asRecord(value: unknown): Record<string, unknown>;
|
|
466
|
+
declare function asNumber(value: unknown): number | undefined;
|
|
467
|
+
declare function asStringArray(value: unknown): string[] | undefined;
|
|
468
|
+
/** `metadata_json` — the free-form blob every entry carries, whatever its schema. */
|
|
469
|
+
declare function entryMetadata(entry: MarvinContentEntry): Record<string, unknown>;
|
|
470
|
+
/** `data_json` — the schema-defined fields. Absent on list items until hydrated. */
|
|
471
|
+
declare function entryData(entry: MarvinContentEntry): Record<string, unknown>;
|
|
472
|
+
/**
|
|
473
|
+
* Read a schema field (`data_json`) from an entry, through the SDK's `Entry` accessor when
|
|
474
|
+
* present and off the raw payload otherwise.
|
|
475
|
+
*/
|
|
476
|
+
declare function entryField<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined;
|
|
477
|
+
/**
|
|
478
|
+
* The precedence rule: structured schema field first (`data_json`), legacy blob second
|
|
479
|
+
* (`metadata_json`).
|
|
480
|
+
*
|
|
481
|
+
* An empty string counts as absent, which matters more than it sounds — an entry type that
|
|
482
|
+
* declares a field the author left blank stores `""`, and without this the legacy value that
|
|
483
|
+
* *is* set would never surface.
|
|
484
|
+
*/
|
|
485
|
+
declare function field<T = unknown>(entry: MarvinContentEntry, key: string): T | undefined;
|
|
486
|
+
/**
|
|
487
|
+
* Read a value off a published asset placement, checking every level the API might have put it
|
|
488
|
+
* at: the placement itself, its metadata blobs, then the underlying asset and its blobs.
|
|
489
|
+
*/
|
|
490
|
+
declare function assetField<T = unknown>(asset: unknown, key: string): T | undefined;
|
|
491
|
+
/** Same layered read as {@link assetField}, for resource relationships. */
|
|
492
|
+
declare function resourceField<T = unknown>(resource: unknown, key: string): T | undefined;
|
|
493
|
+
/**
|
|
494
|
+
* The entry's collection memberships as slugs. Collections arrive as bare slugs on list items,
|
|
495
|
+
* flat `{slug}` objects, or the SDK's nested `{collection: {slug}}` — all three are read here.
|
|
496
|
+
*/
|
|
497
|
+
declare function collectionSlugs(entry: MarvinContentEntry): string[];
|
|
498
|
+
/**
|
|
499
|
+
* The entry's membership role within one specific collection (e.g. `'legal'` in
|
|
500
|
+
* `footer-navigation`). A hydrated entry carries all of its memberships; this finds the one for
|
|
501
|
+
* the given collection.
|
|
502
|
+
*/
|
|
503
|
+
declare function collectionRole(entry: MarvinContentEntry, collectionSlug: string): string | undefined;
|
|
504
|
+
/**
|
|
505
|
+
* A CSS `object-position` value from a focal point. Marvin stores these either pre-formatted
|
|
506
|
+
* (`"40% 60%"`) or as `{x, y}` in 0–1 or 0–100 units.
|
|
507
|
+
*/
|
|
508
|
+
declare function focalPoint(value: unknown): string | undefined;
|
|
509
|
+
declare function entryAssets(entry: MarvinContentEntry): Record<string, unknown>[];
|
|
510
|
+
declare function entryResources(entry: MarvinContentEntry): Record<string, unknown>[];
|
|
511
|
+
declare function selectEntryAsset(entry: MarvinContentEntry, options?: AssetSelectOptions): Record<string, unknown> | undefined;
|
|
512
|
+
/**
|
|
513
|
+
* Match on role ALONE, exactly. `selectEntryAsset` ORs role against a vacuously-true usage
|
|
514
|
+
* check, so `{roles: ['hero-grade']}` there matches the entry's first asset whatever its role.
|
|
515
|
+
* When you mean "the asset whose role is exactly this", use this.
|
|
516
|
+
*/
|
|
517
|
+
declare function selectAssetByRole(entry: MarvinContentEntry, ...roles: string[]): Record<string, unknown> | undefined;
|
|
518
|
+
/** The list-item shorthand: `featuredAsset` is present on collection entries. */
|
|
519
|
+
declare function selectFeaturedAsset(entry: MarvinContentEntry): Record<string, unknown> | undefined;
|
|
520
|
+
declare function selectImageAsset(entry: MarvinContentEntry, options?: Omit<AssetSelectOptions, 'type' | 'types'>): Record<string, unknown> | undefined;
|
|
521
|
+
declare function selectIconAsset(entry: MarvinContentEntry): Record<string, unknown> | undefined;
|
|
522
|
+
declare function assetUrl(asset: unknown): string | undefined;
|
|
523
|
+
declare function assetAlt(asset: unknown, fallback?: string): string | undefined;
|
|
524
|
+
declare function resourceRole(resource: unknown): string | undefined;
|
|
525
|
+
declare function isExternalHref(href: string): boolean;
|
|
526
|
+
|
|
527
|
+
type MarvinContentOptions = MarvinAstroConfig & {
|
|
528
|
+
site?: SiteOptions;
|
|
529
|
+
chrome?: ChromeOptions;
|
|
530
|
+
};
|
|
531
|
+
type MarvinContent = ReturnType<typeof createMarvinContent>;
|
|
532
|
+
/**
|
|
533
|
+
* Build the content layer for one Marvin workspace.
|
|
534
|
+
*
|
|
535
|
+
* Connection settings come from `MARVIN_API_URL`, `MARVIN_SITE_CLIENT_TOKEN` and
|
|
536
|
+
* `MARVIN_WORKSPACE_SLUG` unless passed explicitly. Everything returned degrades to its static
|
|
537
|
+
* fallback when the backend is unreachable, so a site still builds offline.
|
|
538
|
+
*/
|
|
539
|
+
declare function createMarvinContent(options?: MarvinContentOptions): {
|
|
540
|
+
/** The resolved configuration, with the token still in place — don't log it. */
|
|
541
|
+
config: ResolvedConfig;
|
|
542
|
+
/** Latch control and the raw SDK client. */
|
|
543
|
+
backend: MarvinBackend;
|
|
544
|
+
/** Guarded, never-throwing wrappers around every published endpoint. */
|
|
545
|
+
fetch: {
|
|
546
|
+
backend: MarvinBackend;
|
|
547
|
+
collections(): Promise<_inneropen_marvin_sdk.PublishedCollectionSummary[]>;
|
|
548
|
+
collection(slug: string): Promise<_inneropen_marvin_sdk.Collection | null>;
|
|
549
|
+
collectionEntries(slug: string): Promise<_inneropen_marvin_sdk.CollectionEntry[]>;
|
|
550
|
+
collectionEntriesFallback(slugs: string[]): Promise<_inneropen_marvin_sdk.CollectionEntry[]>;
|
|
551
|
+
hydratedCollectionEntries(slug: string): Promise<HydratedEntry[]>;
|
|
552
|
+
hydratedCollectionEntriesFallback(slugs: string[]): Promise<HydratedEntry[]>;
|
|
553
|
+
hydrate: (entries: MarvinContentEntry[]) => Promise<HydratedEntry[]>;
|
|
554
|
+
entry: (slug: string) => Promise<_inneropen_marvin_sdk.Entry | null>;
|
|
555
|
+
site(): Promise<_inneropen_marvin_sdk.MarvinSite | null>;
|
|
556
|
+
workspace(): Promise<_inneropen_marvin_sdk.Workspace | null>;
|
|
557
|
+
assets(type?: string): Promise<_inneropen_marvin_sdk.MarvinAsset[]>;
|
|
558
|
+
asset(slugOrId: string): Promise<_inneropen_marvin_sdk.MarvinAsset | null>;
|
|
559
|
+
resources(resourceType?: string): Promise<_inneropen_marvin_sdk.MarvinResource[]>;
|
|
560
|
+
};
|
|
561
|
+
renderMarkdown: MarkdownRenderer;
|
|
562
|
+
/** True when Marvin is configured and not currently latched out as unreachable. */
|
|
563
|
+
hasBackend: () => boolean;
|
|
564
|
+
/** Resolved site identity, SEO and brand assets. Memoized per process. */
|
|
565
|
+
getSite(): Promise<ApiSite>;
|
|
566
|
+
/** Navigation, footer, legal, social and inquiry links. Memoized per process. */
|
|
567
|
+
getSiteChrome(): Promise<ApiSiteChrome>;
|
|
568
|
+
/** Title/intro/hero for an index page, driven by a `page` entry of the same slug. */
|
|
569
|
+
getSectionLanding(slug: string, sectionOptions?: SectionLandingOptions): Promise<SectionLanding>;
|
|
570
|
+
/** A collection-backed content repository: `all()` / `bySlug()` / `featured()`. */
|
|
571
|
+
repository<T>(repositoryOptions: RepositoryOptions<T>): Repository<T>;
|
|
572
|
+
/** Drop every memoized result and re-open the failure latch. */
|
|
573
|
+
reset(): void;
|
|
574
|
+
};
|
|
575
|
+
|
|
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 };
|