@elytracms/next 0.0.94 → 0.0.95

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)
@@ -307,10 +312,14 @@ interface StaticContentData {
307
312
  documents?: readonly ContentDocumentSource[];
308
313
  assets?: readonly AssetRecord[];
309
314
  /**
310
- * Resolve a serve URL for assets without a stored `url`. Defaults to an
311
- * explicit unresolved-placeholder scheme (never a silent empty string).
315
+ * Resolve a serve URL for assets without a stored `url` (`undefined` when there is
316
+ * none). EC-633: no default — an asset with no URL is no resolved asset (the ONE
317
+ * constructor, `resolvedAsset`, decides it): it renders the canvas's empty state, its
318
+ * field resolves to `null` with an `asset-without-url` issue, and this source names it
319
+ * on `sourceIssues`. The placeholder this used to default to (`asset://unresolved/…`)
320
+ * rendered a broken image on the site.
312
321
  */
313
- assetUrl?: (asset: AssetRecord) => string;
322
+ assetUrl?: (asset: AssetRecord) => string | undefined;
314
323
  /**
315
324
  * EC-585 — the stored rows the host could not read (`parseDeliveredDocuments`), handed
316
325
  * to every client of this source as its `sourceIssues` (and in the results they
@@ -1991,6 +2000,12 @@ interface LiveContentOptions {
1991
2000
  interface DeliverySnapshotPayload {
1992
2001
  projectId: string;
1993
2002
  redirects?: unknown[];
2003
+ /**
2004
+ * EC-628 — the stored redirects the deployment withheld because it could not read them
2005
+ * (`invalid-redirect` issues from the one live rule). Absent from a deployment older than
2006
+ * EC-628, which serves such rows in `redirects` instead (and this host names them there).
2007
+ */
2008
+ redirectIssues?: unknown[];
1994
2009
  documents: unknown[];
1995
2010
  assets: unknown[];
1996
2011
  }
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, 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 { 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,12 +136,16 @@ 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 sourceIssues = [
140
+ ...data.sourceIssues ?? [],
141
+ ...[...assets.values()].filter((record2) => !assetUrl(record2)).map((record2) => assetWithoutUrlIssue(record2.id))
142
+ ];
139
143
  const lookupFor = (_perspective) => ({
140
144
  collection: (id) => collections.get(id),
141
145
  document: (ref) => documents.get(documentKey(ref)),
142
146
  documents: (collectionId) => byCollection.get(collectionId) ?? [],
143
147
  asset: (id) => assets.get(id),
144
- assetUrl: (asset) => asset.url ?? assetUrl(asset)
148
+ assetUrl
145
149
  });
146
150
  const client = (init) => createContentClient({
147
151
  lookup: lookupFor(init.perspective),
@@ -152,7 +156,7 @@ function createStaticContentSource(data) {
152
156
  }),
153
157
  routes,
154
158
  redirects,
155
- ...data.sourceIssues ? { sourceIssues: data.sourceIssues } : {}
159
+ ...sourceIssues.length > 0 ? { sourceIssues } : {}
156
160
  });
