@scalemule/news-publication 0.0.66 → 0.0.68

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.
@@ -0,0 +1,262 @@
1
+ // src/article-workflow-client.ts
2
+ var ArticleWorkflowError = class extends Error {
3
+ constructor(message, status, articleId) {
4
+ super(message);
5
+ this.status = status;
6
+ this.articleId = articleId;
7
+ this.name = "ArticleWorkflowError";
8
+ }
9
+ status;
10
+ articleId;
11
+ };
12
+ var PublicationArticleClient = class {
13
+ constructor(config) {
14
+ this.config = config;
15
+ const url = new URL(config.apiUrl);
16
+ if (url.protocol !== "https:" && !["localhost", "127.0.0.1"].includes(url.hostname))
17
+ throw new Error("Use an HTTPS API endpoint");
18
+ this.base = config.apiUrl.replace(/\/$/, "") + `/v1/blog/publications/${encodeURIComponent(config.publicationId)}`;
19
+ }
20
+ config;
21
+ base;
22
+ async request(path, method = "GET", body, extra) {
23
+ const headers = new Headers(extra);
24
+ headers.set("accept", "application/json");
25
+ if (this.config.apiKey) headers.set("x-api-key", this.config.apiKey);
26
+ if (this.config.accessToken)
27
+ headers.set("authorization", `Bearer ${this.config.accessToken}`);
28
+ const form = body instanceof FormData;
29
+ if (body !== void 0 && !form)
30
+ headers.set("content-type", "application/json");
31
+ const url = new URL(
32
+ path.startsWith("/v1/") ? this.config.apiUrl.replace(/\/$/, "") + path : this.base + path
33
+ );
34
+ if (this.config.applicationId)
35
+ url.searchParams.set("application_id", this.config.applicationId);
36
+ const response = await (this.config.fetch ?? fetch)(url, {
37
+ method,
38
+ headers,
39
+ body: body === void 0 ? void 0 : form ? body : JSON.stringify(body),
40
+ cache: "no-store",
41
+ redirect: "error"
42
+ });
43
+ const value = await response.json();
44
+ if (!response.ok || value.success !== true || value.data == null)
45
+ throw new ArticleWorkflowError(
46
+ value.error?.message || `Article API returned ${response.status}`,
47
+ response.status
48
+ );
49
+ return value.data;
50
+ }
51
+ options(articleId) {
52
+ return this.request(
53
+ `/article-workflow/options${articleId ? `?article_id=${encodeURIComponent(articleId)}` : ""}`
54
+ );
55
+ }
56
+ get(articleId) {
57
+ return this.request(
58
+ `/articles/${encodeURIComponent(articleId)}/collaboration`
59
+ );
60
+ }
61
+ processing(articleId) {
62
+ return this.request(`/articles/${encodeURIComponent(articleId)}/processing`);
63
+ }
64
+ retryProcessing(articleId, expectedVersion, assetSourceFileId) {
65
+ return this.request(
66
+ `/articles/${encodeURIComponent(articleId)}/processing`,
67
+ "POST",
68
+ {
69
+ expected_version: expectedVersion,
70
+ asset_source_file_id: assetSourceFileId
71
+ }
72
+ );
73
+ }
74
+ create(key, authorProfileId, document, reviewPolicy = "SUBJECT_APPROVAL_PLUS_EDITOR") {
75
+ return this.request(
76
+ "/article-workflow",
77
+ "POST",
78
+ {
79
+ operation: "create",
80
+ author_profile_id: authorProfileId,
81
+ document,
82
+ review_policy: reviewPolicy
83
+ },
84
+ { "Idempotency-Key": key }
85
+ );
86
+ }
87
+ save(key, articleId, generation, document, reason = "EDITOR_EDIT") {
88
+ return this.request(
89
+ "/article-workflow",
90
+ "POST",
91
+ {
92
+ operation: "save",
93
+ article_id: articleId,
94
+ expected_generation: generation,
95
+ document,
96
+ reason
97
+ },
98
+ { "Idempotency-Key": key }
99
+ );
100
+ }
101
+ command(articleId, generation, action) {
102
+ return this.request(
103
+ `/articles/${encodeURIComponent(articleId)}/collaboration`,
104
+ "POST",
105
+ { ...action, expected_generation: generation }
106
+ );
107
+ }
108
+ registerPhoto(fileId) {
109
+ return this.request(
110
+ `/v1/photos/${this.config.applicationId ? "admin/register" : "register"}`,
111
+ "POST",
112
+ {
113
+ file_id: fileId,
114
+ content_kind: "article",
115
+ publication_id: this.config.publicationId,
116
+ ...this.config.applicationId ? { application_id: this.config.applicationId } : {}
117
+ }
118
+ );
119
+ }
120
+ upload(articleId, generation, file, filename, metadata) {
121
+ const body = new FormData();
122
+ body.set("metadata", JSON.stringify(metadata));
123
+ body.set("file", file, filename);
124
+ return this.request(`/articles/${encodeURIComponent(articleId)}/assets`, "POST", body, {
125
+ "If-Match": String(generation)
126
+ });
127
+ }
128
+ /** One agent operation over existing owners. Stops at conflicts/unknown upload acknowledgement;
129
+ * replay the same operation key to inspect/reuse staged contributions before another transfer.
130
+ * Editorial approval and release are always separate explicit operations. */
131
+ async submitArticle(input) {
132
+ if (input.document.media?.length)
133
+ throw new Error(
134
+ "Pass file assets separately when creating an article package"
135
+ );
136
+ const capabilities = await this.options();
137
+ if (capabilities.workflow_version !== 1)
138
+ throw new Error("Unsupported article workflow version");
139
+ if (input.assets?.length && !capabilities.capabilities.managed_private_asset_upload)
140
+ throw new Error("Managed article upload is not deployed");
141
+ const created = await this.create(
142
+ input.operationKey,
143
+ input.authorProfileId,
144
+ input.document,
145
+ input.reviewPolicy
146
+ );
147
+ try {
148
+ const selected = [];
149
+ for (const asset of input.assets ?? []) {
150
+ let current = await this.get(created.article_id);
151
+ const digest = Array.from(
152
+ new Uint8Array(
153
+ await crypto.subtle.digest(
154
+ "SHA-256",
155
+ await asset.file.arrayBuffer()
156
+ )
157
+ )
158
+ ).map((v) => v.toString(16).padStart(2, "0")).join("");
159
+ const existing = current.contributions.find(
160
+ (c) => c.kind === "MEDIA" && c.state !== "REJECTED" && c.state !== "WITHDRAWN" && c.payload.sha256 === digest && Object.entries(asset.metadata).every(
161
+ ([k, v]) => c.payload[k] === v
162
+ )
163
+ );
164
+ const staged = existing?.payload.file_id ? { contribution_id: existing.id, file_id: existing.payload.file_id } : await this.upload(
165
+ created.article_id,
166
+ current.generation,
167
+ asset.file,
168
+ asset.filename,
169
+ asset.metadata
170
+ );
171
+ await this.registerPhoto(staged.file_id);
172
+ if (input.approveMedia) {
173
+ if (!asset.metadata.rights_confirmed || asset.metadata.private_preview_only)
174
+ throw new Error("Publication requires confirmed image rights");
175
+ if (existing?.state !== "APPROVED") {
176
+ current = await this.get(created.article_id);
177
+ await this.command(created.article_id, current.generation, {
178
+ action: "RESOLVE",
179
+ contribution_id: staged.contribution_id,
180
+ state: "APPROVED"
181
+ });
182
+ }
183
+ selected.push({
184
+ kind: "image",
185
+ contribution_id: staged.contribution_id,
186
+ file_id: staged.file_id,
187
+ caption: asset.metadata.caption,
188
+ credit: asset.metadata.credit,
189
+ alt: asset.metadata.alt,
190
+ position: asset.position
191
+ });
192
+ }
193
+ }
194
+ if (selected.length) {
195
+ const current = await this.get(created.article_id);
196
+ const document = { ...input.document, media: selected };
197
+ const saved = current.revisions.find(
198
+ (r) => r.id === current.current_revision_id
199
+ )?.document;
200
+ if (!saved) throw new Error("Current immutable revision is missing");
201
+ if (sameDocument(saved, document))
202
+ return {
203
+ ...created,
204
+ revision_id: current.current_revision_id,
205
+ generation: current.generation,
206
+ editorial_status: current.status,
207
+ replayed: true
208
+ };
209
+ if (!sameDocument(
210
+ { ...saved, media: [] },
211
+ { ...input.document, media: [] }
212
+ ) || saved.media?.length)
213
+ throw new ArticleWorkflowError(
214
+ "The article was edited after this package began; load its current revision before changing it",
215
+ 409,
216
+ created.article_id
217
+ );
218
+ return await this.save(
219
+ `${input.operationKey}:assets`,
220
+ created.article_id,
221
+ current.generation,
222
+ document,
223
+ "MEDIA_UPDATE"
224
+ );
225
+ }
226
+ return created;
227
+ } catch (error) {
228
+ if (error instanceof ArticleWorkflowError) {
229
+ error.articleId = created.article_id;
230
+ throw error;
231
+ }
232
+ throw new ArticleWorkflowError(
233
+ error instanceof Error ? error.message : "Article package interrupted",
234
+ 0,
235
+ created.article_id
236
+ );
237
+ }
238
+ }
239
+ };
240
+ function sameDocument(a, b) {
241
+ const canonical = (d) => [
242
+ d.title,
243
+ d.subtitle || "",
244
+ d.content_markdown,
245
+ d.section,
246
+ d.community || "",
247
+ d.format || null,
248
+ d.layout ? [d.layout.id, d.layout.version] : null,
249
+ (d.media || []).map((m) => [
250
+ m.kind || "image",
251
+ m.contribution_id,
252
+ m.file_id,
253
+ m.caption,
254
+ m.credit,
255
+ m.alt,
256
+ m.position
257
+ ])
258
+ ];
259
+ return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
260
+ }
261
+
262
+ export { ArticleWorkflowError, PublicationArticleClient };
@@ -232,4 +232,40 @@ function renderArticleBlocksToHtml(blocks, mediaMap, options) {
232
232
  return parts.join("\n");
233
233
  }
