@scalemule/news-publication 0.0.66 → 0.0.67

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
@@ -301,3 +301,7 @@ users by email or forward one application’s reader token/API key to another.
301
301
  Provision and verify each RP before enabling sign-in or contribution flags.
302
302
 
303
303
  The publisher-supplied `PawMark` preserves the approved SVG paths. Hosts may set `--paw-logo-ink` and `--paw-logo-heart` on their `.paw` scope; the defaults follow the accessible page foreground and coral accent.
304
+
305
+ ### Homepage image identity
306
+
307
+ `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.
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;
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';
@@ -237,9 +237,9 @@ declare function sectionId(name: string): string;
237
237
  declare function sectionPath(name: string): string;
238
238
  declare function sectionDefinition(name: string): EditorialSection;
239
239
  declare function editorialSections(items: NewsItem[]): EditorialSection[];
240
- /** Published inventory only. Selection never duplicates or fabricates a story. */
240
+ /** Published inventory only. All image-bearing slots share one allocation. */
241
241
  declare function composeEdition(items: NewsItem[], selection?: HomepageSelection, now?: number): {
242
- lead: NewsItem;
242
+ lead: NewsItem | undefined;
243
243
  secondary: NewsItem[];
244
244
  briefs: NewsItem[];
245
245
  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';
@@ -237,9 +237,9 @@ declare function sectionId(name: string): string;
237
237
  declare function sectionPath(name: string): string;
238
238
  declare function sectionDefinition(name: string): EditorialSection;
239
239
  declare function editorialSections(items: NewsItem[]): EditorialSection[];
240
- /** Published inventory only. Selection never duplicates or fabricates a story. */
240
+ /** Published inventory only. All image-bearing slots share one allocation. */
241
241
  declare function composeEdition(items: NewsItem[], selection?: HomepageSelection, now?: number): {
242
- lead: NewsItem;
242
+ lead: NewsItem | undefined;
243
243
  secondary: NewsItem[];
244
244
  briefs: NewsItem[];
245
245
  compact: NewsItem[];
package/dist/index.js CHANGED
@@ -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`)
@@ -1,5 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-qj4BeNzB.cjs';
2
- import './types-DBi49N8m.cjs';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-BBTow5Bn.cjs';
2
+ import './types-22lEf_UU.cjs';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-CjP7bSXJ.js';
2
- import './types-DBi49N8m.js';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-CHdsfUnU.js';
2
+ import './types-22lEf_UU.js';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -100,6 +100,11 @@ type NewsItem = {
100
100
  title: string;
101
101
  excerpt: string | null;
102
102
  cover_image_url: string | null;
103
+ /** Versioned canonical pixel identity supplied by the publication owner. */
104
+ visual_identity?: {
105
+ version: number;
106
+ key: string;
107
+ } | null;
103
108
  published_at: string;
104
109
  updated_at: string;
105
110
  reading_time_minutes: number;
@@ -100,6 +100,11 @@ type NewsItem = {
100
100
  title: string;
101
101
  excerpt: string | null;
102
102
  cover_image_url: string | null;
103
+ /** Versioned canonical pixel identity supplied by the publication owner. */
104
+ visual_identity?: {
105
+ version: number;
106
+ key: string;
107
+ } | null;
103
108
  published_at: string;
104
109
  updated_at: string;
105
110
  reading_time_minutes: number;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { V as VerifiedCard } from './types-DBi49N8m.cjs';
2
+ import { V as VerifiedCard } from './types-22lEf_UU.cjs';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { V as VerifiedCard } from './types-DBi49N8m.js';
2
+ import { V as VerifiedCard } from './types-22lEf_UU.js';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
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
  "main": "./dist/index.cjs",