157
161
  return {
158
162
  projectId: data.projectId,
@@ -163,7 +167,14 @@ function createStaticContentSource(data) {
163
167
  // `asset`-typed Image prop (an asset id) to a usable url + intrinsic
164
168
  // dimensions. Baked into the (serializable) render outcome by the route
165
169
  // helper, so a cached page keeps its images during a backend outage.
166
- assets: () => [...assets.values()].map((record2) => resolvedAsset(record2, record2.url ?? assetUrl(record2))),
170
+ // EC-632: in the requested locale (the route helper passes the request's), so a
171
+ // localized alt is baked in the locale the page renders in.
172
+ // EC-633: a record with no URL is no resolved asset — not baked, so the page renders
173
+ // the empty state the canvas renders for it (and `sourceIssues` names it).
174
+ assets: (locale) => [...assets.values()].flatMap((record2) => {
175
+ const asset = resolvedAsset(record2, assetUrl(record2), assetReadLocale(locales, locale));
176
+ return asset ? [asset] : [];
177
+ }),
167
178
  // EC-499: the schema the route helper reads a collection's declared
168
179
  // `presentation` from (which field is the delivery body, which component
169
180
  // wraps it). Already prefetched for the lookup above — exposing it costs
@@ -175,8 +186,16 @@ async function createContentSnapshot(adapter, projectId, options) {
175
186
  const documentRecords = await adapter.cms.listDocuments(projectId);
176
187
  const versionRecords = await adapter.cms.listAllDocumentVersions(projectId);
177
188
  const assetRecords = await adapter.assets.listAssets(projectId);
189
+ const blobs = adapter.blobs;
190
+ const servedUrls = /* @__PURE__ */ new Map();
191
+ await Promise.all(
192
+ assetRecords.filter((record2) => !record2.url).map(async (record2) => {
193
+ const url = blobs.resolveUrl ? await blobs.resolveUrl(record2.storageKey).catch(() => void 0) : blobs.url(record2.storageKey);
194
+ servedUrls.set(record2.id, url || void 0);
195
+ })
196
+ );
178
197
  const redirectRecords = await adapter.records(redirectRecordKind).list(projectId);
179
- const redirects = liveRedirectRecords(redirectRecords);
198
+ const { redirects, issues: redirectIssues } = readLiveRedirects(redirectRecords);
180
199
  const versionsByKey = /* @__PURE__ */ new Map();
181
200
  for (const record2 of versionRecords) {
182
201
  const key = `${record2.collection}:${record2.docId}`;
@@ -194,13 +213,15 @@ async function createContentSnapshot(adapter, projectId, options) {
194
213
  });
195
214
  return createStaticContentSource({
196
215
  projectId,
216
+ // EC-628: only for the stored redirects this source serves (an explicit override replaces them).
217
+ ...options.redirects === void 0 && redirectIssues.length > 0 ? { sourceIssues: redirectIssues } : {},
197
218
  locales: options.locales,
198
219
  ...options.routes ? { routes: options.routes } : {},
199
220
  redirects: options.redirects ? [...options.redirects] : redirects,
200
221
  collections: options.collections,
201
222
  documents,
202
223
  assets: assetRecords,
203
- assetUrl: (asset) => adapter.blobs.url(asset.storageKey)
224
+ assetUrl: (asset) => servedUrls.get(asset.id)
204
225
  });
205
226
  }
206
227
  function seoContextOf(result, assets = []) {
@@ -410,7 +431,7 @@ async function resolveCanvasPage(source, url, perspective, options = {}) {
410
431
  }
411
432
  const presentation = presentationFor(source, result);
412
433
  const body = pageBodyOf(result, presentation);
413
- const allAssets = source.assets?.() ?? [];
434
+ const allAssets = source.assets?.(locale) ?? [];
414
435
  const bakedAssets = options.registry ? scopeAssetsToPage(
415
436
  allAssets,
416
437
  options.registry,
@@ -1083,17 +1104,11 @@ async function runConvexQuery(fetchImpl, convexUrl, path, args) {
1083
1104
  }
1084
1105
  return body.value;
1085
1106
  }
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
1107
  function assembleLiveSource(payload, options, perspective) {
1095
1108
  const locales = options.locales;
1096
- const { documents, issues: sourceIssues } = parseDeliveredDocuments(payload.documents);
1109
+ const { documents, issues: documentIssues } = parseDeliveredDocuments(payload.documents);
1110
+ const delivered = parseDeliveredRedirects(payload.redirects ?? [], payload.redirectIssues ?? []);
1111
+ const sourceIssues = [...documentIssues, ...delivered.issues];
1097
1112
  const routes = [...options.routes];
1098
1113
  const documentSources = perspective === "published" ? documents.map((doc) => ({ draft: doc, published: doc, versions: [] })) : documents;
1099
1114
  return createStaticContentSource({
@@ -1103,7 +1118,7 @@ function assembleLiveSource(payload, options, perspective) {
1103
1118
  // Redirects are editorial DATA (EC-303): the ENABLED ones ship in the
1104
1119
  // snapshot, so they change dynamically (no redeploy). Native next.config
1105
1120
  // redirects run before this.
1106
- redirects: parseRedirects(payload.redirects ?? []),
1121
+ redirects: delivered.redirects,
1107
1122
  collections: [...options.collections],
1108
1123
  documents: documentSources,
1109
1124
  assets: payload.assets,
@@ -1159,7 +1174,7 @@ function resolveGlobals(source, options = {}) {
1159
1174
  const navigationCollection = conventions.navigationCollection ?? "navigation";
1160
1175
  const itemsFields = conventions.navigationItemsFields ?? DEFAULT_ITEMS_FIELDS;
1161
1176
  const client = source.client({ perspective, ...options.locale ? { locale: options.locale } : {} });
1162
- const assets = source.assets?.() ?? [];
1177
+ const assets = source.assets?.(options.locale) ?? [];
1163
1178
  const assetById = new Map(assets.map((asset) => [asset.id, asset]));
1164
1179
  const listDocs = (collection) => {
1165
1180
  const result = client.listDocuments(collection);