@scalemule/news-publication 0.0.19 → 0.0.21

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.cjs CHANGED
@@ -82,7 +82,7 @@ var walnutCreekTimes = definePublication({
82
82
  regionLabel: "Walnut Creek",
83
83
  towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
84
84
  nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
85
- sections: ["Civic life", "Community", "Local businesses", "Events", "Food & places"],
85
+ sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
86
86
  description: "Local news, independent businesses and community voices from Walnut Creek, Rossmoor, Saranap and Pleasant Hill.",
87
87
  layout: "broadsheet",
88
88
  layouts: ["broadsheet"],
@@ -174,9 +174,30 @@ function defaultRevalidateTargets() {
174
174
 
175
175
  // src/editorial.ts
176
176
  var labels = {
177
- "Local businesses": "Local business",
177
+ "Local businesses": "Business",
178
+ "Events": "Things to do",
178
179
  "Food & places": "Food & drink"
179
180
  };
181
+ var EDITORIAL_SECTION_ORDER = [
182
+ "Civic life",
183
+ "Community",
184
+ "Local businesses",
185
+ "Real Estate",
186
+ "Food & places",
187
+ "Events",
188
+ "Health",
189
+ "Schools",
190
+ "Opinion"
191
+ ];
192
+ function orderedSections(sections, order = EDITORIAL_SECTION_ORDER) {
193
+ return sections.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
194
+ const leftRank = order.indexOf(left.name);
195
+ const rightRank = order.indexOf(right.name);
196
+ const leftOrder = leftRank === -1 ? order.length : leftRank;
197
+ const rightOrder = rightRank === -1 ? order.length : rightRank;
198
+ return leftOrder - rightOrder || left.name.localeCompare(right.name);
199
+ });
200
+ }
180
201
  function sectionId(name) {
181
202
  return "section-" + name.normalize("NFKD").replace(/\p{Mark}/gu, "").toLowerCase().replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-|-$/g, "");
182
203
  }
@@ -1815,6 +1836,57 @@ function readerSources(input) {
1815
1836
  return output;
1816
1837
  }
1817
1838
 
