@scalemule/news-publication 0.0.45 → 0.0.47

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.
Files changed (34) hide show
  1. package/README.md +69 -0
  2. package/dist/{chunk-OIEO2YVG.js → chunk-2XSW7JZS.js} +1 -1
  3. package/dist/chunk-66GI2IX6.js +65 -0
  4. package/dist/{chunk-2IYKWGVM.js → chunk-GOWOYMSG.js} +13 -45
  5. package/dist/{chunk-UTBV6JJX.js → chunk-JV76ZEIA.js} +12 -27
  6. package/dist/{chunk-72PKW2HM.js → chunk-UL2Q3N26.js} +1 -1
  7. package/dist/{chunk-GVLJ3HAI.js → chunk-WAC6ATNP.js} +1 -1
  8. package/dist/cookie-consent-banner.cjs +13 -78
  9. package/dist/cookie-consent-banner.js +2 -2
  10. package/dist/cookie-settings-view.cjs +11 -26
  11. package/dist/cookie-settings-view.js +2 -2
  12. package/dist/index.cjs +405 -180
  13. package/dist/index.d.cts +143 -4
  14. package/dist/index.d.ts +143 -4
  15. package/dist/index.js +318 -117
  16. package/dist/network-bar.js +2 -2
  17. package/dist/network-copyright.js +2 -2
  18. package/dist/network.js +1 -1
  19. package/dist/{snapshot-CxuyMF39.d.ts → snapshot-DmRw8G_B.d.ts} +1 -1
  20. package/dist/{snapshot-C0IGpt-B.d.cts → snapshot-Dx4Lv-lB.d.cts} +1 -1
  21. package/dist/snapshot-file.d.cts +2 -2
  22. package/dist/snapshot-file.d.ts +2 -2
  23. package/dist/sponsorship-view.cjs +75 -0
  24. package/dist/sponsorship-view.d.cts +11 -0
  25. package/dist/sponsorship-view.d.ts +11 -0
  26. package/dist/sponsorship-view.js +62 -0
  27. package/dist/sponsorships-CtnBe3S8.d.cts +137 -0
  28. package/dist/sponsorships-CtnBe3S8.d.ts +137 -0
  29. package/dist/{types-DFDCZHbS.d.cts → types-BJFCWWi0.d.cts} +1 -1
  30. package/dist/{types-DFDCZHbS.d.ts → types-BJFCWWi0.d.ts} +1 -1
  31. package/dist/verified-card.d.cts +1 -1
  32. package/dist/verified-card.d.ts +1 -1
  33. package/package.json +11 -4
  34. package/sponsorship.css +78 -0
