@scalemule/news-publication 0.0.81 → 0.0.83

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.js CHANGED
@@ -773,8 +773,11 @@ var EVENTS = {
773
773
  article_shared: browser("engagement"),
774
774
  promotion_viewed: browser("content"),
775
775
  promotion_clicked: browser("engagement"),
776
+ newsletter_invitation_viewed: browser("content"),
777
+ related_stories_viewed: browser("content"),
776
778
  newsletter_signup_started: browser("engagement"),
777
779
  newsletter_subscription_confirmed: server(),
780
+ experiment_exposed: browser("system"),
778
781
  web_vital_recorded: browser("system"),
779
782
  scroll_depth_reached: browser("content"),
780
783
  navigation_clicked: browser("navigation"),
@@ -1020,4 +1023,196 @@ function campaignReportHtml(report) {
1020
1023
  <footer><p>${escape(report.reporting_note)}</p><p>${escape(report.editorial_independence)}</p><p>Campaign reference: ${escape(report.campaign_id)}</p></footer></body></html>`;
1021
1024
  }
1022
1025
 
1023
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, LOCAL_BRIEFING_POLICY_VERSION, NAVIGATION_GROUPS, NETWORK_NAVIGATION, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, applyNavigationConfig, 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, resolveNavigation, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
1026
+ // src/community-conversion.ts
1027
+ var ARTICLE_CONVERSION_EXPERIMENT = "article_conversion_module";
1028
+ var CELEBRATION_PATTERN = /\b(celebrat(?:e|ion|ions|ing)|wedding|weddings|baby|babies|newborn|quarter[- ]?finals?|milestone|milestones|anniversar(?:y|ies)|graduat(?:e|es|ion|ions)|honor(?:s|ed|ing)?|award(?:s|ed)?|champion|champions|hero(?:es)?|little warrior|birthday)\b/iu;
1029
+ var SPONSORED_MARKER_PATTERN = /\b(sponsored|paid post|partner content|advertorial|promoted|brand studio)\b/iu;
1030
+ function resolveArticleConversionVariant(assignedVariant, queryOverride) {
1031
+ const candidate = (queryOverride ?? assignedVariant ?? "").trim().toLowerCase();
1032
+ if (candidate === "treatment" || candidate === "inline" || candidate === "b") {
1033
+ return "treatment";
1034
+ }
1035
+ return "control";
1036
+ }
1037
+ function classifyStoryInvitationCategory(story) {
1038
+ const combinedText = [
1039
+ story.title,
1040
+ story.excerpt ?? "",
1041
+ story.section ?? "",
1042
+ ...(story.tags ?? []).map((t) => t.name),
1043
+ ...(story.categories ?? []).map((c) => c.name)
1044
+ ].join(" ");
1045
+ if (CELEBRATION_PATTERN.test(combinedText)) {
1046
+ return "celebrations";
1047
+ }
1048
+ const section = resolveSection(story);
1049
+ switch (section) {
1050
+ case "Local businesses":
1051
+ return "local_businesses";
1052
+ case "Schools":
1053
+ return "education";
1054
+ case "Events":
1055
+ return "community_events";
1056
+ case "Civic life":
1057
+ case "Real Estate":
1058
+ case "Health":
1059
+ return "neighborhood_news";
1060
+ default:
1061
+ return "community";
1062
+ }
1063
+ }
1064
+ function contextualCommunityInvitation(story, publication) {
1065
+ const category = classifyStoryInvitationCategory(story);
1066
+ const region = publication.regionLabel.trim() || publication.name.trim();
1067
+ const coverage = publication.coverageText?.trim() || (publication.towns && publication.towns.length > 0 ? publication.towns.slice(0, 3).join(", ") : region);
1068
+ switch (category) {
1069
+ case "celebrations":
1070
+ return {
1071
+ category,
1072
+ eyebrow: `${region} Community`,
1073
+ heading: `Stay connected to ${region}`,
1074
+ subprompt: "Discover more people and milestones worth celebrating.",
1075
+ description: `${region} is full of stories worth celebrating. Discover the people, places, events, and milestones shaping ${coverage}.`,
1076
+ ctaLabel: "Subscribe",
1077
+ relatedHeading: `More stories from ${region}`
1078
+ };
1079
+ case "local_businesses":
1080
+ return {
1081
+ category,
1082
+ eyebrow: `${region} Local Businesses`,
1083
+ heading: `Stay connected to ${region}`,
1084
+ subprompt: "Meet the entrepreneurs building your community.",
1085
+ description: `Discover the founders, independent shops, places, and events shaping ${coverage}.`,
1086
+ ctaLabel: "Subscribe",
1087
+ relatedHeading: `More local stories in ${region}`
1088
+ };
1089
+ case "education":
1090
+ return {
1091
+ category,
1092
+ eyebrow: `${region} Schools & Youth`,
1093
+ heading: `Stay connected to ${region}`,
1094
+ subprompt: "Follow achievements from local students and schools.",
1095
+ description: `Discover the students, educators, neighborhood milestones, and events shaping ${coverage}.`,
1096
+ ctaLabel: "Subscribe",
1097
+ relatedHeading: `More stories from ${region}`
1098
+ };
1099
+ case "community_events":
1100
+ return {
1101
+ category,
1102
+ eyebrow: `${region} Events & Gatherings`,
1103
+ heading: `Stay connected to ${region}`,
1104
+ subprompt: "Find something happening near you this weekend.",
1105
+ description: `Discover the people, places, weekend gatherings, and local stories shaping ${coverage}.`,
1106
+ ctaLabel: "Subscribe",
1107
+ relatedHeading: `Happening around ${region}`
1108
+ };
1109
+ case "neighborhood_news":
1110
+ return {
1111
+ category,
1112
+ eyebrow: `${region} Neighborhood Briefing`,
1113
+ heading: `Stay connected to ${region}`,
1114
+ subprompt: "Stay informed about changes affecting your neighborhood.",
1115
+ description: `Follow the civic decisions, neighborhood projects, people, and events shaping ${coverage}.`,
1116
+ ctaLabel: "Subscribe",
1117
+ relatedHeading: `More from ${region}`
1118
+ };
1119
+ case "community":
1120
+ default:
1121
+ return {
1122
+ category: "community",
1123
+ eyebrow: `${region} Community`,
1124
+ heading: `Stay connected to ${region}`,
1125
+ subprompt: "Discover the people, places, and events shaping your community.",
1126
+ description: `Receive local reporting on the people, places, events, and stories shaping ${coverage}.`,
1127
+ ctaLabel: "Subscribe",
1128
+ relatedHeading: `More stories from ${region}`
1129
+ };
1130
+ }
1131
+ }
1132
+ function isSponsoredStoryItem(item) {
1133
+ const markers = [
1134
+ item.section ?? "",
1135
+ ...(item.tags ?? []).map((t) => `${t.name} ${t.slug}`),
1136
+ ...(item.categories ?? []).map((c) => `${c.name} ${c.slug}`)
1137
+ ].join(" ");
1138
+ return SPONSORED_MARKER_PATTERN.test(markers);
1139
+ }
1140
+ function normalizedCoverIdentity(item) {
1141
+ const raw = item.cover_image_url?.trim();
1142
+ if (!raw) return null;
1143
+ if (item.visual_identity?.version === 1 && /^[a-f0-9]{64}$/u.test(item.visual_identity.key)) {
1144
+ return `pixels:${item.visual_identity.key}`;
1145
+ }
1146
+ try {
1147
+ const url = new URL(raw);
1148
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
1149
+ url.hash = "";
1150
+ url.searchParams.sort();
1151
+ return `url:${url.toString()}`;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+ function selectRelevantLocalStories(currentStory, candidates, publication, limit = 3) {
1157
+ if (limit <= 0) return [];
1158
+ const currentSection = resolveSection(currentStory);
1159
+ const pubTowns = Array.isArray(publication.towns) ? publication.towns : [];
1160
+ const currentTowns = new Set(storyTowns(currentStory, pubTowns).map((t) => t.toLowerCase()));
1161
+ const currentTags = /* @__PURE__ */ new Set([
1162
+ ...(currentStory.tags ?? []).map((t) => t.name.toLowerCase()),
1163
+ ...(currentStory.categories ?? []).map((c) => c.name.toLowerCase())
1164
+ ]);
1165
+ const primaryRegion = publication.regionLabel.trim().toLowerCase();
1166
+ const seenIds = /* @__PURE__ */ new Set([currentStory.id]);
1167
+ const seenSlugs = /* @__PURE__ */ new Set([currentStory.slug]);
1168
+ const usedCovers = /* @__PURE__ */ new Set();
1169
+ const currentCover = normalizedCoverIdentity(currentStory);
1170
+ if (currentCover) usedCovers.add(currentCover);
1171
+ const scored = candidates.filter((item) => {
1172
+ if (!item?.id || !item?.slug) return false;
1173
+ if (seenIds.has(item.id) || seenSlugs.has(item.slug)) return false;
1174
+ if (isSponsoredStoryItem(item)) return false;
1175
+ return true;
1176
+ }).map((item) => {
1177
+ let score = 0;
1178
+ const cover = normalizedCoverIdentity(item);
1179
+ if (cover) score += 40;
1180
+ const itemTowns = storyTowns(item, pubTowns).map((t) => t.toLowerCase());
1181
+ if (itemTowns.some((t) => currentTowns.has(t))) {
1182
+ score += 25;
1183
+ } else if (primaryRegion && `${item.title} ${item.excerpt ?? ""}`.toLowerCase().includes(primaryRegion)) {
1184
+ score += 18;
1185
+ }
1186
+ const section = resolveSection(item);
1187
+ if (section === currentSection) {
1188
+ score += 20;
1189
+ } else if (["Community", "Events", "Schools", "Local businesses", "Food & places"].includes(section)) {
1190
+ score += 12;
1191
+ }
1192
+ const itemTags = [
1193
+ ...(item.tags ?? []).map((t) => t.name.toLowerCase()),
1194
+ ...(item.categories ?? []).map((c) => c.name.toLowerCase())
1195
+ ];
1196
+ const sharedTags = itemTags.filter((t) => currentTags.has(t)).length;
1197
+ score += Math.min(sharedTags * 8, 16);
1198
+ if (CELEBRATION_PATTERN.test(`${item.title} ${item.excerpt ?? ""}`)) {
1199
+ score += 6;
1200
+ }
1201
+ const parsedTime = Date.parse(item.published_at);
1202
+ const publishedMs = Number.isFinite(parsedTime) ? parsedTime : 0;
1203
+ return { item: { ...item, section }, cover, score, publishedMs };
1204
+ }).sort((a, b) => b.score - a.score || b.publishedMs - a.publishedMs);
1205
+ const selected = [];
1206
+ for (const entry of scored) {
1207
+ if (selected.length >= limit) break;
1208
+ if (seenIds.has(entry.item.id) || seenSlugs.has(entry.item.slug)) continue;
1209
+ if (entry.cover && usedCovers.has(entry.cover)) continue;
1210
+ seenIds.add(entry.item.id);
1211
+ seenSlugs.add(entry.item.slug);
1212
+ if (entry.cover) usedCovers.add(entry.cover);
1213
+ selected.push(entry.item);
1214
+ }
1215
+ return selected;
1216
+ }
1217
+
1218
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ARTICLE_CONVERSION_EXPERIMENT, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, LOCAL_BRIEFING_POLICY_VERSION, NAVIGATION_GROUPS, NETWORK_NAVIGATION, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, applyNavigationConfig, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, classifyStoryInvitationCategory, composeEdition, composeLocalBriefing, contextualCommunityInvitation, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, isSponsoredStoryItem, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, referralCode, resolveArticleConversionVariant, resolveCoupon, resolveNavigation, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, selectRelevantLocalStories, 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.81",
3
+ "version": "0.0.83",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -508,12 +508,15 @@
508
508
  .publication [class*="sectionCollection"] > [class*="section"]:only-child [class*="sectionFeature"],
509
509
  .publication [class*="sectionCollection"] > [class*="section"]:last-child:nth-child(odd) [class*="sectionFeature"] {
510
510
  display: block;
511
+ grid-column: auto;
512
+ grid-row: auto;
511
513
  }
512
514
 
513
515
  .publication [class*="sectionCollection"] > [class*="section"]:only-child [class*="sectionSupporting"],
514
516
  .publication [class*="sectionCollection"] > [class*="section"]:last-child:nth-child(odd) [class*="sectionSupporting"] {
515
517
  display: grid;
516
- grid-template-columns: minmax(0, 1fr);
518
+ grid-column: auto;
519
+ grid-template-columns: repeat(2, minmax(0, 1fr));
517
520
  gap: 20px;
518
521
  align-content: start;
519
522
  }