@elytracms/next 0.0.94 → 0.0.96

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/dist/index.d.ts CHANGED
@@ -227,8 +227,13 @@ interface ContentSource {
227
227
  * backend outage. Optional so existing custom sources stay valid; without it,
228
228
  * asset-typed props fall back to their raw value (the renderer's visible
229
229
  * degradation), never a crash.
230
+ *
231
+ * EC-632: resolved in `locale` (absent → the source's default locale) — a localized
232
+ * asset-collection field (alt, …) carries its value in that locale, with the default
233
+ * locale as its fallback. The route helper passes the request's locale, so a page bakes
234
+ * its assets in the locale it renders in.
230
235
  */
231
- assets?(): readonly ResolvedAsset[];
236
+ assets?(locale?: Locale): readonly ResolvedAsset[];
232
237
  }
233
238
  /**
234
239
  * Merge accessor cache tags into one deterministic set (sorted, deduplicated)
@@ -296,6 +301,13 @@ interface StaticContentData {
296
301
  routes?: readonly RouteRecord[];
297
302
  /** Redirect records served by `resolvePage` (EC-019/020). */
298
303
  redirects?: readonly RedirectRecord[];
304
+ /**
305
+ * The project's collection definitions (every host path hands the resolved config's). They
306
+ * are also the asset metadata's lens (EC-659): the asset collection's `localized` fields
307
+ * are the only ones a stored per-locale value is read for. ABSENT means no schema — no
308
+ * field is localized, so every stored translation is drift: never applied (the shared
309
+ * value is served), each one named on `sourceIssues`.
310
+ */
299
311
  collections?: readonly CollectionDef[];
300
312
  /**
301
313
  * Stored document sources. Either a bare `CmsDocument` (gated by its own
@@ -307,10 +319,14 @@ interface StaticContentData {
307
319
  documents?: readonly ContentDocumentSource[];
308
320
  assets?: readonly AssetRecord[];
309
321
  /**
310
- * Resolve a serve URL for assets without a stored `url`. Defaults to an
311
- * explicit unresolved-placeholder scheme (never a silent empty string).
322
+ * Resolve a serve URL for assets without a stored `url` (`undefined` when there is
323
+ * none). EC-633: no default — an asset with no URL is no resolved asset (the ONE
324
+ * constructor, `resolvedAsset`, decides it): it renders the canvas's empty state, its
325
+ * field resolves to `null` with an `asset-without-url` issue, and this source names it
326
+ * on `sourceIssues`. The placeholder this used to default to (`asset://unresolved/…`)
327
+ * rendered a broken image on the site.
312
328
  */
313
- assetUrl?: (asset: AssetRecord) => string;
329
+ assetUrl?: (asset: AssetRecord) => string | undefined;
314
330
  /**
315
331
  * EC-585 — the stored rows the host could not read (`parseDeliveredDocuments`), handed
316
332
  * to every client of this source as its `sourceIssues` (and in the results they
@@ -1991,6 +2007,12 @@ interface LiveContentOptions {
1991
2007
  interface DeliverySnapshotPayload {
1992
2008
  projectId: string;
1993
2009
  redirects?: unknown[];
2010
+ /**
2011
+ * EC-628 — the stored redirects the deployment withheld because it could not read them
2012
+ * (`invalid-redirect` issues from the one live rule). Absent from a deployment older than
2013
+ * EC-628, which serves such rows in `redirects` instead (and this host names them there).
2014
+ */
2015
+ redirectIssues?: unknown[];
1994
2016
  documents: unknown[];
1995
2017
  assets: unknown[];
1996
2018
  }
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { registerComponents, createRenderContext, renderDocumentBodyItems, documentBodyItems, serializeListingQuery, documentBodyRichTextValues, documentBodyCompositionNodes, richTextEmbedsInValue, collectReferencedAssetIds, collectRichTextRefsInValue, collectReferencedRelations, collectReferencedListings, collectReferencedRichTextLinks, listingRequestOf } from '@elytracms/core/runtime-renderer';
2
2
  import { isBlockSequenceValueShape } from '@elytracms/core/rich-text';
3
3
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
- import { cacheTags, isDocumentHistory, resolvedAsset, parseDeliveredDocuments, createContentClient, createContentContext } from '@elytracms/core/content';
5
- import { DEFAULT_SEO_FIELD, parseLocaleConfig, documentKey, liveRedirectRecords, SEO_SUBFIELDS, splitLocalePath, DRAFT_PERSPECTIVE, splitPath, localeUrlPath, variantOf, DRAFT_VARIANT, createRouter, presentationBodyFields, redirectRecordSchema } from '@elytracms/core/cms-core';
4
+ import { cacheTags, isDocumentHistory, assetWithoutUrlIssue, assetMetaDriftIssue, resolvedAsset, parseDeliveredDocuments, parseDeliveredRedirects, createContentClient, createContentContext } from '@elytracms/core/content';
5
+ import { DEFAULT_SEO_FIELD, parseLocaleConfig, documentKey, readLiveRedirects, SEO_SUBFIELDS, splitLocalePath, DRAFT_PERSPECTIVE, splitPath, localeUrlPath, variantOf, DRAFT_VARIANT, createRouter, presentationBodyFields } from '@elytracms/core/cms-core';
6
6
  export { DRAFT_PERSPECTIVE, documentSchema, localeConfigSchema, localeUrlPath, parseLocaleConfig, redirectRecordSchema, resolveLocale, routeRecordSchema, splitLocalePath, textDirection, variantOf } from '@elytracms/core/cms-core';