package/dist/index.js CHANGED
@@ -1,119 +1,20 @@
1
+ export { NetworkCopyrightNotice } from './chunk-WAC6ATNP.js';
1
2
  export { CURATED_COMMUNITY_CARDS, VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './chunk-FMCHQI7K.js';
2
- export { CookieConsentBanner } from './chunk-2IYKWGVM.js';
3
- export { CookieSettingsView } from './chunk-72PKW2HM.js';
3
+ export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-GB36YM56.js';
4
+ export { CookieConsentBanner } from './chunk-GOWOYMSG.js';
5
+ export { CookieSettingsView } from './chunk-UL2Q3N26.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
- export { LocalNetworkBar } from './chunk-OIEO2YVG.js';
6
- export { NetworkCopyrightNotice } from './chunk-GVLJ3HAI.js';
7
- import { localPublications } from './chunk-UTBV6JJX.js';
8
- 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, decorateNetworkUrl, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, hasConsented, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from './chunk-UTBV6JJX.js';
7
+ export { LocalNetworkBar } from './chunk-2XSW7JZS.js';
8
+ import { localPublications } from './chunk-JV76ZEIA.js';
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, decorateNetworkUrl, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, hasConsented, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from './chunk-JV76ZEIA.js';
10
+ export { SPONSORSHIP_OFFERINGS, SPONSORSHIP_POLICY, placementAvailable, safeSponsorUrl } from './chunk-66GI2IX6.js';
9
11
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
10
12
  export { formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-OISLCYER.js';
11
13
  import { definePublication } from './chunk-57YVL6BD.js';
12
14
  export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-57YVL6BD.js';
13
15
  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-GB36YM56.js';
15
16
  import { jsxs, jsx } from 'react/jsx-runtime';
16
17
 
17
- // src/preset.ts
18
- var walnutCreekTimes = definePublication({
19
- slug: "walnutcreektimes",
20
- name: "Walnut Creek Times",
21
- masthead: ["Walnut", "Creek", "Times"],
22
- region: "Walnut Creek",
23
- regionLabel: "Walnut Creek",
24
- towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
25
- nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
26
- sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
27
- description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
28
- tagline: "Local stories. Stronger communities.",
29
- layout: "broadsheet",
30
- layouts: ["broadsheet"],
31
- features: { smile: true, jobs: true, classifieds: true },
32
- modules: { home: ["daily-smile"] }
33
- });
34
-
35
- // src/cache.ts
36
- var NEWS_CACHE = {
37
- feedRevalidateSeconds: 30,
38
- articleRevalidateSeconds: 60,
39
- edgeMaxAgeSeconds: 60,
40
- staleWhileRevalidateSeconds: 300,
41
- feedTag: "news-feed",
42
- articleTag: "news-article",
43
- inventoryTag: "public-news-discovery-v2",
44
- paths: ["/", "/news"]
45
- };
46
- var feedCacheOptions = {
47
- revalidate: NEWS_CACHE.feedRevalidateSeconds,
48
- tags: [NEWS_CACHE.feedTag]
49
- };
50
- var articleCacheOptions = {
51
- revalidate: NEWS_CACHE.articleRevalidateSeconds,
52
- tags: [NEWS_CACHE.articleTag]
53
- };
54
- var inventoryCacheOptions = {
55
- revalidate: NEWS_CACHE.feedRevalidateSeconds,
56
- tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
57
- };
58
- function edgeCacheControl() {
59
- return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
60
- }
61
- function feedCacheKey(publicationId, before, section) {
62
- return `${publicationId}:${before ?? ""}:${section ?? ""}`;
63
- }
64
- function articleCacheKey(publicationId, slug) {
65
- return `${publicationId}:${slug}`;
66
- }
67
- var NewsError = class extends Error {
68
- constructor(status, message) {
69
- super(message);
70
- this.status = status;
71
- this.name = "NewsError";
72
- }
73
- status;
74
- };
75
- function shouldServeLastKnown(error) {
76
- const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
77
- return status !== 404;
78
- }
79
- async function withLastKnown(store, key, load, accept = () => true) {
80
- try {
81
- const value = await load();
82
- if (accept(value)) store.set(key, value);
83
- return value;
84
- } catch (error) {
85
- if (!shouldServeLastKnown(error)) throw error;
86
- const cached = store.get(key);
87
- if (cached) return cached;
88
- throw error;
89
- }
90
- }
91
- function secretsMatch(provided, secret) {
92
- if (!provided || !secret) return false;
93
- const left = Array.from(provided);
94
- const right = Array.from(secret);
95
- let mismatch = left.length === right.length ? 0 : 1;
96
- const length = Math.max(left.length, right.length);
97
- for (let index = 0; index < length; index += 1) {
98
- mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
99
- }
100
- return mismatch === 0;
101
- }
102
- function revalidateDecision(input) {
103
- if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
104
- return { status: 401, body: { error: "Unauthorized" } };
105
- }
106
- const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
107
- const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
108
- return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
109
- }
110
- function defaultRevalidateTargets() {
111
- return {
112
- tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
113
- paths: [...NEWS_CACHE.paths]
114
- };
115
- }
116
-
117
18
  // src/editorial.ts
118
19
  var labels = {
119
20
  "Local businesses": "Business",
@@ -131,8 +32,8 @@ var EDITORIAL_SECTION_ORDER = [
131
32
  "Schools",
132
33
  "Opinion"
133
34
  ];
134
- function orderedSections(sections, order = EDITORIAL_SECTION_ORDER) {
135
- return sections.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
35
+ function orderedSections(sections2, order = EDITORIAL_SECTION_ORDER) {
36
+ return sections2.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
136
37
  const leftRank = order.indexOf(left.name);
137
38
  const rightRank = order.indexOf(right.name);
138
39
  const leftOrder = leftRank === -1 ? order.length : leftRank;
@@ -193,12 +94,12 @@ function composeEdition(items, selection = {}, now = Date.now()) {
193
94
  secondary.push(deferredWithDuplicateImage.shift());
194
95
  }
195
96
  for (const item of secondary) remaining.splice(remaining.indexOf(item), 1);
196
- const eligible = remaining.filter((item) => {
97
+ const eligible2 = remaining.filter((item) => {
197
98
  const age = now - Date.parse(item.published_at);
198
99
  return item.excerpt?.trim() && age >= 0 && age <= 7 * 864e5;
199
100
  });
200
- const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible.find((item) => item.slug === slug) ?? []);
201
- const candidates = [...selected, ...editorialSections(eligible).map((group) => group.items[0]), ...eligible];
101
+ const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible2.find((item) => item.slug === slug) ?? []);
102
+ const candidates = [...selected, ...editorialSections(eligible2).map((group) => group.items[0]), ...eligible2];
202
103
  const briefIds = /* @__PURE__ */ new Set();
203
104
  const briefs = candidates.filter((item) => {
204
105
  if (!item || briefIds.has(item.id) || briefIds.size >= 4) return false;
@@ -207,8 +108,8 @@ function composeEdition(items, selection = {}, now = Date.now()) {
207
108
  });
208
109
  const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
209
110
  const compactIds = new Set(compact.map((item) => item.id));
210
- const sections = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
211
- return { lead, secondary, briefs, compact, sections, navigation: editorialSections(inventory) };
111
+ const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
112
+ return { lead, secondary, briefs, compact, sections: sections2, navigation: editorialSections(inventory) };
212
113
  }
213
114
  function displayHeadline(article, selection = {}) {
214
115
  return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
@@ -275,8 +176,8 @@ function shortDeck(value, limit = 160) {
275
176
  return (space > opening.length / 2 ? opening.slice(0, space) : opening).trimEnd() + "\u2026";
276
177
  }
277
178
  function storyDate(value) {
278
- const date = new Date(value);
279
- return Number.isNaN(date.getTime()) ? null : date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "America/Los_Angeles" });
179
+ const date2 = new Date(value);
180
+ return Number.isNaN(date2.getTime()) ? null : date2.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "America/Los_Angeles" });
280
181
  }
281
182
 
282
183
  // src/seo.ts
@@ -373,6 +274,265 @@ function llmsText(publication, canonical) {
373
274
  ].join("\n");
374
275
  }