234
234
 
235
- export { formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown };
235
+ // src/article-presentation.ts
236
+ var standard = {
237
+ id: "standard",
238
+ version: 1,
239
+ name: "Standard article",
240
+ renderer: "standard",
241
+ hero_aspect: "landscape",
242
+ headline_position: "before_hero",
243
+ wide_body: false
244
+ };
245
+ var formats = [
246
+ "news",
247
+ "brief",
248
+ "explainer",
249
+ "interview",
250
+ "opinion",
251
+ "photo_essay",
252
+ "video_story"
253
+ ];
254
+ function articlePresentation(value) {
255
+ const l = value?.layout;
256
+ if (!l || !/^[a-z0-9_-]{1,80}$/.test(l.id) || !Number.isInteger(l.version) || l.version < 1 || !["standard", "feature", "portrait"].includes(l.renderer) || !["landscape", "portrait", "square"].includes(l.hero_aspect) || !["before_hero", "after_hero"].includes(l.headline_position) || typeof l.wide_body !== "boolean") {
257
+ return { layout: standard };
258
+ }
259
+ return {
260
+ format: value?.format && formats.includes(value.format) ? value.format : void 0,
261
+ layout: l
262
+ };
263
+ }
264
+ function articleHeroRatio(layout) {
265
+ return layout.hero_aspect === "portrait" ? 4 / 5 : layout.hero_aspect === "square" ? 1 : 16 / 9;
266
+ }
267
+ function articleFormatLabel(format) {
268
+ return format === "opinion" ? "Opinion" : format === "explainer" ? "Explainer" : format === "interview" ? "Interview" : format === "photo_essay" ? "Photo essay" : format === "video_story" ? "Video story" : void 0;
269
+ }
270
+
271
+ export { articleFormatLabel, articleHeroRatio, articlePresentation, formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown };
package/dist/index.cjs CHANGED
@@ -158,42 +158,72 @@ function editorialSections(items) {
158
158
  }
