@scalemule/news-publication 0.0.82 → 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.cjs +201 -0
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +196 -1
- package/package.json +1 -1
- package/tokens/broadsheet.css +4 -1
package/dist/index.cjs
CHANGED
|
@@ -829,8 +829,11 @@ var EVENTS = {
|
|
|
829
829
|
article_shared: browser("engagement"),
|
|
830
830
|
promotion_viewed: browser("content"),
|
|
831
831
|
promotion_clicked: browser("engagement"),
|
|
832
|
+
newsletter_invitation_viewed: browser("content"),
|
|
833
|
+
related_stories_viewed: browser("content"),
|
|
832
834
|
newsletter_signup_started: browser("engagement"),
|
|
833
835
|
newsletter_subscription_confirmed: server(),
|
|
836
|
+
experiment_exposed: browser("system"),
|
|
834
837
|
web_vital_recorded: browser("system"),
|
|
835
838
|
scroll_depth_reached: browser("content"),
|
|
836
839
|
navigation_clicked: browser("navigation"),
|
|
@@ -3633,6 +3636,198 @@ function articleFormatLabel(format) {
|
|
|
3633
3636
|
return format === "opinion" ? "Opinion" : format === "explainer" ? "Explainer" : format === "interview" ? "Interview" : format === "photo_essay" ? "Photo essay" : format === "video_story" ? "Video story" : void 0;
|
|
3634
3637
|
}
|
|
3635
3638
|
|
|
3639
|
+
// src/community-conversion.ts
|
|
3640
|
+
var ARTICLE_CONVERSION_EXPERIMENT = "article_conversion_module";
|
|
3641
|
+
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;
|
|
3642
|
+
var SPONSORED_MARKER_PATTERN = /\b(sponsored|paid post|partner content|advertorial|promoted|brand studio)\b/iu;
|
|
3643
|
+
function resolveArticleConversionVariant(assignedVariant, queryOverride) {
|
|
3644
|
+
const candidate = (queryOverride ?? assignedVariant ?? "").trim().toLowerCase();
|
|
3645
|
+
if (candidate === "treatment" || candidate === "inline" || candidate === "b") {
|
|
3646
|
+
return "treatment";
|
|
3647
|
+
}
|
|
3648
|
+
return "control";
|
|
3649
|
+
}
|
|
3650
|
+
function classifyStoryInvitationCategory(story) {
|
|
3651
|
+
const combinedText = [
|
|
3652
|
+
story.title,
|
|
3653
|
+
story.excerpt ?? "",
|
|
3654
|
+
story.section ?? "",
|
|
3655
|
+
...(story.tags ?? []).map((t) => t.name),
|
|
3656
|
+
...(story.categories ?? []).map((c) => c.name)
|
|
3657
|
+
].join(" ");
|
|
3658
|
+
if (CELEBRATION_PATTERN.test(combinedText)) {
|
|
3659
|
+
return "celebrations";
|
|
3660
|
+
}
|
|
3661
|
+
const section = resolveSection(story);
|
|
3662
|
+
switch (section) {
|
|
3663
|
+
case "Local businesses":
|
|
3664
|
+
return "local_businesses";
|
|
3665
|
+
case "Schools":
|
|
3666
|
+
return "education";
|
|
3667
|
+
case "Events":
|
|
3668
|
+
return "community_events";
|
|
3669
|
+
case "Civic life":
|
|
3670
|
+
case "Real Estate":
|
|
3671
|
+
case "Health":
|
|
3672
|
+
return "neighborhood_news";
|
|
3673
|
+
default:
|
|
3674
|
+
return "community";
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
function contextualCommunityInvitation(story, publication) {
|
|
3678
|
+
const category = classifyStoryInvitationCategory(story);
|
|
3679
|
+
const region = publication.regionLabel.trim() || publication.name.trim();
|
|
3680
|
+
const coverage = publication.coverageText?.trim() || (publication.towns && publication.towns.length > 0 ? publication.towns.slice(0, 3).join(", ") : region);
|
|
3681
|
+
switch (category) {
|
|
3682
|
+
case "celebrations":
|
|
3683
|
+
return {
|
|
3684
|
+
category,
|
|
3685
|
+
eyebrow: `${region} Community`,
|
|
3686
|
+
heading: `Stay connected to ${region}`,
|
|
3687
|
+
subprompt: "Discover more people and milestones worth celebrating.",
|
|
3688
|
+
description: `${region} is full of stories worth celebrating. Discover the people, places, events, and milestones shaping ${coverage}.`,
|
|
3689
|
+
ctaLabel: "Subscribe",
|
|
3690
|
+
relatedHeading: `More stories from ${region}`
|
|
3691
|
+
};
|
|
3692
|
+
case "local_businesses":
|
|
3693
|
+
return {
|
|
3694
|
+
category,
|
|
3695
|
+
eyebrow: `${region} Local Businesses`,
|
|
3696
|
+
heading: `Stay connected to ${region}`,
|
|
3697
|
+
subprompt: "Meet the entrepreneurs building your community.",
|
|
3698
|
+
description: `Discover the founders, independent shops, places, and events shaping ${coverage}.`,
|
|
3699
|
+
ctaLabel: "Subscribe",
|
|
3700
|
+
relatedHeading: `More local stories in ${region}`
|
|
3701
|
+
};
|
|
3702
|
+
case "education":
|
|
3703
|
+
return {
|
|
3704
|
+
category,
|
|
3705
|
+
eyebrow: `${region} Schools & Youth`,
|
|
3706
|
+
heading: `Stay connected to ${region}`,
|
|
3707
|
+
subprompt: "Follow achievements from local students and schools.",
|
|
3708
|
+
description: `Discover the students, educators, neighborhood milestones, and events shaping ${coverage}.`,
|
|
3709
|
+
ctaLabel: "Subscribe",
|
|
3710
|
+
relatedHeading: `More stories from ${region}`
|
|
3711
|
+
};
|
|
3712
|
+
case "community_events":
|
|
3713
|
+
return {
|
|
3714
|
+
category,
|
|
3715
|
+
eyebrow: `${region} Events & Gatherings`,
|
|
3716
|
+
heading: `Stay connected to ${region}`,
|
|
3717
|
+
subprompt: "Find something happening near you this weekend.",
|
|
3718
|
+
description: `Discover the people, places, weekend gatherings, and local stories shaping ${coverage}.`,
|
|
3719
|
+
ctaLabel: "Subscribe",
|
|
3720
|
+
relatedHeading: `Happening around ${region}`
|
|
3721
|
+
};
|
|
3722
|
+
case "neighborhood_news":
|
|
3723
|
+
return {
|
|
3724
|
+
category,
|
|
3725
|
+
eyebrow: `${region} Neighborhood Briefing`,
|
|
3726
|
+
heading: `Stay connected to ${region}`,
|
|
3727
|
+
subprompt: "Stay informed about changes affecting your neighborhood.",
|
|
3728
|
+
description: `Follow the civic decisions, neighborhood projects, people, and events shaping ${coverage}.`,
|
|
3729
|
+
ctaLabel: "Subscribe",
|
|
3730
|
+
relatedHeading: `More from ${region}`
|
|
3731
|
+
};
|
|
3732
|
+
case "community":
|
|
3733
|
+
default:
|
|
3734
|
+
return {
|
|
3735
|
+
category: "community",
|
|
3736
|
+
eyebrow: `${region} Community`,
|
|
3737
|
+
heading: `Stay connected to ${region}`,
|
|
3738
|
+
subprompt: "Discover the people, places, and events shaping your community.",
|
|
3739
|
+
description: `Receive local reporting on the people, places, events, and stories shaping ${coverage}.`,
|
|
3740
|
+
ctaLabel: "Subscribe",
|
|
3741
|
+
relatedHeading: `More stories from ${region}`
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
function isSponsoredStoryItem(item) {
|
|
3746
|
+
const markers = [
|
|
3747
|
+
item.section ?? "",
|
|
3748
|
+
...(item.tags ?? []).map((t) => `${t.name} ${t.slug}`),
|
|
3749
|
+
...(item.categories ?? []).map((c) => `${c.name} ${c.slug}`)
|
|
3750
|
+
].join(" ");
|
|
3751
|
+
return SPONSORED_MARKER_PATTERN.test(markers);
|
|
3752
|
+
}
|
|
3753
|
+
function normalizedCoverIdentity(item) {
|
|
3754
|
+
const raw = item.cover_image_url?.trim();
|
|
3755
|
+
if (!raw) return null;
|
|
3756
|
+
if (item.visual_identity?.version === 1 && /^[a-f0-9]{64}$/u.test(item.visual_identity.key)) {
|
|
3757
|
+
return `pixels:${item.visual_identity.key}`;
|
|
3758
|
+
}
|
|
3759
|
+
try {
|
|
3760
|
+
const url = new URL(raw);
|
|
3761
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
3762
|
+
url.hash = "";
|
|
3763
|
+
url.searchParams.sort();
|
|
3764
|
+
return `url:${url.toString()}`;
|
|
3765
|
+
} catch {
|
|
3766
|
+
return null;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
function selectRelevantLocalStories(currentStory, candidates, publication, limit = 3) {
|
|
3770
|
+
if (limit <= 0) return [];
|
|
3771
|
+
const currentSection = resolveSection(currentStory);
|
|
3772
|
+
const pubTowns = Array.isArray(publication.towns) ? publication.towns : [];
|
|
3773
|
+
const currentTowns = new Set(storyTowns(currentStory, pubTowns).map((t) => t.toLowerCase()));
|
|
3774
|
+
const currentTags = /* @__PURE__ */ new Set([
|
|
3775
|
+
...(currentStory.tags ?? []).map((t) => t.name.toLowerCase()),
|
|
3776
|
+
...(currentStory.categories ?? []).map((c) => c.name.toLowerCase())
|
|
3777
|
+
]);
|
|
3778
|
+
const primaryRegion = publication.regionLabel.trim().toLowerCase();
|
|
3779
|
+
const seenIds = /* @__PURE__ */ new Set([currentStory.id]);
|
|
3780
|
+
const seenSlugs = /* @__PURE__ */ new Set([currentStory.slug]);
|
|
3781
|
+
const usedCovers = /* @__PURE__ */ new Set();
|
|
3782
|
+
const currentCover = normalizedCoverIdentity(currentStory);
|
|
3783
|
+
if (currentCover) usedCovers.add(currentCover);
|
|
3784
|
+
const scored = candidates.filter((item) => {
|
|
3785
|
+
if (!item?.id || !item?.slug) return false;
|
|
3786
|
+
if (seenIds.has(item.id) || seenSlugs.has(item.slug)) return false;
|
|
3787
|
+
if (isSponsoredStoryItem(item)) return false;
|
|
3788
|
+
return true;
|
|
3789
|
+
}).map((item) => {
|
|
3790
|
+
let score = 0;
|
|
3791
|
+
const cover = normalizedCoverIdentity(item);
|
|
3792
|
+
if (cover) score += 40;
|
|
3793
|
+
const itemTowns = storyTowns(item, pubTowns).map((t) => t.toLowerCase());
|
|
3794
|
+
if (itemTowns.some((t) => currentTowns.has(t))) {
|
|
3795
|
+
score += 25;
|
|
3796
|
+
} else if (primaryRegion && `${item.title} ${item.excerpt ?? ""}`.toLowerCase().includes(primaryRegion)) {
|
|
3797
|
+
score += 18;
|
|
3798
|
+
}
|
|
3799
|
+
const section = resolveSection(item);
|
|
3800
|
+
if (section === currentSection) {
|
|
3801
|
+
score += 20;
|
|
3802
|
+
} else if (["Community", "Events", "Schools", "Local businesses", "Food & places"].includes(section)) {
|
|
3803
|
+
score += 12;
|
|
3804
|
+
}
|
|
3805
|
+
const itemTags = [
|
|
3806
|
+
...(item.tags ?? []).map((t) => t.name.toLowerCase()),
|
|
3807
|
+
...(item.categories ?? []).map((c) => c.name.toLowerCase())
|
|
3808
|
+
];
|
|
3809
|
+
const sharedTags = itemTags.filter((t) => currentTags.has(t)).length;
|
|
3810
|
+
score += Math.min(sharedTags * 8, 16);
|
|
3811
|
+
if (CELEBRATION_PATTERN.test(`${item.title} ${item.excerpt ?? ""}`)) {
|
|
3812
|
+
score += 6;
|
|
3813
|
+
}
|
|
3814
|
+
const parsedTime = Date.parse(item.published_at);
|
|
3815
|
+
const publishedMs = Number.isFinite(parsedTime) ? parsedTime : 0;
|
|
3816
|
+
return { item: { ...item, section }, cover, score, publishedMs };
|
|
3817
|
+
}).sort((a, b) => b.score - a.score || b.publishedMs - a.publishedMs);
|
|
3818
|
+
const selected = [];
|
|
3819
|
+
for (const entry of scored) {
|
|
3820
|
+
if (selected.length >= limit) break;
|
|
3821
|
+
if (seenIds.has(entry.item.id) || seenSlugs.has(entry.item.slug)) continue;
|
|
3822
|
+
if (entry.cover && usedCovers.has(entry.cover)) continue;
|
|
3823
|
+
seenIds.add(entry.item.id);
|
|
3824
|
+
seenSlugs.add(entry.item.slug);
|
|
3825
|
+
if (entry.cover) usedCovers.add(entry.cover);
|
|
3826
|
+
selected.push(entry.item);
|
|
3827
|
+
}
|
|
3828
|
+
return selected;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3636
3831
|
Object.defineProperty(exports, "COOKIE_CONSENT_BACKUP_KEY", {
|
|
3637
3832
|
enumerable: true,
|
|
3638
3833
|
get: function () { return compliance.COOKIE_CONSENT_BACKUP_KEY; }
|
|
@@ -3731,6 +3926,7 @@ Object.defineProperty(exports, "setNetworkConsentHub", {
|
|
|
3731
3926
|
});
|
|
3732
3927
|
exports.ANALYTICS_COOKIE = ANALYTICS_COOKIE;
|
|
3733
3928
|
exports.ANALYTICS_VERSION = ANALYTICS_VERSION;
|
|
3929
|
+
exports.ARTICLE_CONVERSION_EXPERIMENT = ARTICLE_CONVERSION_EXPERIMENT;
|
|
3734
3930
|
exports.ArticleProvenance = ArticleProvenance;
|
|
3735
3931
|
exports.BROADSHEET_THEME = BROADSHEET_THEME;
|
|
3736
3932
|
exports.CLASSIFIED_CATEGORIES = CLASSIFIED_CATEGORIES;
|
|
@@ -3787,12 +3983,14 @@ exports.canHostMeetup = canHostMeetup;
|
|
|
3787
3983
|
exports.categoryLabel = categoryLabel;
|
|
3788
3984
|
exports.classifiedPath = classifiedPath;
|
|
3789
3985
|
exports.classifiedSearchPath = classifiedSearchPath;
|
|
3986
|
+
exports.classifyStoryInvitationCategory = classifyStoryInvitationCategory;
|
|
3790
3987
|
exports.clearTheme = clearTheme;
|
|
3791
3988
|
exports.clipChars = clipChars;
|
|
3792
3989
|
exports.communityMapEmbeds = communityMapEmbeds;
|
|
3793
3990
|
exports.communityPublications = communityPublications;
|
|
3794
3991
|
exports.composeEdition = composeEdition;
|
|
3795
3992
|
exports.composeLocalBriefing = composeLocalBriefing;
|
|
3993
|
+
exports.contextualCommunityInvitation = contextualCommunityInvitation;
|
|
3796
3994
|
exports.dealDollars = dealDollars;
|
|
3797
3995
|
exports.dealGoPath = dealGoPath;
|
|
3798
3996
|
exports.dealsFeedPath = dealsFeedPath;
|
|
@@ -3818,6 +4016,7 @@ exports.isAdult = isAdult;
|
|
|
3818
4016
|
exports.isClassifiedCategory = isClassifiedCategory;
|
|
3819
4017
|
exports.isEmploymentType = isEmploymentType;
|
|
3820
4018
|
exports.isSafeActionUrl = isSafeActionUrl;
|
|
4019
|
+
exports.isSponsoredStoryItem = isSponsoredStoryItem;
|
|
3821
4020
|
exports.jobEmployerBody = jobEmployerBody;
|
|
3822
4021
|
exports.jobOpeningBody = jobOpeningBody;
|
|
3823
4022
|
exports.jobPostingBody = jobPostingBody;
|
|
@@ -3850,6 +4049,7 @@ exports.readerSources = readerSources;
|
|
|
3850
4049
|
exports.recordConsentToCompliance = recordConsentToCompliance;
|
|
3851
4050
|
exports.referralCode = referralCode;
|
|
3852
4051
|
exports.renderArticleBlocksToHtml = renderArticleBlocksToHtml;
|
|
4052
|
+
exports.resolveArticleConversionVariant = resolveArticleConversionVariant;
|
|
3853
4053
|
exports.resolveCoupon = resolveCoupon;
|
|
3854
4054
|
exports.resolveEffectiveTheme = resolveEffectiveTheme;
|
|
3855
4055
|
exports.resolveLayout = resolveLayout;
|
|
@@ -3863,6 +4063,7 @@ exports.secretsMatch = secretsMatch;
|
|
|
3863
4063
|
exports.sectionDefinition = sectionDefinition;
|
|
3864
4064
|
exports.sectionId = sectionId;
|
|
3865
4065
|
exports.sectionPath = sectionPath;
|
|
4066
|
+
exports.selectRelevantLocalStories = selectRelevantLocalStories;
|
|
3866
4067
|
exports.setTheme = setTheme;
|
|
3867
4068
|
exports.shortDeck = shortDeck;
|
|
3868
4069
|
exports.shortText = shortText;
|
package/dist/index.d.cts
CHANGED
|
@@ -624,4 +624,54 @@ interface SponsorshipReport {
|
|
|
624
624
|
/** Shared by the newsroom print/export view and the repeatable PDF renderer. */
|
|
625
625
|
declare function campaignReportHtml(report: SponsorshipReport): string;
|
|
626
626
|
|
|
627
|
-
|
|
627
|
+
declare const ARTICLE_CONVERSION_EXPERIMENT = "article_conversion_module";
|
|
628
|
+
type ArticleConversionVariant = "control" | "treatment";
|
|
629
|
+
type CommunityInvitationCategory = "celebrations" | "local_businesses" | "education" | "neighborhood_news" | "community_events" | "community";
|
|
630
|
+
interface ContextualCommunityInvitation {
|
|
631
|
+
category: CommunityInvitationCategory;
|
|
632
|
+
eyebrow: string;
|
|
633
|
+
heading: string;
|
|
634
|
+
subprompt: string;
|
|
635
|
+
description: string;
|
|
636
|
+
ctaLabel: string;
|
|
637
|
+
relatedHeading: string;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Resolves the `article_conversion_module` experiment variant consistently.
|
|
641
|
+
* Supports query-parameter preview overrides (`?conversion=treatment` or `?conversion=control`)
|
|
642
|
+
* before falling back to the assigned ScaleMule flag variant (`"control"` default).
|
|
643
|
+
*/
|
|
644
|
+
declare function resolveArticleConversionVariant(assignedVariant?: string | null, queryOverride?: string | null): ArticleConversionVariant;
|
|
645
|
+
/**
|
|
646
|
+
* Infers the reader-facing community invitation category from a story's section,
|
|
647
|
+
* tags, categories, headline, and excerpt.
|
|
648
|
+
*/
|
|
649
|
+
declare function classifyStoryInvitationCategory(story: Pick<NewsItem, "title" | "excerpt" | "section" | "tags" | "categories">): CommunityInvitationCategory;
|
|
650
|
+
/**
|
|
651
|
+
* Builds a publication-aware, category-tailored invitation that connects the reader's
|
|
652
|
+
* reason for visiting a specific article to staying connected with the local community.
|
|
653
|
+
*/
|
|
654
|
+
interface CommunityInvitationPublicationContext {
|
|
655
|
+
name: string;
|
|
656
|
+
regionLabel: string;
|
|
657
|
+
coverageText?: string;
|
|
658
|
+
towns?: string[];
|
|
659
|
+
}
|
|
660
|
+
declare function contextualCommunityInvitation(story: Pick<NewsItem, "title" | "excerpt" | "section" | "tags" | "categories">, publication: CommunityInvitationPublicationContext): ContextualCommunityInvitation;
|
|
661
|
+
/**
|
|
662
|
+
* Returns true if a story carries any sponsored, paid, or partner-content markers.
|
|
663
|
+
* Editorial recommendation slots must never mix unlabeled sponsored items.
|
|
664
|
+
*/
|
|
665
|
+
declare function isSponsoredStoryItem(item: NewsItem): boolean;
|
|
666
|
+
/**
|
|
667
|
+
* Selects 2–3 relevant local editorial stories for the post-prose Community Conversion Module.
|
|
668
|
+
*
|
|
669
|
+
* Rules:
|
|
670
|
+
* - Never includes the current story or duplicate story IDs/slugs.
|
|
671
|
+
* - Never includes sponsored/paid items (`isSponsoredStoryItem`).
|
|
672
|
+
* - Deduplicates cover images so recommended cards never repeat the current story's cover or each other's cover.
|
|
673
|
+
* - Prioritizes stories with valid cover photographs, shared town/geographic relevance, and community/section affinity.
|
|
674
|
+
*/
|
|
675
|
+
declare function selectRelevantLocalStories(currentStory: NewsItem, candidates: NewsItem[], publication: Pick<NewsPublication, "regionLabel" | "towns">, limit?: number): NewsItem[];
|
|
676
|
+
|
|
677
|
+
export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ARTICLE_CONVERSION_EXPERIMENT, type ArticleConversionVariant, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, type CommunityInvitationCategory, type ContextualCommunityInvitation, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, type FeaturedDestination, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, Measurement, NAVIGATION_GROUPS, NETWORK_NAVIGATION, NEWS_CACHE, type NavigationConfig, type NavigationContext, type NavigationDestination, type NavigationGroup, type NavigationGroupLinks, type NavigationKind, type NavigationLink, type NavigationOverride, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, type ResolvedNavigation, SESSION_IDLE_MS, type SponsorshipReport, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyNavigationConfig, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, classifyStoryInvitationCategory, clearTheme, composeEdition, composeLocalBriefing, contextualCommunityInvitation, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, isSponsoredStoryItem, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveArticleConversionVariant, resolveCoupon, resolveEffectiveTheme, resolveLayout, resolveNavigation, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, selectRelevantLocalStories, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
|
package/dist/index.d.ts
CHANGED
|
@@ -624,4 +624,54 @@ interface SponsorshipReport {
|
|
|
624
624
|
/** Shared by the newsroom print/export view and the repeatable PDF renderer. */
|
|
625
625
|
declare function campaignReportHtml(report: SponsorshipReport): string;
|
|
626
626
|
|
|
627
|
-
|
|
627
|
+
declare const ARTICLE_CONVERSION_EXPERIMENT = "article_conversion_module";
|
|
628
|
+
type ArticleConversionVariant = "control" | "treatment";
|
|
629
|
+
type CommunityInvitationCategory = "celebrations" | "local_businesses" | "education" | "neighborhood_news" | "community_events" | "community";
|
|
630
|
+
interface ContextualCommunityInvitation {
|
|
631
|
+
category: CommunityInvitationCategory;
|
|
632
|
+
eyebrow: string;
|
|
633
|
+
heading: string;
|
|
634
|
+
subprompt: string;
|
|
635
|
+
description: string;
|
|
636
|
+
ctaLabel: string;
|
|
637
|
+
relatedHeading: string;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Resolves the `article_conversion_module` experiment variant consistently.
|
|
641
|
+
* Supports query-parameter preview overrides (`?conversion=treatment` or `?conversion=control`)
|
|
642
|
+
* before falling back to the assigned ScaleMule flag variant (`"control"` default).
|
|
643
|
+
*/
|
|
644
|
+
declare function resolveArticleConversionVariant(assignedVariant?: string | null, queryOverride?: string | null): ArticleConversionVariant;
|
|
645
|
+
/**
|
|
646
|
+
* Infers the reader-facing community invitation category from a story's section,
|
|
647
|
+
* tags, categories, headline, and excerpt.
|
|
648
|
+
*/
|
|
649
|
+
declare function classifyStoryInvitationCategory(story: Pick<NewsItem, "title" | "excerpt" | "section" | "tags" | "categories">): CommunityInvitationCategory;
|
|
650
|
+
/**
|
|
651
|
+
* Builds a publication-aware, category-tailored invitation that connects the reader's
|
|
652
|
+
* reason for visiting a specific article to staying connected with the local community.
|
|
653
|
+
*/
|
|
654
|
+
interface CommunityInvitationPublicationContext {
|
|
655
|
+
name: string;
|
|
656
|
+
regionLabel: string;
|
|
657
|
+
coverageText?: string;
|
|
658
|
+
towns?: string[];
|
|
659
|
+
}
|
|
660
|
+
declare function contextualCommunityInvitation(story: Pick<NewsItem, "title" | "excerpt" | "section" | "tags" | "categories">, publication: CommunityInvitationPublicationContext): ContextualCommunityInvitation;
|
|
661
|
+
/**
|
|
662
|
+
* Returns true if a story carries any sponsored, paid, or partner-content markers.
|
|
663
|
+
* Editorial recommendation slots must never mix unlabeled sponsored items.
|
|
664
|
+
*/
|
|
665
|
+
declare function isSponsoredStoryItem(item: NewsItem): boolean;
|
|
666
|
+
/**
|
|
667
|
+
* Selects 2–3 relevant local editorial stories for the post-prose Community Conversion Module.
|
|
668
|
+
*
|
|
669
|
+
* Rules:
|
|
670
|
+
* - Never includes the current story or duplicate story IDs/slugs.
|
|
671
|
+
* - Never includes sponsored/paid items (`isSponsoredStoryItem`).
|
|
672
|
+
* - Deduplicates cover images so recommended cards never repeat the current story's cover or each other's cover.
|
|
673
|
+
* - Prioritizes stories with valid cover photographs, shared town/geographic relevance, and community/section affinity.
|
|
674
|
+
*/
|
|
675
|
+
declare function selectRelevantLocalStories(currentStory: NewsItem, candidates: NewsItem[], publication: Pick<NewsPublication, "regionLabel" | "towns">, limit?: number): NewsItem[];
|
|
676
|
+
|
|
677
|
+
export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ARTICLE_CONVERSION_EXPERIMENT, type ArticleConversionVariant, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, type CommunityInvitationCategory, type ContextualCommunityInvitation, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, type FeaturedDestination, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, Measurement, NAVIGATION_GROUPS, NETWORK_NAVIGATION, NEWS_CACHE, type NavigationConfig, type NavigationContext, type NavigationDestination, type NavigationGroup, type NavigationGroupLinks, type NavigationKind, type NavigationLink, type NavigationOverride, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, type ResolvedNavigation, SESSION_IDLE_MS, type SponsorshipReport, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyNavigationConfig, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, classifyStoryInvitationCategory, clearTheme, composeEdition, composeLocalBriefing, contextualCommunityInvitation, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, isSponsoredStoryItem, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveArticleConversionVariant, resolveCoupon, resolveEffectiveTheme, resolveLayout, resolveNavigation, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, selectRelevantLocalStories, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
|
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
|
-
|
|
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.
|
|
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": {
|
package/tokens/broadsheet.css
CHANGED
|
@@ -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-
|
|
518
|
+
grid-column: auto;
|
|
519
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
517
520
|
gap: 20px;
|
|
518
521
|
align-content: start;
|
|
519
522
|
}
|