@scalemule/news-publication 0.0.83 → 0.0.84

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 CHANGED
@@ -324,7 +324,7 @@ The publisher-supplied `PawMark` preserves the approved SVG paths. Hosts may set
324
324
 
325
325
  ### Homepage image identity
326
326
 
327
- `NewsItem.visual_identity` is a versioned public identity from the Blog feed. `composeEdition` allocates covers across the entire bounded inventory, gives the selected lead priority, and holds repeated covers as `null` without dropping stories or mutating inputs. Never re-add duplicate images as fallback. Legacy Photo rendition URLs are normalized by tenant/asset ID; server-side Photo identity detects copies under different IDs and ordinary resize/re-encoding/crop variants. Do not fetch or decode images in page components. Preserve existing caption/credit policy. See platform `docs/HOMEPAGE-PHOTO-IDENTITY.md` for storage, rights, concurrency and rollout tests.
327
+ `NewsItem.visual_identity` is a versioned public identity from the Blog feed. `composeEdition` allocates covers across the entire bounded inventory and holds repeated covers as `null` without mutating inputs. Lead, secondary, compact and section image placements require a usable distinct cover, including pinned stories. A held lead yields to the first ready story; an entirely unready inventory has no visual lead. Intentional briefs retain their editorial selection rules. `pendingImages` and the complete `navigation` inventory retain unresolved stories for repair and discovery: excluding them from image placements does not mean their photos were repaired. Consumer planners must not refill image placements from raw inventory or reintroduce duplicate covers. Legacy Photo rendition URLs are normalized by tenant/asset ID; server-side Photo identity detects copies under different IDs and ordinary resize/re-encoding/crop variants. Do not fetch or decode images in page components. Preserve existing caption/credit policy. See platform `docs/HOMEPAGE-PHOTO-IDENTITY.md` for storage, rights, concurrency and rollout tests.
328
328
 
329
329
  ### Standard article submission
330
330
 
package/dist/index.cjs CHANGED
@@ -188,7 +188,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
188
188
  return true;
189
189
  });
190
190
  const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
191
- const selectedLead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
191
+ const selectedLead = inventory.find((item) => item.slug === pins.lead_slug && coverIdentity(item)) ?? inventory.find((item) => coverIdentity(item));
192
192
  const rest = inventory.filter((item) => item !== selectedLead);
193
193
  const requested = new Set(pins.secondary_slugs ?? []);
194
194
  const ordered = [
@@ -197,13 +197,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
197
197
  ];
198
198
  const originalOrder = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
199
199
  const seenImages = /* @__PURE__ */ new Set();
200
- const duplicateImages = /* @__PURE__ */ new Set();
201
200
  const display = /* @__PURE__ */ new Map();
202
201
  for (const item of [selectedLead, ...originalOrder]) {
203
202
  if (!item) continue;
204
203
  const key = coverIdentity(item);
205
204
  if (key && seenImages.has(key)) {
206
- duplicateImages.add(item.id);
207
205
  display.set(item.id, { ...item, cover_image_url: null });
208
206
  } else {
209
207
  display.set(item.id, item);
@@ -214,14 +212,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
214
212
  const lead = selectedLead ? display.get(selectedLead.id) : void 0;
215
213
  let remaining = rest.map((item) => display.get(item.id));
216
214
  const uniqueOrdered = originalOrder.map((item) => display.get(item.id));
217
- const secondary = [];
218
- const deferred = [];
219
- for (const item of uniqueOrdered) {
220
- if (secondary.length >= 3) break;
221
- if (duplicateImages.has(item.id)) deferred.push(item);
222
- else secondary.push(item);
223
- }
224
- while (secondary.length < 3 && deferred.length > 0) secondary.push(deferred.shift());
215
+ const secondary = uniqueOrdered.filter((item) => coverIdentity(item)).slice(0, 3);
225
216
  const secondaryIds = new Set(secondary.map((item) => item.id));
226
217
  remaining = remaining.filter((item) => !secondaryIds.has(item.id));
227
218
  const eligible2 = remaining.filter((item) => {
@@ -236,10 +227,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
236
227
  briefIds.add(item.id);
237
228
  return true;
238
229
  });
239
- const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
230
+ const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].filter((item) => coverIdentity(item)).slice(0, 4);
240
231
  const compactIds = new Set(compact.map((item) => item.id));
241
- const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
242
- return { lead, secondary, briefs, compact, sections: sections2, navigation: editorialSections(inventory) };
232
+ const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id) && coverIdentity(item)));
233
+ const pendingImages = inventory.filter((item) => !coverIdentity(item));
234
+ return { lead, secondary, briefs, compact, sections: sections2, pendingImages, navigation: editorialSections(inventory) };
243
235
  }