1839
+ // src/listening.ts
1840
+ function publicationListeningTrack(article, publicationId, publicationName, origin) {
1841
+ const articleUrl2 = new URL(`/news/${encodeURIComponent(article.slug)}`, origin).href;
1842
+ let storyId = articleUrl2;
1843
+ if (article.canonical_url) {
1844
+ try {
1845
+ const canonical = new URL(article.canonical_url);
1846
+ if (canonical.protocol === "https:" && !canonical.username && !canonical.password && /^\/news\/[^/]+\/?$/.test(canonical.pathname) && localPublications.some((item) => new URL(item.url).hostname === canonical.hostname.replace(/^www\./, ""))) {
1847
+ canonical.search = "";
1848
+ canonical.hash = "";
1849
+ canonical.hostname = canonical.hostname.replace(/^www\./, "");
1850
+ canonical.pathname = canonical.pathname.replace(/\/$/, "");
1851
+ storyId = canonical.href;
1852
+ }
1853
+ } catch {
1854
+ }
1855
+ }
1856
+ const revision = article.editorial?.modified_at;
1857
+ return {
1858
+ id: article.id,
1859
+ publicationId,
1860
+ publicationName,
1861
+ title: article.title,
1862
+ articleUrl: articleUrl2,
1863
+ storyId,
1864
+ ...revision && Number.isFinite(Date.parse(revision)) ? { revision } : {},
1865
+ ...article.audio?.duration_ms && article.audio.duration_ms > 0 ? { durationSeconds: article.audio.duration_ms / 1e3 } : {},
1866
+ ...article.section ? { section: article.section } : {},
1867
+ ...article.published_at ? { publishedAt: article.published_at } : {}
1868
+ };
1869
+ }
1870
+ async function publicationListeningCandidates(items, read, publicationId, publicationName, origin) {
1871
+ const output = [];
1872
+ const recent = items.filter((item) => !item.editorial?.duplicate_url).slice(0, 30);
1873
+ let failures = 0;
1874
+ for (let i = 0; i < recent.length; i += 4) {
1875
+ const batch = await Promise.allSettled(recent.slice(i, i + 4).map((item) => read(item.slug)));
1876
+ for (const result of batch) {
1877
+ if (result.status === "rejected") {
1878
+ failures++;
1879
+ continue;
1880
+ }
1881
+ const article = result.value;
1882
+ if (!article.editorial?.duplicate_url && article.audio?.url && article.audio.duration_ms && article.audio.duration_ms > 0)
1883
+ output.push(publicationListeningTrack(article, publicationId, publicationName, origin));
1884
+ }
1885
+ }
1886
+ if (!output.length && failures) throw new Error("Audio inventory could not be loaded");
1887
+ return output.sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""));
1888
+ }
1889
+
1818
1890
  Object.defineProperty(exports, "COOKIE_CONSENT_BACKUP_KEY", {
1819
1891
  enumerable: true,
1820
1892
  get: function () { return compliance.COOKIE_CONSENT_BACKUP_KEY; }
@@ -1907,6 +1979,7 @@ exports.CLASSIFIED_CATEGORIES = CLASSIFIED_CATEGORIES;
1907
1979
  exports.COOKIE_CATEGORIES = COOKIE_CATEGORIES;
1908
1980
  exports.CookieConsentBanner = CookieConsentBanner;
1909
1981
  exports.CookieSettingsView = CookieSettingsView;
1982
+ exports.EDITORIAL_SECTION_ORDER = EDITORIAL_SECTION_ORDER;
1910
1983
  exports.EVENTS = EVENTS;
1911
1984
  exports.FEATURE_NAMES = FEATURE_NAMES;
1912
1985
  exports.JOB_EMPLOYMENT_TYPES = JOB_EMPLOYMENT_TYPES;
@@ -1974,6 +2047,7 @@ exports.matchesStoryTag = matchesStoryTag;
1974
2047
  exports.nearbyUpdates = nearbyUpdates;
1975
2048
  exports.newsArticleJsonLd = newsArticleJsonLd;
1976
2049
  exports.normalizedText = normalizedText;
2050
+ exports.orderedSections = orderedSections;
1977
2051
  exports.parseSnapshot = parseSnapshot;
1978
2052
  exports.prepareClassified = prepareClassified;
1979
2053
  exports.prepareJobPost = prepareJobPost;
@@ -1984,6 +2058,8 @@ exports.publicJobsPath = publicJobsPath;
1984
2058
  exports.publicUrl = publicUrl;
1985
2059
  exports.publicationFromSnapshot = publicationFromSnapshot;
1986
2060
  exports.publicationJsonLd = publicationJsonLd;
2061
+ exports.publicationListeningCandidates = publicationListeningCandidates;
2062
+ exports.publicationListeningTrack = publicationListeningTrack;
1987
2063
  exports.publicationMeta = publicationMeta;
1988
2064
  exports.publicationRecord = publicationRecord;
1989
2065
  exports.readerSources = readerSources;
package/dist/index.d.cts CHANGED
@@ -102,6 +102,16 @@ declare function defaultRevalidateTargets(): {
102
102
  paths: string[];
103
103
  };
104
104
 
105
+ /**
106
+ * Canonical reader-facing order for editorial sections across the network.
107
+ * Names not listed here sort after these, alphabetically. "Schools" and
108
+ * "Opinion" are reserved so future sections slot in without a release.
109
+ */
110
+ declare const EDITORIAL_SECTION_ORDER: readonly ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health", "Schools", "Opinion"];
111
+ /** Sorts sections into the canonical order, dropping the implicit "Local news" bucket. */
112
+ declare function orderedSections<T extends {
113
+ name: string;
114
+ }>(sections: T[], order?: readonly string[]): T[];
105
115
  declare function sectionId(name: string): string;
106
116
  declare function sectionPath(name: string): string;
107
117
  declare function sectionDefinition(name: string): EditorialSection;
@@ -301,4 +311,47 @@ declare function readerSources(input: unknown): {
301
311
  publisher?: string;
302
312
  }[];
303
313
 
304
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CouponOffer, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, publicHttps, publicUrl, publicationJsonLd, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
314
+ interface ListeningArticle {
315
+ id: string;
316
+ slug: string;
317
+ title: string;
318
+ canonical_url?: string | null;
319
+ section?: string | null;
320
+ published_at?: string;
321
+ editorial?: {
322
+ duplicate_url?: string;
323
+ modified_at?: string;
324
+ } | null;
325
+ audio?: {
326
+ url?: string | null;
327
+ duration_ms?: number | null;
328
+ };
329
+ }
330
+ /** Public metadata only. Never cache signed recording URLs in reader history. */
331
+ declare function publicationListeningTrack(article: ListeningArticle, publicationId: string, publicationName: string, origin: string): {
332
+ publishedAt?: string | undefined;
333
+ section?: string | undefined;
334
+ durationSeconds?: number | undefined;
335
+ revision?: string | undefined;
336
+ id: string;
337
+ publicationId: string;
338
+ publicationName: string;
339
+ title: string;
340
+ articleUrl: string;
341
+ storyId: string;
342
+ };
343
+ /** Bounded public read fan-out, initiated only when a reader requests a catch-up queue. */
344
+ declare function publicationListeningCandidates<T extends ListeningArticle>(items: T[], read: (slug: string) => Promise<ListeningArticle>, publicationId: string, publicationName: string, origin: string): Promise<{
345
+ publishedAt?: string | undefined;
346
+ section?: string | undefined;
347
+ durationSeconds?: number | undefined;
348
+ revision?: string | undefined;
349
+ id: string;
350
+ publicationId: string;
351
+ publicationName: string;
352
+ title: string;
353
+ articleUrl: string;
354
+ storyId: string;
355
+ }[]>;
356
+
357
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, type ListeningArticle, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/dist/index.d.ts CHANGED
@@ -102,6 +102,16 @@ declare function defaultRevalidateTargets(): {
102
102
  paths: string[];
103
103
  };
104
104
 
105
+ /**
106
+ * Canonical reader-facing order for editorial sections across the network.
107
+ * Names not listed here sort after these, alphabetically. "Schools" and
108
+ * "Opinion" are reserved so future sections slot in without a release.
109
+ */
110
+ declare const EDITORIAL_SECTION_ORDER: readonly ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health", "Schools", "Opinion"];
111
+ /** Sorts sections into the canonical order, dropping the implicit "Local news" bucket. */
112
+ declare function orderedSections<T extends {
113
+ name: string;
114
+ }>(sections: T[], order?: readonly string[]): T[];
105
115
  declare function sectionId(name: string): string;
106
116
  declare function sectionPath(name: string): string;
107
117
  declare function sectionDefinition(name: string): EditorialSection;
@@ -301,4 +311,47 @@ declare function readerSources(input: unknown): {
301
311
  publisher?: string;
302
312
  }[];
303
313
 
304
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CouponOffer, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, publicHttps, publicUrl, publicationJsonLd, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
314
+ interface ListeningArticle {
315
+ id: string;
316
+ slug: string;
317
+ title: string;
318
+ canonical_url?: string | null;
319
+ section?: string | null;
320
+ published_at?: string;
321
+ editorial?: {
322
+ duplicate_url?: string;
323
+ modified_at?: string;
324
+ } | null;
325
+ audio?: {
326
+ url?: string | null;
327
+ duration_ms?: number | null;
328
+ };
329
+ }
330
+ /** Public metadata only. Never cache signed recording URLs in reader history. */
331
+ declare function publicationListeningTrack(article: ListeningArticle, publicationId: string, publicationName: string, origin: string): {
332
+ publishedAt?: string | undefined;
333
+ section?: string | undefined;
334
+ durationSeconds?: number | undefined;
335
+ revision?: string | undefined;
336
+ id: string;
337
+ publicationId: string;
338
+ publicationName: string;
339
+ title: string;
340
+ articleUrl: string;
341
+ storyId: string;
342
+ };
343
+ /** Bounded public read fan-out, initiated only when a reader requests a catch-up queue. */
344
+ declare function publicationListeningCandidates<T extends ListeningArticle>(items: T[], read: (slug: string) => Promise<ListeningArticle>, publicationId: string, publicationName: string, origin: string): Promise<{
345
+ publishedAt?: string | undefined;
346
+ section?: string | undefined;
347
+ durationSeconds?: number | undefined;
348
+ revision?: string | undefined;
349
+ id: string;
350
+ publicationId: string;
351
+ publicationName: string;
352
+ title: string;
353
+ articleUrl: string;
354
+ storyId: string;
355
+ }[]>;
356
+
357
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, COOKIE_CATEGORIES, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, type ListeningArticle, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, clearTheme, composeEdition, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { CLASSIFIED_CATEGORIES, JOB_EMPLOYMENT_TYPES, categoryLabel, classifiedP
6
6
  export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-GB36YM56.js';
7
7
  export { CookieConsentBanner } from './chunk-HTMIFWNA.js';
8
8
  export { CookieSettingsView } from './chunk-3MFKPHO3.js';
9
+ import { localPublications } from './chunk-2IYYEFLD.js';
9
10
  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, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, communityPublications, decorateNetworkUrl, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, hasConsented, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from './chunk-2IYYEFLD.js';
10
11
  import { jsxs, jsx } from 'react/jsx-runtime';
11
12
 
@@ -18,7 +19,7 @@ var walnutCreekTimes = definePublication({
18
19
  regionLabel: "Walnut Creek",
19
20
  towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
20
21
  nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
21
- sections: ["Civic life", "Community", "Local businesses", "Events", "Food & places"],
22
+ sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
22
23
  description: "Local news, independent businesses and community voices from Walnut Creek, Rossmoor, Saranap and Pleasant Hill.",
23
24
  layout: "broadsheet",
24
25
  layouts: ["broadsheet"],
@@ -110,9 +111,30 @@ function defaultRevalidateTargets() {
110
111
 
111
112
  // src/editorial.ts
112
113
  var labels = {
113
- "Local businesses": "Local business",
114
+ "Local businesses": "Business",
115
+ "Events": "Things to do",
114
116
  "Food & places": "Food & drink"
115
117
  };
118
+ var EDITORIAL_SECTION_ORDER = [
119
+ "Civic life",
120
+ "Community",
121
+ "Local businesses",
122
+ "Real Estate",
123
+ "Food & places",
124
+ "Events",
125
+ "Health",
126
+ "Schools",
127
+ "Opinion"
128
+ ];
129
+ function orderedSections(sections, order = EDITORIAL_SECTION_ORDER) {
130
+ return sections.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
131
+ const leftRank = order.indexOf(left.name);
132
+ const rightRank = order.indexOf(right.name);
133
+ const leftOrder = leftRank === -1 ? order.length : leftRank;
134
+ const rightOrder = rightRank === -1 ? order.length : rightRank;
135
+ return leftOrder - rightOrder || left.name.localeCompare(right.name);
136
+ });
137
+ }
116
138
  function sectionId(name) {
117
139
  return "section-" + name.normalize("NFKD").replace(/\p{Mark}/gu, "").toLowerCase().replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-|-$/g, "");
118
140
  }
@@ -496,4 +518,55 @@ function readerSources(input) {
496
518
  return output;
497
519
  }
498
520
 
499
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EVENTS, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, composeEdition, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, publicHttps, publicUrl, publicationJsonLd, publicationMeta, readerSources, referralCode, resolveCoupon, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
521
+ // src/listening.ts
522
+ function publicationListeningTrack(article, publicationId, publicationName, origin) {
523
+ const articleUrl2 = new URL(`/news/${encodeURIComponent(article.slug)}`, origin).href;
524
+ let storyId = articleUrl2;
525
+ if (article.canonical_url) {
526
+ try {
527
+ const canonical = new URL(article.canonical_url);
528
+ if (canonical.protocol === "https:" && !canonical.username && !canonical.password && /^\/news\/[^/]+\/?$/.test(canonical.pathname) && localPublications.some((item) => new URL(item.url).hostname === canonical.hostname.replace(/^www\./, ""))) {
529
+ canonical.search = "";
530
+ canonical.hash = "";
531
+ canonical.hostname = canonical.hostname.replace(/^www\./, "");
532
+ canonical.pathname = canonical.pathname.replace(/\/$/, "");
533
+ storyId = canonical.href;
534
+ }
535
+ } catch {
536
+ }
537
+ }
538
+ const revision = article.editorial?.modified_at;
539
+ return {
540
+ id: article.id,
541
+ publicationId,
542
+ publicationName,
543
+ title: article.title,
544
+ articleUrl: articleUrl2,
545
+ storyId,
546
+ ...revision && Number.isFinite(Date.parse(revision)) ? { revision } : {},
547
+ ...article.audio?.duration_ms && article.audio.duration_ms > 0 ? { durationSeconds: article.audio.duration_ms / 1e3 } : {},
548
+ ...article.section ? { section: article.section } : {},
549
+ ...article.published_at ? { publishedAt: article.published_at } : {}
550
+ };
551
+ }
552
+ async function publicationListeningCandidates(items, read, publicationId, publicationName, origin) {
553
+ const output = [];
554
+ const recent = items.filter((item) => !item.editorial?.duplicate_url).slice(0, 30);
555
+ let failures = 0;
556
+ for (let i = 0; i < recent.length; i += 4) {
557
+ const batch = await Promise.allSettled(recent.slice(i, i + 4).map((item) => read(item.slug)));
558
+ for (const result of batch) {
559
+ if (result.status === "rejected") {
560
+ failures++;
561
+ continue;
562
+ }
563
+ const article = result.value;
564
+ if (!article.editorial?.duplicate_url && article.audio?.url && article.audio.duration_ms && article.audio.duration_ms > 0)
565
+ output.push(publicationListeningTrack(article, publicationId, publicationName, origin));
566
+ }
567
+ }
568
+ if (!output.length && failures) throw new Error("Audio inventory could not be loaded");
569
+ return output.sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""));
570
+ }
571
+
572
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, composeEdition, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, referralCode, resolveCoupon, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
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",