159
159
  return Array.from(groups, ([name, articles]) => ({ ...sectionDefinition(name), items: articles }));
160
160
  }
161
+ function coverIdentity(item) {
162
+ if (!item.cover_image_url?.trim()) return null;
163
+ const identity = item.visual_identity;
164
+ if (identity?.version === 1 && /^[a-f0-9]{64}$/u.test(identity.key)) return `pixels:${identity.key}`;
165
+ try {
166
+ const url = new URL(item.cover_image_url.trim());
167
+ if (url.protocol !== "https:" || url.username || url.password) return null;
168
+ if (url.hostname === "cdn-public.scalemule.com") {
169
+ const match = url.pathname.match(/^\/([a-f0-9-]{36})\/anonymous\/photos\/([a-f0-9-]{36})\//iu);
170
+ if (match) return `photo:${match[1].toLowerCase()}:${match[2].toLowerCase()}`;
171
+ }
172
+ if (["api.scalemule.com", "gw.scalemule.com"].includes(url.hostname)) {
173
+ const match = url.pathname.match(/^\/v1\/photos\/public\/([a-f0-9-]{36})(?:\/|$)/iu);
174
+ if (match) return `photo-public:${match[1].toLowerCase()}`;
175
+ }
176
+ url.hash = "";
177
+ url.searchParams.sort();
178
+ return `url:${url.toString()}`;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
161
183
  function composeEdition(items, selection = {}, now = Date.now()) {
162
184
  const seen = /* @__PURE__ */ new Set();
163
- const inventory = items.filter((item) => {
185
+ let inventory = items.slice(0, 200).filter((item) => {
164
186
  if (!item.id || !item.slug || seen.has(item.id)) return false;
165
187
  seen.add(item.id);
166
188
  return true;
167
189
  });
168
190
  const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
169
- const lead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
170
- const remaining = inventory.filter((item) => item !== lead);
191
+ const selectedLead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
192
+ const rest = inventory.filter((item) => item !== selectedLead);
171
193
  const requested = new Set(pins.secondary_slugs ?? []);
172
194
  const ordered = [
173
- ...(pins.secondary_slugs ?? []).flatMap((slug) => remaining.find((item) => item.slug === slug) ?? []),
174
- ...remaining.filter((item) => !requested.has(item.slug))
195
+ ...(pins.secondary_slugs ?? []).flatMap((slug) => rest.find((item) => item.slug === slug) ?? []),
196
+ ...rest.filter((item) => !requested.has(item.slug))
175
197
  ];
176
- const uniqueOrdered = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
198
+ const originalOrder = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
177
199
  const seenImages = /* @__PURE__ */ new Set();
178
- if (lead?.cover_image_url?.trim()) {
179
- seenImages.add(lead.cover_image_url.trim());
200
+ const duplicateImages = /* @__PURE__ */ new Set();
201
+ const display = /* @__PURE__ */ new Map();
202
+ for (const item of [selectedLead, ...originalOrder]) {
203
+ if (!item) continue;
204
+ const key = coverIdentity(item);
205
+ if (key && seenImages.has(key)) {
206
+ duplicateImages.add(item.id);
207
+ display.set(item.id, { ...item, cover_image_url: null });
208
+ } else {
209
+ display.set(item.id, item);
210
+ if (key) seenImages.add(key);
211
+ }
180
212
  }
213
+ inventory = inventory.map((item) => display.get(item.id));
214
+ const lead = selectedLead ? display.get(selectedLead.id) : void 0;
215
+ let remaining = rest.map((item) => display.get(item.id));
216
+ const uniqueOrdered = originalOrder.map((item) => display.get(item.id));
181
217
  const secondary = [];
182
- const deferredWithDuplicateImage = [];
218
+ const deferred = [];
183
219
  for (const item of uniqueOrdered) {
184
220
  if (secondary.length >= 3) break;
185
- const img = item.cover_image_url?.trim();
186
- if (img && seenImages.has(img)) {
187
- deferredWithDuplicateImage.push(item);
188
- } else {
189
- secondary.push(item);
190
- if (img) seenImages.add(img);
191
- }
192
- }
193
- while (secondary.length < 3 && deferredWithDuplicateImage.length > 0) {
194
- secondary.push(deferredWithDuplicateImage.shift());
221
+ if (duplicateImages.has(item.id)) deferred.push(item);
222
+ else secondary.push(item);
195
223
  }
196
- for (const item of secondary) remaining.splice(remaining.indexOf(item), 1);
224
+ while (secondary.length < 3 && deferred.length > 0) secondary.push(deferred.shift());
225
+ const secondaryIds = new Set(secondary.map((item) => item.id));
226
+ remaining = remaining.filter((item) => !secondaryIds.has(item.id));
197
227
  const eligible2 = remaining.filter((item) => {
198
228
  const age = now - Date.parse(item.published_at);
199
229
  return item.excerpt?.trim() && age >= 0 && age <= 7 * 864e5;
@@ -3556,6 +3586,42 @@ function campaignReportHtml(report) {
3556
3586
  <footer><p>${escape(report.reporting_note)}</p><p>${escape(report.editorial_independence)}</p><p>Campaign reference: ${escape(report.campaign_id)}</p></footer></body></html>`;
3557
3587
  }
3558
3588
 
3589
+ // src/article-presentation.ts
3590
+ var standard = {
3591
+ id: "standard",
3592
+ version: 1,
3593
+ name: "Standard article",
3594
+ renderer: "standard",
3595
+ hero_aspect: "landscape",
3596
+ headline_position: "before_hero",
3597
+ wide_body: false
3598
+ };
3599
+ var formats = [
3600
+ "news",
3601
+ "brief",
3602
+ "explainer",
3603
+ "interview",
3604
+ "opinion",
3605
+ "photo_essay",
3606
+ "video_story"
3607
+ ];
3608
+ function articlePresentation(value) {
3609
+ const l = value?.layout;
3610
+ if (!l || !/^[a-z0-9_-]{1,80}$/.test(l.id) || !Number.isInteger(l.version) || l.version < 1 || !["standard", "feature", "portrait"].includes(l.renderer) || !["landscape", "portrait", "square"].includes(l.hero_aspect) || !["before_hero", "after_hero"].includes(l.headline_position) || typeof l.wide_body !== "boolean") {
3611
+ return { layout: standard };
3612
+ }
3613
+ return {
3614
+ format: value?.format && formats.includes(value.format) ? value.format : void 0,
3615
+ layout: l
3616
+ };
3617
+ }
3618
+ function articleHeroRatio(layout) {
3619
+ return layout.hero_aspect === "portrait" ? 4 / 5 : layout.hero_aspect === "square" ? 1 : 16 / 9;
3620
+ }
3621
+ function articleFormatLabel(format) {
3622
+ return format === "opinion" ? "Opinion" : format === "explainer" ? "Explainer" : format === "interview" ? "Interview" : format === "photo_essay" ? "Photo essay" : format === "video_story" ? "Video story" : void 0;
3623
+ }
3624
+
3559
3625
  Object.defineProperty(exports, "COOKIE_CONSENT_BACKUP_KEY", {
3560
3626
  enumerable: true,
3561
3627
  get: function () { return compliance.COOKIE_CONSENT_BACKUP_KEY; }
@@ -3696,7 +3762,10 @@ exports.applyNavigationConfig = applyNavigationConfig;
3696
3762
  exports.applyTheme = applyTheme;
3697
3763
  exports.articleCacheKey = articleCacheKey;
3698
3764
  exports.articleCacheOptions = articleCacheOptions;
3765
+ exports.articleFormatLabel = articleFormatLabel;
3766
+ exports.articleHeroRatio = articleHeroRatio;
3699
3767
  exports.articleMeta = articleMeta;
3768
+ exports.articlePresentation = articlePresentation;
3700
3769
  exports.articleUrl = articleUrl;
3701
3770
  exports.attribution = attribution;
3702
3771
  exports.briefSummary = briefSummary;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-DBi49N8m.cjs';
2
- export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-DBi49N8m.cjs';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-22lEf_UU.cjs';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-22lEf_UU.cjs';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-CO9Unz5M.cjs';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.cjs';
5
- export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-qj4BeNzB.cjs';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-BBTow5Bn.cjs';
6
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
7
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, consentModel, decorateNetworkUrl, effectivePreferences, getCookieConsent, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.cjs';
@@ -17,6 +17,7 @@ export { C as CommunityMapArticleEmbed, c as communityMapEmbeds } from './commun
17
17
  export { C as ContentBlock, S as StructuredDocument, a as StructuredMedia, f as formatArtworkCaption, p as parseDocumentBlocks, r as renderArticleBlocksToHtml, s as sanitizeInlineMarkdown } from './article-blocks-DsabIDmH.cjs';
18
18
  import { M as Measurement } from './sponsorships-CtnBe3S8.cjs';
19
19
  export { a as Campaign, b as CampaignStatus, c as CampaignTarget, C as Creative, d as Placement, e as PlacementType, P as PublicAcknowledgment, S as SPONSORSHIP_OFFERINGS, f as SPONSORSHIP_POLICY, g as SponsorParty, p as placementAvailable, s as safeSponsorUrl } from './sponsorships-CtnBe3S8.cjs';
20
+ export { A as ArticleFormat, b as ArticleLayout, a as ArticlePresentation, c as articleFormatLabel, d as articleHeroRatio, e as articlePresentation } from './article-presentation-0d1FiDgA.cjs';
20
21
  import './pets.cjs';
21
22
 
22
23
  /** Walnut Creek Times broadsheet colors. A paper overrides only what it changes. */
@@ -237,9 +238,9 @@ declare function sectionId(name: string): string;
237
238
  declare function sectionPath(name: string): string;
238
239
  declare function sectionDefinition(name: string): EditorialSection;
239
240
  declare function editorialSections(items: NewsItem[]): EditorialSection[];
240
- /** Published inventory only. Selection never duplicates or fabricates a story. */
241
+ /** Published inventory only. All image-bearing slots share one allocation. */
241
242
  declare function composeEdition(items: NewsItem[], selection?: HomepageSelection, now?: number): {
242
- lead: NewsItem;
243
+ lead: NewsItem | undefined;
243
244
  secondary: NewsItem[];
244
245
  briefs: NewsItem[];
245
246
  compact: NewsItem[];
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-DBi49N8m.js';
2
- export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-DBi49N8m.js';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-22lEf_UU.js';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-22lEf_UU.js';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-BWpxY20X.js';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.js';
5
- export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-CjP7bSXJ.js';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-CHdsfUnU.js';
6
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
7
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, consentModel, decorateNetworkUrl, effectivePreferences, getCookieConsent, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.js';
@@ -17,6 +17,7 @@ export { C as CommunityMapArticleEmbed, c as communityMapEmbeds } from './commun
17
17
  export { C as ContentBlock, S as StructuredDocument, a as StructuredMedia, f as formatArtworkCaption, p as parseDocumentBlocks, r as renderArticleBlocksToHtml, s as sanitizeInlineMarkdown } from './article-blocks-DsabIDmH.js';
18
18
  import { M as Measurement } from './sponsorships-CtnBe3S8.js';
19
19
  export { a as Campaign, b as CampaignStatus, c as CampaignTarget, C as Creative, d as Placement, e as PlacementType, P as PublicAcknowledgment, S as SPONSORSHIP_OFFERINGS, f as SPONSORSHIP_POLICY, g as SponsorParty, p as placementAvailable, s as safeSponsorUrl } from './sponsorships-CtnBe3S8.js';
20
+ export { A as ArticleFormat, b as ArticleLayout, a as ArticlePresentation, c as articleFormatLabel, d as articleHeroRatio, e as articlePresentation } from './article-presentation-0d1FiDgA.js';
20
21
  import './pets.js';
21
22
 
22
23
  /** Walnut Creek Times broadsheet colors. A paper overrides only what it changes. */
@@ -237,9 +238,9 @@ declare function sectionId(name: string): string;
237
238
  declare function sectionPath(name: string): string;
238
239
  declare function sectionDefinition(name: string): EditorialSection;
239
240
  declare function editorialSections(items: NewsItem[]): EditorialSection[];
240
- /** Published inventory only. Selection never duplicates or fabricates a story. */
241
+ /** Published inventory only. All image-bearing slots share one allocation. */
241
242
  declare function composeEdition(items: NewsItem[], selection?: HomepageSelection, now?: number): {
242
- lead: NewsItem;
243
+ lead: NewsItem | undefined;
243
244
  secondary: NewsItem[];
244
245
  briefs: NewsItem[];
245
246
  compact: NewsItem[];
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
+ export { NetworkCopyrightNotice } from './chunk-FWEYKWKP.js';
1
2
  export { CURATED_COMMUNITY_CARDS, VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './chunk-KYUVXEJY.js';
3
+ export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-AMLVCZDP.js';
2
4
  export { CookieConsentBanner } from './chunk-XSAVJJVW.js';
3
5
  export { CookieSettingsView } from './chunk-X7AU7DV6.js';
4
6
  export { THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, ThemeToggle, applyTheme, clearTheme, getStoredTheme, getSystemTheme, resolveEffectiveTheme, setTheme, subscribeTheme, themeInitScript } from './chunk-NQD3M7ZY.js';
5
7
  export { LocalNetworkBar } from './chunk-Q77T2S7B.js';
6
- export { NetworkCopyrightNotice } from './chunk-FWEYKWKP.js';
7
8
  import { localPublications } from './chunk-XWXPERLK.js';
8
9
  export { COOKIE_CATEGORIES, COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EDITORIAL_PRINCIPLES, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, NETWORK_PARENT, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, communityPublications, consentModel, decorateNetworkUrl, effectivePreferences, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub, topicalPublications } from './chunk-XWXPERLK.js';
9
10
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
10
- export { formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-OISLCYER.js';
11
+ export { articleFormatLabel, articleHeroRatio, articlePresentation, formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-DN3TBZLK.js';
11
12
  import { definePublication } from './chunk-57YVL6BD.js';
12
13
  export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-57YVL6BD.js';
13
14
  export { CLASSIFIED_CATEGORIES, JOB_EMPLOYMENT_TYPES, categoryLabel, classifiedPath, classifiedSearchPath, clipChars, dealDollars, dealGoPath, dealsFeedPath, employmentLabel, isClassifiedCategory, isEmploymentType, jobEmployerBody, jobOpeningBody, jobPostingBody, prepareClassified, prepareJobPost, publicJobPath, publicJobsPath } from './chunk-C7ZPLH2K.js';
14
- export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-AMLVCZDP.js';
15
15
  export { SPONSORSHIP_OFFERINGS, SPONSORSHIP_POLICY, placementAvailable, safeSponsorUrl } from './chunk-66GI2IX6.js';
16
16
  import './chunk-AFJ6S5WJ.js';
17
17
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -101,42 +101,72 @@ function editorialSections(items) {
101
101
  }
102
102
  return Array.from(groups, ([name, articles]) => ({ ...sectionDefinition(name), items: articles }));
103
103
  }
104
+ function coverIdentity(item) {
105
+ if (!item.cover_image_url?.trim()) return null;
106
+ const identity = item.visual_identity;
107
+ if (identity?.version === 1 && /^[a-f0-9]{64}$/u.test(identity.key)) return `pixels:${identity.key}`;
108
+ try {
109
+ const url = new URL(item.cover_image_url.trim());
110
+ if (url.protocol !== "https:" || url.username || url.password) return null;
111
+ if (url.hostname === "cdn-public.scalemule.com") {
112
+ const match = url.pathname.match(/^\/([a-f0-9-]{36})\/anonymous\/photos\/([a-f0-9-]{36})\//iu);
113
+ if (match) return `photo:${match[1].toLowerCase()}:${match[2].toLowerCase()}`;
114
+ }
115
+ if (["api.scalemule.com", "gw.scalemule.com"].includes(url.hostname)) {
116
+ const match = url.pathname.match(/^\/v1\/photos\/public\/([a-f0-9-]{36})(?:\/|$)/iu);
117
+ if (match) return `photo-public:${match[1].toLowerCase()}`;
118
+ }
119
+ url.hash = "";
120
+ url.searchParams.sort();
121
+ return `url:${url.toString()}`;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
104
126
  function composeEdition(items, selection = {}, now = Date.now()) {
105
127
  const seen = /* @__PURE__ */ new Set();
106
- const inventory = items.filter((item) => {
128
+ let inventory = items.slice(0, 200).filter((item) => {
107
129
  if (!item.id || !item.slug || seen.has(item.id)) return false;
108
130
  seen.add(item.id);
109
131
  return true;
110
132
  });
111
133
  const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
112
- const lead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
113
- const remaining = inventory.filter((item) => item !== lead);
134
+ const selectedLead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
135
+ const rest = inventory.filter((item) => item !== selectedLead);
114
136
  const requested = new Set(pins.secondary_slugs ?? []);
115
137
  const ordered = [
116
- ...(pins.secondary_slugs ?? []).flatMap((slug) => remaining.find((item) => item.slug === slug) ?? []),
117
- ...remaining.filter((item) => !requested.has(item.slug))
138
+ ...(pins.secondary_slugs ?? []).flatMap((slug) => rest.find((item) => item.slug === slug) ?? []),
139
+ ...rest.filter((item) => !requested.has(item.slug))
118
140
  ];
119
- const uniqueOrdered = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
141
+ const originalOrder = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
120
142
  const seenImages = /* @__PURE__ */ new Set();
121
- if (lead?.cover_image_url?.trim()) {
122
- seenImages.add(lead.cover_image_url.trim());
143
+ const duplicateImages = /* @__PURE__ */ new Set();
144
+ const display = /* @__PURE__ */ new Map();
145
+ for (const item of [selectedLead, ...originalOrder]) {
146
+ if (!item) continue;
147
+ const key = coverIdentity(item);
148
+ if (key && seenImages.has(key)) {
149
+ duplicateImages.add(item.id);
150
+ display.set(item.id, { ...item, cover_image_url: null });
151
+ } else {
152
+ display.set(item.id, item);
153
+ if (key) seenImages.add(key);
154
+ }
123
155
  }
156
+ inventory = inventory.map((item) => display.get(item.id));
157
+ const lead = selectedLead ? display.get(selectedLead.id) : void 0;
158
+ let remaining = rest.map((item) => display.get(item.id));
159
+ const uniqueOrdered = originalOrder.map((item) => display.get(item.id));
124
160
  const secondary = [];
125
- const deferredWithDuplicateImage = [];
161
+ const deferred = [];
126
162
  for (const item of uniqueOrdered) {
127
163
  if (secondary.length >= 3) break;
128
- const img = item.cover_image_url?.trim();
129
- if (img && seenImages.has(img)) {
130
- deferredWithDuplicateImage.push(item);
131
- } else {
132
- secondary.push(item);
133
- if (img) seenImages.add(img);
134
- }
135
- }
136
- while (secondary.length < 3 && deferredWithDuplicateImage.length > 0) {
137
- secondary.push(deferredWithDuplicateImage.shift());
164
+ if (duplicateImages.has(item.id)) deferred.push(item);
165
+ else secondary.push(item);
138
166
  }
139
- for (const item of secondary) remaining.splice(remaining.indexOf(item), 1);
167
+ while (secondary.length < 3 && deferred.length > 0) secondary.push(deferred.shift());
168
+ const secondaryIds = new Set(secondary.map((item) => item.id));
169
+ remaining = remaining.filter((item) => !secondaryIds.has(item.id));
140
170
  const eligible2 = remaining.filter((item) => {
141
171
  const age = now - Date.parse(item.published_at);
142
172
  return item.excerpt?.trim() && age >= 0 && age <= 7 * 864e5;
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-DBi49N8m.cjs';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-22lEf_UU.cjs';
2
2
 
3
3
  /**
4
4
  * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-DBi49N8m.js';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-22lEf_UU.js';
2
2
 
3
3
  /**
4
4
  * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)