7
- import { redirectRecordKind } from '@elytracms/core/persistence';
7
+ import { assetMetaLens, assetMetaDrift, assetReadLocale, redirectRecordKind } from '@elytracms/core/persistence';
8
8
  import { revalidateTag, cacheTag } from 'next/cache';
9
9
  import { draftMode } from 'next/headers';
10
10
  import { redirect, notFound, permanentRedirect } from 'next/navigation';
@@ -122,7 +122,7 @@ function createStaticContentSource(data) {
122
122
  const locales = parseLocaleConfig(data.locales);
123
123
  const routes = data.routes ?? [];
124
124
  const redirects = data.redirects ?? [];
125
- const assetUrl = data.assetUrl ?? ((asset) => `asset://unresolved/${asset.storageKey}`);
125
+ const assetUrl = (asset) => asset.url ?? data.assetUrl?.(asset);
126
126
  const collections = /* @__PURE__ */ new Map();
127
127
  for (const collection of data.collections ?? []) collections.set(collection.id, collection);
128
128
  const documents = /* @__PURE__ */ new Map();
@@ -136,23 +136,32 @@ function createStaticContentSource(data) {
136
136
  }
137
137
  const assets = /* @__PURE__ */ new Map();
138
138
  for (const record2 of data.assets ?? []) assets.set(record2.id, record2);
139
+ const metaLens = assetMetaLens(data.collections ?? []);
140
+ const sourceIssues = [
141
+ ...data.sourceIssues ?? [],
142
+ ...[...assets.values()].filter((record2) => !assetUrl(record2)).map((record2) => assetWithoutUrlIssue(record2.id)),
143
+ ...[...assets.values()].flatMap(
144
+ (record2) => assetMetaDrift(record2, metaLens).map((drift) => assetMetaDriftIssue(record2.id, drift))
145
+ )
146
+ ];
139
147
  const lookupFor = (_perspective) => ({
140
148
  collection: (id) => collections.get(id),
141
149
  document: (ref) => documents.get(documentKey(ref)),
142
150
  documents: (collectionId) => byCollection.get(collectionId) ?? [],
143
151
  asset: (id) => assets.get(id),
144
- assetUrl: (asset) => asset.url ?? assetUrl(asset)
152
+ assetUrl
145
153
  });
146
154
  const client = (init) => createContentClient({
147
155
  lookup: lookupFor(init.perspective),
148
156
  context: createContentContext({
149
157
  locale: init.locale ?? locales.default,
150
158
  perspective: init.perspective,
151
- locales
159
+ locales,
160
+ assetMetaLens: metaLens
152
161
  }),
153
162
  routes,
154
163
  redirects,
155
- ...data.sourceIssues ? { sourceIssues: data.sourceIssues } : {}
164
+ ...sourceIssues.length > 0 ? { sourceIssues } : {}
156
165
  });
157
166
  return {
158
167
  projectId: data.projectId,
@@ -163,7 +172,14 @@ function createStaticContentSource(data) {
163
172
  // `asset`-typed Image prop (an asset id) to a usable url + intrinsic
164
173
  // dimensions. Baked into the (serializable) render outcome by the route
165
174
  // helper, so a cached page keeps its images during a backend outage.
166
- assets: () => [...assets.values()].map((record2) => resolvedAsset(record2, record2.url ?? assetUrl(record2))),
175
+ // EC-632: in the requested locale (the route helper passes the request's), so a
176
+ // localized alt is baked in the locale the page renders in.
177
+ // EC-633: a record with no URL is no resolved asset — not baked, so the page renders
178
+ // the empty state the canvas renders for it (and `sourceIssues` names it).
179
+ assets: (locale) => [...assets.values()].flatMap((record2) => {
180
+ const asset = resolvedAsset(record2, assetUrl(record2), assetReadLocale(locales, metaLens, locale));
181
+ return asset ? [asset] : [];
182
+ }),
167
183
  // EC-499: the schema the route helper reads a collection's declared
168
184
  // `presentation` from (which field is the delivery body, which component
169
185
  // wraps it). Already prefetched for the lookup above — exposing it costs
@@ -175,8 +191,16 @@ async function createContentSnapshot(adapter, projectId, options) {
175
191
  const documentRecords = await adapter.cms.listDocuments(projectId);
176
192
  const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
177
193
  const assetRecords = await adapter.assets.listAssets(projectId);
194
+ const blobs = adapter.blobs;
195
+ const servedUrls = /* @__PURE__ */ new Map();
196
+ await Promise.all(
197
+ assetRecords.filter((record2) => !record2.url).map(async (record2) => {
198
+ const url = blobs.resolveUrl ? await blobs.resolveUrl(record2.storageKey).catch(() => void 0) : blobs.url(record2.storageKey);
199
+ servedUrls.set(record2.id, url || void 0);
200
+ })
201
+ );
178
202
  const redirectRecords = await adapter.records(redirectRecordKind).list(projectId);
179
- const redirects = liveRedirectRecords(redirectRecords);
203
+ const { redirects, issues: redirectIssues } = readLiveRedirects(redirectRecords);
180
204
  const versionsByKey = /* @__PURE__ */ new Map();
181
205
  for (const record2 of versionRecords) {
182
206
  const key = `${record2.collection}:${record2.docId}`;
@@ -194,13 +218,15 @@ async function createContentSnapshot(adapter, projectId, options) {
194
218
  });
195
219
  return createStaticContentSource({
196
220
  projectId,
221
+ // EC-628: only for the stored redirects this source serves (an explicit override replaces them).
222
+ ...options.redirects === void 0 && redirectIssues.length > 0 ? { sourceIssues: redirectIssues } : {},
197
223
  locales: options.locales,
198
224
  ...options.routes ? { routes: options.routes } : {},
199
225
  redirects: options.redirects ? [...options.redirects] : redirects,
200
226
  collections: options.collections,
201
227
  documents,
202
228
  assets: assetRecords,
203
- assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
229
+ assetUrl: (asset) => servedUrls.get(asset.id)
204
230
  });
205
231
  }
206
232
  function seoContextOf(result, assets = []) {
@@ -410,7 +436,7 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
410
436
  }
411
437
  const presentation = presentationFor(source, result);
412
438
  const body = pageBodyOf(result, presentation);
413
- const allAssets = source.assets?.() ?? [];
439
+ const allAssets = source.assets?.(locale) ?? [];
414
440
  const bakedAssets = options.registry ? scopeAssetsToPage(
415
441
  allAssets,
416
442
  options.registry,
@@ -1083,17 +1109,11 @@ async function runConvexQuery(fetchImpl, convexUrl, path, args) {
1083
1109
  }
1084
1110
  return body.value;
1085
1111
  }
1086
- function parseRedirects(rows) {
1087
- const redirects = [];
1088
- for (const row of rows) {
1089
- const parsed = redirectRecordSchema.safeParse(row);
1090
- if (parsed.success) redirects.push(parsed.data);
1091
- }
1092
- return redirects;
1093
- }
1094
1112
  function assembleLiveSource(payload, options, perspective) {
1095
1113
  const locales = options.locales;
1096
- const { documents, issues: sourceIssues } = parseDeliveredDocuments(payload.documents);
1114
+ const { documents, issues: documentIssues } = parseDeliveredDocuments(payload.documents);
1115
+ const delivered = parseDeliveredRedirects(payload.redirects ?? [], payload.redirectIssues ?? []);
1116
+ const sourceIssues = [...documentIssues, ...delivered.issues];
1097
1117
  const routes = [...options.routes];
1098
1118
  const documentSources = perspective === "published" ? documents.map((doc) => ({ draft: doc, published: doc, versions: [] })) : documents;
1099
1119
  return createStaticContentSource({
@@ -1103,7 +1123,7 @@ function assembleLiveSource(payload, options, perspective) {
1103
1123
  // Redirects are editorial DATA (EC-303): the ENABLED ones ship in the
1104
1124
  // snapshot, so they change dynamically (no redeploy). Native next.config
1105
1125
  // redirects run before this.
1106
- redirects: parseRedirects(payload.redirects ?? []),
1126
+ redirects: delivered.redirects,
1107
1127
  collections: [...options.collections],
1108
1128
  documents: documentSources,
1109
1129
  assets: payload.assets,
@@ -1159,7 +1179,7 @@ function resolveGlobals(source, options = {}) {
1159
1179
  const navigationCollection = conventions.navigationCollection ?? "navigation";
1160
1180
  const itemsFields = conventions.navigationItemsFields ?? DEFAULT_ITEMS_FIELDS;
1161
1181
  const client = source.client({ perspective, ...options.locale ? { locale: options.locale } : {} });
1162
- const assets = source.assets?.() ?? [];
1182
+ const assets = source.assets?.(options.locale) ?? [];
1163
1183
  const assetById = new Map(assets.map((asset) => [asset.id, asset]));
1164
1184
  const listDocs = (collection) => {
1165
1185
  const result = client.listDocuments(collection);