375
276
 
277
+ // src/briefing.ts
278
+ var LOCAL_BRIEFING_POLICY_VERSION = "1";
279
+ var DAY = 864e5;
280
+ var verifications = /* @__PURE__ */ new Set([
281
+ "unverified",
282
+ "community_submitted",
283
+ "officially_confirmed",
284
+ "independently_verified",
285
+ "disputed"
286
+ ]);
287
+ var sections = /* @__PURE__ */ new Set(["important", "around_you", "people", "discover", "participate", "following"]);
288
+ var overlaps = (a, b = []) => a.some((id) => id.trim() !== "" && b.includes(id));
289
+ function timestamp(value) {
290
+ if (!/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,3})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(value) || !validCalendarDate(value.slice(0, 10))) return NaN;
291
+ return Date.parse(value);
292
+ }
293
+ function sourcesFor(candidate) {
294
+ const seen = /* @__PURE__ */ new Set();
295
+ return candidate.sources.flatMap((source) => {
296
+ const url = publicHttps(source.url);
297
+ if (!source.id.trim() || !source.publisher.trim() || !source.title.trim() || !url || seen.has(source.id)) return [];
298
+ seen.add(source.id);
299
+ return [{ id: source.id, url, title: source.title, publisher: source.publisher }];
300
+ });
301
+ }
302
+ function eligible(candidate, coverage, now) {
303
+ if (candidate.publication_status !== "published" || candidate.visibility !== "public" || candidate.editorial_approval !== "approved" || candidate.lifecycle !== "current" || !candidate.article.id.trim() || !candidate.article.slug.trim() || !candidate.article.title.trim() || !verifications.has(candidate.verification) || !sections.has(candidate.section) || !overlaps(candidate.geography_ids, coverage)) return false;
304
+ const published = timestamp(candidate.article.published_at);
305
+ const expiry = timestamp(candidate.relevant_until);
306
+ if (!Number.isFinite(published) || published > now || !Number.isFinite(expiry) || expiry <= now) return false;
307
+ const verified = candidate.last_verified_at === null ? null : timestamp(candidate.last_verified_at);
308
+ const review = candidate.review_due_at === null ? null : timestamp(candidate.review_due_at);
309
+ if (verified !== null && (!Number.isFinite(verified) || verified > now)) return false;
310
+ if (review !== null && (!Number.isFinite(review) || review <= now || verified !== null && review <= verified)) return false;
311
+ const confirmed = candidate.verification === "officially_confirmed" || candidate.verification === "independently_verified";
312
+ return !confirmed || verified !== null && review !== null && sourcesFor(candidate).length > 0;
313
+ }
314
+ function projectionKey(candidate) {
315
+ const article = candidate.article;
316
+ return JSON.stringify([
317
+ article.id,
318
+ article.slug,
319
+ article.title,
320
+ article.excerpt,
321
+ article.published_at,
322
+ article.author_name,
323
+ publicHttps(article.canonical_url),
324
+ [...candidate.geography_ids].sort(),
325
+ [...candidate.topic_ids].sort(),
326
+ [...candidate.entity_ids].sort(),
327
+ [...candidate.issue_ids].sort(),
328
+ candidate.section,
329
+ candidate.civic_importance,
330
+ candidate.practical_value,
331
+ candidate.discovery_value,
332
+ candidate.why_it_matters,
333
+ candidate.relevant_until,
334
+ candidate.verification,
335
+ candidate.last_verified_at,
336
+ candidate.review_due_at,
337
+ sourcesFor(candidate).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
338
+ ]);
339
+ }
340
+ function composeLocalBriefing(candidates, options) {
341
+ const now = options.now ?? Date.now();
342
+ const limit = options.limit ?? 6;
343
+ if (!options.sm_application_id.trim() || !options.publication_id.trim() || !Number.isFinite(now) || !Number.isFinite(new Date(now).getTime()) || !Number.isInteger(limit) || limit < 1 || limit > 12) {
344
+ throw new RangeError("A briefing requires application/publication scope, a valid time and a limit from 1 to 12");
345
+ }
346
+ const preferences = options.personalized === false ? {} : options.preferences ?? {};
347
+ const personalized = [preferences.community_ids, preferences.topic_ids, preferences.entity_ids, preferences.issue_ids].some((ids) => ids?.some((id) => id.trim() !== ""));
348
+ const newest = /* @__PURE__ */ new Map();
349
+ for (const candidate of candidates) {
350
+ if (candidate.sm_application_id !== options.sm_application_id || candidate.publication_id !== options.publication_id || !candidate.canonical_key.trim() || !Number.isSafeInteger(candidate.revision) || candidate.revision < 1) continue;
351
+ const group = newest.get(candidate.canonical_key);
352
+ if (!group || candidate.revision > group[0].revision) newest.set(candidate.canonical_key, [candidate]);
353
+ else if (candidate.revision === group[0].revision) group.push(candidate);
354
+ }
355
+ const ranked = [];
356
+ for (const group of newest.values()) {
357
+ if (group.some((candidate2) => !eligible(candidate2, options.coverage_area_ids, now))) continue;
358
+ const candidate = group[0];
359
+ if (group.length > 1 && group.some((copy) => projectionKey(copy) !== projectionKey(candidate))) continue;
360
+ const summary = candidate.article.excerpt?.trim() ?? "";
361
+ if (!summary || Array.from(summary).length > 280 || !/[.!?。!?][”’"']?$/u.test(summary)) continue;
362
+ const community = overlaps(candidate.geography_ids, preferences.community_ids);
363
+ const entity = overlaps(candidate.entity_ids, preferences.entity_ids);
364
+ const issue = overlaps(candidate.issue_ids, preferences.issue_ids);
365
+ const topic = overlaps(candidate.topic_ids, preferences.topic_ids);
366
+ const published = timestamp(candidate.article.published_at);
367
+ const age = now - published;
368
+ const important = candidate.civic_importance === "important";
369
+ const relevance = {
370
+ geographic: community ? 2 : 1,
371
+ practical: candidate.practical_value ? 1 : 0,
372
+ relationship: entity || issue ? 1 : 0,
373
+ interest: topic ? 1 : 0,
374
+ temporal: age < DAY ? 2 : age < 7 * DAY ? 1 : 0,
375
+ civic: important ? 1 : 0,
376
+ discovery: candidate.discovery_value ? 1 : 0
377
+ };
378
+ const reasons = [];
379
+ if (important) reasons.push("important_civic_information");
380
+ reasons.push(community ? "selected_community" : "publication_coverage");
381
+ if (candidate.practical_value) reasons.push("practical_value");
382
+ if (entity) reasons.push("following_entity");
383
+ if (issue) reasons.push("following_issue");
384
+ if (topic) reasons.push("selected_topic");
385
+ if (relevance.temporal > 0) reasons.push("recent_reporting");
386
+ if (candidate.discovery_value) reasons.push("local_discovery");
387
+ const article = candidate.article;
388
+ const item = {
389
+ canonical_key: candidate.canonical_key,
390
+ revision: candidate.revision,
391
+ article: {
392
+ id: article.id,
393
+ slug: article.slug,
394
+ title: article.title,
395
+ published_at: article.published_at,
396
+ author_name: article.author_name,
397
+ canonical_url: publicHttps(article.canonical_url) ?? null
398
+ },
399
+ summary,
400
+ section: important ? "important" : entity || issue ? "following" : candidate.section,
401
+ geography_ids: [...new Set(candidate.geography_ids)],
402
+ why_it_matters: candidate.why_it_matters?.trim() || void 0,
403
+ reasons,
404
+ relevance,
405
+ verification: candidate.verification,
406
+ freshness: candidate.last_verified_at !== null && candidate.review_due_at !== null ? "current" : "unknown",
407
+ last_verified_at: candidate.last_verified_at,
408
+ review_due_at: candidate.review_due_at,
409
+ relevant_until: candidate.relevant_until,
410
+ sources: sourcesFor(candidate)
411
+ };
412
+ const score = 4 * relevance.geographic + 3 * relevance.practical + 3 * relevance.relationship + 2 * relevance.interest + relevance.temporal + relevance.discovery;
413
+ ranked.push({ item, score, published, important });
414
+ }
415
+ ranked.sort((a, b) => Number(b.important) - Number(a.important) || b.score - a.score || b.published - a.published || (a.item.canonical_key < b.item.canonical_key ? -1 : a.item.canonical_key > b.item.canonical_key ? 1 : 0));
416
+ const seenArticles = /* @__PURE__ */ new Set();
417
+ const seenUrls = /* @__PURE__ */ new Set();
418
+ const deduplicated = ranked.filter(({ item }) => {
419
+ const url = item.article.canonical_url;
420
+ if (seenArticles.has(item.article.id) || url && seenUrls.has(url)) return false;
421
+ seenArticles.add(item.article.id);
422
+ if (url) seenUrls.add(url);
423
+ return true;
424
+ });
425
+ return {
426
+ schema_version: 1,
427
+ policy_version: LOCAL_BRIEFING_POLICY_VERSION,
428
+ publication_id: options.publication_id,
429
+ generated_at: new Date(now).toISOString(),
430
+ personalized,
431
+ items: deduplicated.slice(0, limit).map(({ item }) => item),
432
+ remaining_important_count: deduplicated.slice(limit).filter(({ important }) => important).length
433
+ };
434
+ }
435
+
436
+ // src/preset.ts
437
+ var walnutCreekTimes = definePublication({
438
+ slug: "walnutcreektimes",
439
+ name: "Walnut Creek Times",
440
+ masthead: ["Walnut", "Creek", "Times"],
441
+ region: "Walnut Creek",
442
+ regionLabel: "Walnut Creek",
443
+ towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
444
+ nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
445
+ sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
446
+ description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
447
+ tagline: "Local stories. Stronger communities.",
448
+ layout: "broadsheet",
449
+ layouts: ["broadsheet"],
450
+ features: { smile: true, jobs: true, classifieds: true },
451
+ modules: { home: ["daily-smile"] }
452
+ });
453
+
454
+ // src/cache.ts
455
+ var NEWS_CACHE = {
456
+ feedRevalidateSeconds: 30,
457
+ articleRevalidateSeconds: 60,
458
+ edgeMaxAgeSeconds: 60,
459
+ staleWhileRevalidateSeconds: 300,
460
+ feedTag: "news-feed",
461
+ articleTag: "news-article",
462
+ inventoryTag: "public-news-discovery-v2",
463
+ paths: ["/", "/news"]
464
+ };
465
+ var feedCacheOptions = {
466
+ revalidate: NEWS_CACHE.feedRevalidateSeconds,
467
+ tags: [NEWS_CACHE.feedTag]
468
+ };
469
+ var articleCacheOptions = {
470
+ revalidate: NEWS_CACHE.articleRevalidateSeconds,
471
+ tags: [NEWS_CACHE.articleTag]
472
+ };
473
+ var inventoryCacheOptions = {
474
+ revalidate: NEWS_CACHE.feedRevalidateSeconds,
475
+ tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
476
+ };
477
+ function edgeCacheControl() {
478
+ return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
479
+ }
480
+ function feedCacheKey(publicationId, before, section) {
481
+ return `${publicationId}:${before ?? ""}:${section ?? ""}`;
482
+ }
483
+ function articleCacheKey(publicationId, slug) {
484
+ return `${publicationId}:${slug}`;
485
+ }
486
+ var NewsError = class extends Error {
487
+ constructor(status, message) {
488
+ super(message);
489
+ this.status = status;
490
+ this.name = "NewsError";
491
+ }
492
+ status;
493
+ };
494
+ function shouldServeLastKnown(error) {
495
+ const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
496
+ return status !== 404;
497
+ }
498
+ async function withLastKnown(store, key, load, accept = () => true) {
499
+ try {
500
+ const value = await load();
501
+ if (accept(value)) store.set(key, value);
502
+ return value;
503
+ } catch (error) {
504
+ if (!shouldServeLastKnown(error)) throw error;
505
+ const cached = store.get(key);
506
+ if (cached) return cached;
507
+ throw error;
508
+ }
509
+ }
510
+ function secretsMatch(provided, secret) {
511
+ if (!provided || !secret) return false;
512
+ const left = Array.from(provided);
513
+ const right = Array.from(secret);
514
+ let mismatch = left.length === right.length ? 0 : 1;
515
+ const length = Math.max(left.length, right.length);
516
+ for (let index = 0; index < length; index += 1) {
517
+ mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
518
+ }
519
+ return mismatch === 0;
520
+ }
521
+ function revalidateDecision(input) {
522
+ if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
523
+ return { status: 401, body: { error: "Unauthorized" } };
524
+ }
525
+ const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
526
+ const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
527
+ return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
528
+ }
529
+ function defaultRevalidateTargets() {
530
+ return {
531
+ tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
532
+ paths: [...NEWS_CACHE.paths]
533
+ };
534
+ }
535
+
376
536
  // src/analytics.ts
377
537
  var ANALYTICS_VERSION = 1;
378
538
  var ANALYTICS_COOKIE = "napsite_analytics_v1";
@@ -593,4 +753,45 @@ async function publicationListeningCandidates(items, read, publicationId, public
593
753
  return output.sort((a, b) => (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""));
594
754
  }
595
755
 
596
- 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 };
756
+ // src/measurement-consent.ts
757
+ function measurementConsent(headers, sponsorship = false, now = Date.now()) {
758
+ if (headers.get("dnt") === "1" || headers.get("sec-gpc") === "1")
759
+ return false;
760
+ const cookies = headers.get("cookie")?.split(";").map((value) => value.trim()) ?? [];
761
+ const cookie = cookies.find((value) => value.startsWith("sm_cookie_consent_v1=")) ?? cookies.find((value) => value.startsWith("napsite_cookie_consent_v1="));
762
+ if (!cookie) return false;
763
+ try {
764
+ const record = JSON.parse(
765
+ decodeURIComponent(cookie.substring(cookie.indexOf("=") + 1))
766
+ );
767
+ const date2 = Date.parse(record.timestamp);
768
+ return record.version === 1 && record.preferences?.essential === true && typeof record.preferences?.functional === "boolean" && typeof record.preferences?.advertising === "boolean" && record.preferences?.performance === true && (!sponsorship || record.preferences?.advertising === true) && Number.isFinite(date2) && date2 <= now + 3e5 && now - date2 < 365 * 864e5;
769
+ } catch {
770
+ return false;
771
+ }
772
+ }
773
+
774
+ // src/sponsorship-report.ts
775
+ var escape = (text) => String(text ?? "").replace(
776
+ /[&<>"']/g,
777
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
778
+ );
779
+ var date = (value) => new Date(value).toLocaleString("en-US", {
780
+ timeZone: "UTC",
781
+ dateStyle: "medium",
782
+ timeStyle: "short"
783
+ }) + " UTC";
784
+ function metric(value) {
785
+ if (value.status === "unavailable" || value.value === null)
786
+ return "Unavailable";
787
+ return `${escape(value.value.toLocaleString("en-US"))} <small>${escape(value.status)}</small>${value.from && value.to ? `<small>Observed period:<br>${escape(date(value.from))}<br>to ${escape(date(value.to))}</small>` : ""}`;
788
+ }
789
+ function campaignReportHtml(report) {
790
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escape(report.campaign_name)} | Sponsorship report</title><style>
791
+ @page{size:Letter;margin:18mm}*{box-sizing:border-box}body{font:11pt/1.5 Arial,sans-serif;color:#172d37;margin:0}h1,h2{font-family:Georgia,serif;font-weight:normal}h1{font-size:30pt;line-height:1.12;margin:16pt 0}h2{font-size:19pt;margin:0 0 8pt}header{border-bottom:2pt solid #173d32;padding-bottom:16pt}.brand{font-size:10pt;letter-spacing:1pt}p{margin:8pt 0}.meta{color:#4e5d64;font-size:10pt}section{break-inside:avoid;margin:24pt 0}table{width:100%;border-collapse:collapse;margin:12pt 0}td,th{text-align:left;border-bottom:1px solid #d8dedc;padding:8pt 5pt}th{font-size:9pt}small{display:block;color:#4e5d64;font-size:8pt}footer{border-top:1px solid #d8dedc;padding-top:12pt;font-size:9pt}.notes{font-size:9pt;color:#4e5d64}
792
+ </style></head><body><header><div class="brand">BAY AREA NEWS NETWORK</div><p>Community sponsorship report</p><h1>${escape(report.campaign_name)}</h1><p>${escape(report.sponsor)}</p><p class="meta">Campaign period: ${escape(date(report.starts_at))} to ${escape(date(report.ends_at))}<br>Status: ${escape(report.status)}<br>Prepared: ${escape(date(report.generated_at))}</p></header>
793
+ ${report.placements.map((p) => `<section><h2>${escape(p.publication)}</h2><p>${escape(p.placement)} \xB7 ${escape(p.product_path)}</p><p class="meta">Community scope: ${escape(p.communities.length ? p.communities.join(", ") : "Publication-wide")}<br>Placement delivery: ${escape(p.delivery_status)}</p><table><thead><tr><th>Observed placement views</th><th>Observed sponsor link clicks</th><th>Newsletter deliveries</th></tr></thead><tbody><tr><td>${metric(p.views)}</td><td>${metric(p.clicks)}</td><td>${metric(p.newsletter_deliveries)}</td></tr></tbody></table><p class="notes">Views: ${escape(p.views.methodology)}<br>Clicks: ${escape(p.clicks.methodology)}<br>Newsletter: ${escape(p.newsletter_deliveries.methodology)}</p></section>`).join("")}
794
+ <footer><p>${escape(report.reporting_note)}</p><p>${escape(report.editorial_independence)}</p><p>Campaign reference: ${escape(report.campaign_id)}</p></footer></body></html>`;
795
+ }
796
+
797
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, LOCAL_BRIEFING_POLICY_VERSION, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, composeEdition, composeLocalBriefing, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, 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 };
@@ -1,2 +1,2 @@
1
- export { LocalNetworkBar } from './chunk-OIEO2YVG.js';
2
- import './chunk-UTBV6JJX.js';
1
+ export { LocalNetworkBar } from './chunk-2XSW7JZS.js';
2
+ import './chunk-JV76ZEIA.js';
@@ -1,2 +1,2 @@
1
- export { NetworkCopyrightNotice } from './chunk-GVLJ3HAI.js';
2
- import './chunk-UTBV6JJX.js';
1
+ export { NetworkCopyrightNotice } from './chunk-WAC6ATNP.js';
2
+ import './chunk-JV76ZEIA.js';
package/dist/network.js CHANGED
@@ -1 +1 @@
1
- export { EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications } from './chunk-UTBV6JJX.js';
1
+ export { EDITORIAL_PRINCIPLES, NETWORK_PARENT, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, isNetworkHostname, listedPublications, localPublications } from './chunk-JV76ZEIA.js';
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-DFDCZHbS.js';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-BJFCWWi0.js';
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-DFDCZHbS.cjs';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-BJFCWWi0.cjs';
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-C0IGpt-B.cjs';
2
- import './types-DFDCZHbS.cjs';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-Dx4Lv-lB.cjs';
2
+ import './types-BJFCWWi0.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-CxuyMF39.js';
2
- import './types-DFDCZHbS.js';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-DmRw8G_B.js';
2
+ import './types-BJFCWWi0.js';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+
5
+ // src/sponsorships.ts
6
+ function safeSponsorUrl(value) {
7
+ if (typeof value !== "string" || value.length > 2048) return void 0;
8
+ try {
9
+ const url = new URL(value);
10
+ if (url.protocol !== "https:" || url.username || url.password)
11
+ return void 0;
12
+ return url.toString();
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+ function SponsorAcknowledgment({
18
+ creative,
19
+ label = "Community Partner",
20
+ preview = false,
21
+ acknowledgment
22
+ }) {
23
+ const logo = safeSponsorUrl(creative.logo_url);
24
+ const destination = safeSponsorUrl(creative.destination_url);
25
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26
+ "aside",
27
+ {
28
+ className: "sm-sponsorship",
29
+ "aria-label": `${label}: ${creative.name}`,
30
+ "data-sponsorship": preview ? void 0 : acknowledgment?.campaign_id,
31
+ "data-placement-id": preview ? void 0 : acknowledgment?.placement_id,
32
+ "data-publication-id": preview ? void 0 : acknowledgment?.publication_id,
33
+ "data-sponsor-id": preview ? void 0 : acknowledgment?.sponsor_id,
34
+ children: [
35
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-sponsorship-copy", children: [
36
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "sm-sponsorship-label", children: [
37
+ label,
38
+ preview ? " \xB7 Illustrative preview" : ""
39
+ ] }),
40
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-sponsorship-name", children: creative.name }),
41
+ creative.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-sponsorship-description", children: creative.description }),
42
+ destination && /* @__PURE__ */ jsxRuntime.jsxs(
43
+ "a",
44
+ {
45
+ href: destination,
46
+ rel: "sponsored noopener noreferrer",
47
+ referrerPolicy: "no-referrer",
48
+ "data-analytics-skip": "true",
49
+ children: [
50
+ creative.link_label || "Learn more",
51
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sm-sponsorship-sr", children: [
52
+ " about ",
53
+ creative.name
54
+ ] })
55
+ ]
56
+ }
57
+ )
58
+ ] }),
59
+ logo && /* @__PURE__ */ jsxRuntime.jsx(
60
+ "img",
61
+ {
62
+ src: logo,
63
+ alt: `${creative.name} logo`,
64
+ loading: "lazy",
65
+ decoding: "async",
66
+ referrerPolicy: "no-referrer",
67
+ className: "sm-sponsorship-logo"
68
+ }
69
+ )
70
+ ]
71
+ }
72
+ );
73
+ }
74
+
75
+ exports.SponsorAcknowledgment = SponsorAcknowledgment;
@@ -0,0 +1,11 @@
1
+ import react__default from 'react';
2
+ import { C as Creative, P as PublicAcknowledgment } from './sponsorships-CtnBe3S8.cjs';
3
+
4
+ declare function SponsorAcknowledgment({ creative, label, preview, acknowledgment, }: {
5
+ creative: Creative;
6
+ label?: "Community Partner" | "Sponsored";
7
+ preview?: boolean;
8
+ acknowledgment?: PublicAcknowledgment;
9
+ }): react__default.JSX.Element;
10
+
11
+ export { SponsorAcknowledgment };
@@ -0,0 +1,11 @@
1
+ import react__default from 'react';
2
+ import { C as Creative, P as PublicAcknowledgment } from './sponsorships-CtnBe3S8.js';
3
+
4
+ declare function SponsorAcknowledgment({ creative, label, preview, acknowledgment, }: {
5
+ creative: Creative;
6
+ label?: "Community Partner" | "Sponsored";
7
+ preview?: boolean;
8
+ acknowledgment?: PublicAcknowledgment;
9
+ }): react__default.JSX.Element;
10
+
11
+ export { SponsorAcknowledgment };