244
236
  function displayHeadline(article, selection = {}) {
245
237
  return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
package/dist/index.d.cts CHANGED
@@ -246,6 +246,7 @@ declare function composeEdition(items: NewsItem[], selection?: HomepageSelection
246
246
  briefs: NewsItem[];
247
247
  compact: NewsItem[];
248
248
  sections: EditorialSection[];
249
+ pendingImages: NewsItem[];
249
250
  navigation: EditorialSection[];
250
251
  };
251
252
  declare function displayHeadline(article: NewsItem, selection?: HomepageSelection): string;
package/dist/index.d.ts CHANGED
@@ -246,6 +246,7 @@ declare function composeEdition(items: NewsItem[], selection?: HomepageSelection
246
246
  briefs: NewsItem[];
247
247
  compact: NewsItem[];
248
248
  sections: EditorialSection[];
249
+ pendingImages: NewsItem[];
249
250
  navigation: EditorialSection[];
250
251
  };
251
252
  declare function displayHeadline(article: NewsItem, selection?: HomepageSelection): string;
package/dist/index.js CHANGED
@@ -132,7 +132,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
132
132
  return true;
133
133
  });
134
134
  const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
135
- const selectedLead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
135
+ const selectedLead = inventory.find((item) => item.slug === pins.lead_slug && coverIdentity(item)) ?? inventory.find((item) => coverIdentity(item));
136
136
  const rest = inventory.filter((item) => item !== selectedLead);
137
137
  const requested = new Set(pins.secondary_slugs ?? []);
138
138
  const ordered = [
@@ -141,13 +141,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
141
141
  ];
142
142
  const originalOrder = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
143
143
  const seenImages = /* @__PURE__ */ new Set();
144
- const duplicateImages = /* @__PURE__ */ new Set();
145
144
  const display = /* @__PURE__ */ new Map();
146
145
  for (const item of [selectedLead, ...originalOrder]) {
147
146
  if (!item) continue;
148
147
  const key = coverIdentity(item);
149
148
  if (key && seenImages.has(key)) {
150
- duplicateImages.add(item.id);
151
149
  display.set(item.id, { ...item, cover_image_url: null });
152
150
  } else {
153
151
  display.set(item.id, item);
@@ -158,14 +156,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
158
156
  const lead = selectedLead ? display.get(selectedLead.id) : void 0;
159
157
  let remaining = rest.map((item) => display.get(item.id));
160
158
  const uniqueOrdered = originalOrder.map((item) => display.get(item.id));
161
- const secondary = [];
162
- const deferred = [];
163
- for (const item of uniqueOrdered) {
164
- if (secondary.length >= 3) break;
165
- if (duplicateImages.has(item.id)) deferred.push(item);
166
- else secondary.push(item);
167
- }
168
- while (secondary.length < 3 && deferred.length > 0) secondary.push(deferred.shift());
159
+ const secondary = uniqueOrdered.filter((item) => coverIdentity(item)).slice(0, 3);
169
160
  const secondaryIds = new Set(secondary.map((item) => item.id));
170
161
  remaining = remaining.filter((item) => !secondaryIds.has(item.id));
171
162
  const eligible2 = remaining.filter((item) => {
@@ -180,10 +171,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
180
171
  briefIds.add(item.id);
181
172
  return true;
182
173
  });
183
- const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
174
+ const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].filter((item) => coverIdentity(item)).slice(0, 4);
184
175
  const compactIds = new Set(compact.map((item) => item.id));
185
- const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
186
- return { lead, secondary, briefs, compact, sections: sections2, navigation: editorialSections(inventory) };
176
+ const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id) && coverIdentity(item)));
177
+ const pendingImages = inventory.filter((item) => !coverIdentity(item));
178
+ return { lead, secondary, briefs, compact, sections: sections2, pendingImages, navigation: editorialSections(inventory) };
187
179
  }
188
180
  function displayHeadline(article, selection = {}) {
189
181
  return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.83",
3
+ "version": "0.0.84",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
6
  "bin": {