@scalemule/news-publication 0.0.42 → 0.0.44

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
@@ -53,7 +53,7 @@ function definePublication(input) {
53
53
  nearbyTowns: input.nearbyTowns ?? [],
54
54
  sections: input.sections ?? [],
55
55
  description: input.description,
56
- tagline: input.tagline ?? "Local stories. Close to home.",
56
+ tagline: input.tagline ?? "Local stories. Stronger communities.",
57
57
  briefTitle: input.briefTitle ?? `${input.regionLabel} in Brief`,
58
58
  parent: input.parent === true,
59
59
  layout,
@@ -83,7 +83,8 @@ var walnutCreekTimes = definePublication({
83
83
  towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
84
84
  nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
85
85
  sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
86
- description: "Local news, independent businesses and community voices from Walnut Creek, Rossmoor, Saranap and Pleasant Hill.",
86
+ description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
87
+ tagline: "Local stories. Stronger communities.",
87
88
  layout: "broadsheet",
88
89
  layouts: ["broadsheet"],
89
90
  features: { smile: true, jobs: true, classifieds: true },
@@ -830,11 +831,108 @@ async function syncPublication(input) {
830
831
  }
831
832
 
832
833
  // src/network.ts
834
+ var EDITORIAL_PRINCIPLES = [
835
+ {
836
+ id: "independent-trustworthy-journalism",
837
+ number: 1,
838
+ title: "Independent, Trustworthy Local Journalism",
839
+ summary: "Our first responsibility is to provide accurate, useful, and independent local information.",
840
+ guidelines: [
841
+ "Verify information before publication.",
842
+ "Attribute reporting to credible sources.",
843
+ "Clearly distinguish original reporting from aggregation.",
844
+ "Correct mistakes promptly and transparently.",
845
+ "Maintain editorial independence from advertisers, sponsors, political organizations, and public officials.",
846
+ "Avoid sensationalism, manufactured outrage, and misleading headlines.",
847
+ "Provide appropriate context rather than simply repeating announcements."
848
+ ]
849
+ },
850
+ {
851
+ id: "people-communities-first",
852
+ number: 2,
853
+ title: "People and Communities First",
854
+ summary: "Local journalism should be about more than institutions, government meetings, and press releases. We tell stories about the people who make communities interesting.",
855
+ guidelines: [
856
+ "Spotlight local residents doing extraordinary things.",
857
+ "Highlight teachers, students, and educational achievements.",
858
+ "Feature volunteers and nonprofit organizations.",
859
+ "Cover neighborhood initiatives and community milestones.",
860
+ "Recognize local artists, musicians, and creators.",
861
+ "Support small businesses and entrepreneurs.",
862
+ "Preserve local history and living traditions."
863
+ ]
864
+ },
865
+ {
866
+ id: "connection-belonging",
867
+ number: 3,
868
+ title: "Connection and Belonging",
869
+ summary: "Our publications help residents feel connected to their communities and cultivate a stronger sense of local belonging.",
870
+ guidelines: [
871
+ "Help residents discover what is happening around them.",
872
+ "Spotlight who is contributing to their neighborhoods.",
873
+ "Highlight which organizations are making a difference.",
874
+ "Show where residents can participate and engage.",
875
+ "Celebrate what makes each community distinctive.",
876
+ "Build useful community experiences rather than attention-maximizing feeds."
877
+ ]
878
+ },
879
+ {
880
+ id: "constructive-participation",
881
+ number: 4,
882
+ title: "Constructive Community Participation",
883
+ summary: "Readers have structured, respectful, and purposeful opportunities to contribute to their communities through our platform.",
884
+ guidelines: [
885
+ "Submit story ideas and news tips directly to the newsroom.",
886
+ "Share verified community photographs and visual stories.",
887
+ "Recommend local events and gatherings.",
888
+ "Contribute to seasonal community guides and neighborhood maps.",
889
+ "Recognize community volunteers and local achievers.",
890
+ "Suggest neighborhood improvements and share local knowledge.",
891
+ "Participate in carefully moderated civic discussions."
892
+ ]
893
+ },
894
+ {
895
+ id: "accountability-constructive-journalism",
896
+ number: 5,
897
+ title: "Accountability and Constructive Journalism",
898
+ summary: "Communities benefit from understanding both their successes and their challenges. We report thoroughly, fairly, and responsibly without manufacturing conflict.",
899
+ guidelines: [
900
+ "Explain how local decisions affect residents.",
901
+ "Investigate matters of public interest.",
902
+ "Hold institutions and elected leaders accountable.",
903
+ "Provide useful context and historical depth.",
904
+ "Explore credible solutions where appropriate.",
905
+ "Give relevant stakeholders opportunities to respond.",
906
+ "Help readers understand how to participate in civic life."
907
+ ]
908
+ },
909
+ {
910
+ id: "sustainable-respectful-media",
911
+ number: 6,
912
+ title: "Sustainable, Respectful Local Media",
913
+ summary: "We are developing a sustainable model for high-quality local journalism that respects readers and prioritizes trust over short-term traffic.",
914
+ guidelines: [
915
+ "Avoid intrusive advertising and clickbait distractions.",
916
+ "Avoid deceptive engagement tactics or endless outrage feeds.",
917
+ "Clearly identify sponsored content and commercial relationships.",
918
+ "Maintain a meaningful separation between advertising and editorial decisions.",
919
+ "Build long-term relationships with local businesses and community organizations.",
920
+ "Prioritize reader trust over short-term page views.",
921
+ "Design fast, accessible, readable websites.",
922
+ "Avoid low-quality syndication and tracking-heavy ad networks."
923
+ ]
924
+ }
925
+ ];
833
926
  var NETWORK_PARENT = {
834
927
  name: "Bay Area News Network",
835
928
  url: "https://bayareanewsnetwork.com",
836
- tagline: "Local stories. Stronger communities.",
837
- description: "The institutional network of independent local publications covering the San Francisco Bay Area."
929
+ brandSlogan: "Bringing communities closer.",
930
+ tagline: "Bringing communities closer.",
931
+ mission: "To strengthen local communities through independent journalism, meaningful connections, and opportunities for residents to participate in the places they call home.",
932
+ extendedMission: "We believe communities thrive when people are informed, connected, and engaged. Our publications bring neighbors closer by sharing the stories that matter, celebrating local life, holding institutions accountable, and giving residents a voice in their community\u2019s future. Through independent local journalism and community participation, we aim to help people better understand, appreciate, and shape the places they call home.",
933
+ publicationSlogan: "Local stories. Stronger communities.",
934
+ description: "An institutional network of independent local publications that use journalism, technology, and community participation to strengthen the places they serve.",
935
+ principles: EDITORIAL_PRINCIPLES
838
936
  };
839
937
  var localPublications = [
840
938
  {
@@ -842,77 +940,99 @@ var localPublications = [
842
940
  name: "Bay Area Chronicle",
843
941
  url: "https://bayareachronicle.com",
844
942
  area: "San Francisco Bay Area",
845
- description: "Regional reporting and in-depth investigations covering the nine-county San Francisco Bay Area."
943
+ slogan: "Local stories. Stronger communities.",
944
+ description: "Regional investigative reporting and in-depth policy analysis covering housing, transit, environmental resilience, and civic institutions across the nine-county Bay Area.",
945
+ aboutIntro: "Bay Area Chronicle is an independent regional publication providing in-depth reporting and investigative journalism across the nine-county San Francisco Bay Area. While our sister publications focus on the immediate pulse of individual towns and valleys, Bay Area Chronicle investigates the broader challenges and systemic choices that span our region\u2014from regional transit integration and housing affordability to climate resilience and major civic governance. We believe regional awareness complements local loyalty, equipping Bay Area residents with the deep context required to solve shared problems."
846
946
  },
847
947
  {
848
948
  slug: "concordchronicle",
849
949
  name: "Concord Chronicle",
850
950
  url: "https://concordchronicle.com",
851
- area: "Concord",
852
- description: "A new place for Concord\u2019s local stories, independent businesses and community voices."
951
+ area: "Concord & Clayton",
952
+ slogan: "Local stories. Stronger communities.",
953
+ description: "Independent reporting on municipal governance, school districts, small business, and neighborhood culture in Concord and Clayton.",
954
+ aboutIntro: "Concord Chronicle is an independent local publication dedicated to chronicling community life and civic affairs across Concord and Clayton. We report closely on city council deliberations, the Mt. Diablo Unified School District, the transformation of the former naval weapons station, and the vibrant small businesses that anchor Todos Santos Plaza and neighborhood shopping districts. By illuminating both local challenges and community milestones, we aim to provide reliable reporting that deepens resident participation and fosters a stronger civic community."
853
955
  },
854
956
  {
855
957
  slug: "walnutcreektimes",
856
958
  name: "Walnut Creek Times",
857
959
  url: "https://walnutcreektimes.com",
858
960
  area: "Walnut Creek \xB7 Rossmoor \xB7 Saranap \xB7 Pleasant Hill",
859
- description: "A new place for Walnut Creek\u2019s local stories, independent businesses and community voices."
961
+ slogan: "Local stories. Stronger communities.",
962
+ description: "Independent local journalism covering civic decisions, downtown commerce, neighborhood life, and community achievements across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill.",
963
+ aboutIntro: "Walnut Creek Times is an independent local publication dedicated to helping residents stay informed, connected, and engaged with the community they call home. We cover the people, places, decisions, and events that shape life across Walnut Creek, Rossmoor, Saranap, and Pleasant Hill. From downtown revitalization and neighborhood planning to open space stewardship, community celebrations, and the achievements of everyday residents, our goal is to provide trustworthy journalism that helps strengthen the connections between people and the places they live."
860
964
  },
861
965
  {
862
966
  slug: "lamorindapost",
863
967
  name: "Lamorinda Post",
864
968
  url: "https://lamorindapost.com",
865
969
  area: "Orinda \xB7 Lafayette \xB7 Moraga",
866
- description: "A new home for the neighbors, businesses and community stories of Lamorinda."
970
+ slogan: "Local stories. Stronger communities.",
971
+ description: "Community-rooted journalism for the three hillside towns of Lamorinda, covering local schools, civic leadership, wildfire preparedness, and neighborhood traditions.",
972
+ aboutIntro: "Lamorinda Post is an independent local publication dedicated to the neighbors, families, and civic institutions of Orinda, Lafayette, and Moraga. Connected across rolling ridgelines and oak-studded canyons, our communities share exceptional schools, active civic traditions, and vital concerns including wildfire resilience, open space preservation, and sensible town planning. We provide thoughtful, verified reporting on the decisions and people shaping our three towns, helping neighbors stay informed and actively engaged in their community\u2019s future."
867
973
  },
868
974
  {
869
975
  slug: "alamoweekly",
870
976
  name: "Alamo Weekly",
871
977
  url: "https://alamoweekly.com",
872
- area: "Alamo",
873
- description: "A new place for Alamo\u2019s local stories, independent businesses and community voices."
978
+ area: "Alamo & Round Hill",
979
+ slogan: "Local stories. Stronger communities.",
980
+ description: "Hyperlocal news focused on the unincorporated enclave of Alamo, Round Hill, local schools, neighborhood preservation, and community organizations.",
981
+ aboutIntro: "Alamo Weekly is an independent local publication committed to serving the unincorporated community of Alamo and the surrounding Round Hill neighborhoods. Nestled between Mount Diablo and the Las Trampas ridge, Alamo cherishes its semi-rural character, scenic trails, and close-knit neighborhood traditions. We follow county land-use planning, local school achievements, historic preservation, and community volunteer efforts, ensuring Alamo residents have a dedicated voice and a dependable source of local information."
874
982
  },
875
983
  {
876
984
  slug: "danvilletimes",
877
985
  name: "Danville Times",
878
986
  url: "https://danvilletimes.com",
879
- area: "Danville",
880
- description: "A new place for Danville\u2019s local stories, independent businesses and community voices."
987
+ area: "Danville & Blackhawk",
988
+ slogan: "Local stories. Stronger communities.",
989
+ description: "Civic reporting, community events, local enterprise, youth sports, and cultural heritage for the Town of Danville and Blackhawk.",
990
+ aboutIntro: "Danville Times is an independent local publication dedicated to reporting on the people, businesses, civic decisions, and community traditions of Danville and Blackhawk. From town council meetings and historic downtown preservation along Hartz Avenue to the bustling Iron Horse Trail, youth athletics, and Blackhawk community life, we chronicle the stories that matter to local families. Our mission is to deliver fair, accurate reporting that celebrates community achievement, holds local institutions accountable, and reinforces the bonds that unite our neighborhoods."
881
991
  },
882
992
  {
883
993
  slug: "sanramontimes",
884
994
  name: "San Ramon Times",
885
995
  url: "https://sanramontimes.com",
886
- area: "San Ramon",
887
- description: "A new place for San Ramon\u2019s local stories, independent businesses and community voices."
996
+ area: "San Ramon & Dougherty Valley",
997
+ slogan: "Local stories. Stronger communities.",
998
+ description: "Daily reporting on municipal policy, City Center Bishop Ranch, San Ramon Valley schools, and the neighborhoods of Dougherty Valley.",
999
+ aboutIntro: "San Ramon Times is an independent local publication focused on the rapidly evolving civic, commercial, and residential landscape of San Ramon. We cover the municipal governance that guides our growing city, innovations at Bishop Ranch and City Center, educational excellence in the San Ramon Valley Unified School District, and community life throughout Dougherty Valley. Through clear and constructive journalism, we help residents understand how local decisions affect their daily lives and provide opportunities to participate meaningfully in city affairs."
888
1000
  },
889
1001
  {
890
1002
  slug: "diablovalleynews",
891
1003
  name: "Diablo Valley News",
892
1004
  url: "https://diablovalleynews.com",
893
1005
  area: "Concord \xB7 Walnut Creek \xB7 Pleasant Hill \xB7 Alamo \xB7 Danville \xB7 San Ramon",
894
- description: "Local stories about the people, businesses and community life of the Diablo Valley."
1006
+ slogan: "Local stories. Stronger communities.",
1007
+ description: "Regional perspectives on economic trends, transportation, environmental stewardship, and community life surrounding Mount Diablo.",
1008
+ aboutIntro: "Diablo Valley News is an independent local publication offering cross-community reporting for the towns and cities nestled in the shadow of Mount Diablo. From the I-680 and Route 24 commute corridors to regional parklands, Diablo Valley College initiatives, and valley-wide economic trends, we illuminate the connections between our neighboring communities. By bringing together reporting from across the valley, we help residents understand how shared regional developments influence their individual neighborhoods."
895
1009
  },
896
1010
  {
897
1011
  slug: "trivalleyweekly",
898
1012
  name: "Tri-Valley Weekly",
899
1013
  url: "https://trivalleyweekly.com",
900
- area: "Alamo \xB7 Danville \xB7 San Ramon \xB7 Dublin \xB7 Pleasanton \xB7 Livermore",
901
- description: "Local stories about the businesses, people and community life of the Tri-Valley."
1014
+ area: "Dublin \xB7 Pleasanton \xB7 Livermore",
1015
+ slogan: "Local stories. Stronger communities.",
1016
+ description: "Comprehensive reporting across the innovation and wine corridor of Dublin, Pleasanton, and Livermore, connecting civic developments, local research, and regional culture.",
1017
+ aboutIntro: "Tri-Valley Weekly is an independent local publication dedicated to connecting the dynamic communities of Dublin, Pleasanton, and Livermore. Spanning one of Northern California's premier corridors for technological research, historic downtown heritage, and historic viticulture, the Tri-Valley balances rapid civic growth with cherished local traditions. We report across city lines on transportation, regional commerce, school governance, environmental stewardship, and community culture, giving residents the comprehensive context needed to navigate and shape their shared valley."
902
1018
  },
903
1019
  {
904
1020
  slug: "eastbaypost",
905
1021
  name: "East Bay Post",
906
1022
  url: "https://eastbaypost.com",
907
1023
  area: "The East Bay",
908
- description: "A new publication connecting the people and neighborhoods of the East Bay."
1024
+ slogan: "Local stories. Stronger communities.",
1025
+ description: "Connecting the diverse neighborhoods, public policy, shoreline communities, and independent businesses across the East Bay.",
1026
+ aboutIntro: "East Bay Post is an independent regional publication devoted to exploring the diverse neighborhoods, public policies, and cultural vitality of the East Bay. From waterfront revitalization and transit infrastructure to local arts, community advocacy, and neighborhood commerce across Alameda and Contra Costa counties, we investigate the forces shaping our interconnected urban and suburban communities. We provide rigorous, constructive reporting that bridges community divides and empowers residents to advocate for resilient, inclusive neighborhoods."
909
1027
  },
910
1028
  {
911
1029
  slug: "napagate",
912
1030
  name: "Napa Gate",
913
1031
  url: "https://napagate.com",
914
1032
  area: "Napa Valley",
915
- description: "A new local publication for the people, places and everyday life of Napa Valley.",
1033
+ slogan: "Local stories. Stronger communities.",
1034
+ description: "Bespoke local reporting for the people, agriculture, culinary culture, riverfront, and civic institutions of the Napa Valley.",
1035
+ aboutIntro: "Napa Gate is an independent local publication devoted to the distinctive communities, agricultural heritage, and civic life of the Napa Valley. From municipal decisions in the City of Napa to vineyard stewardship, hospitality labor, wildfire defense, and cultural events throughout Yountville, St. Helena, and Calistoga, we tell the stories of the people who live and work here year-round. We deliver nuanced, independent journalism that respects the valley\u2019s unique identity while examining the economic and environmental choices that will define its future.",
916
1036
  listed: false
917
1037
  }
918
1038
  ];
@@ -1835,7 +1955,9 @@ function NetworkCopyrightNotice({
1835
1955
  children: NETWORK_PARENT.name
1836
1956
  }
1837
1957
  ),
1838
- "."
1958
+ " ",
1959
+ "\u2014 ",
1960
+ NETWORK_PARENT.brandSlogan
1839
1961
  ] }),
1840
1962
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "sm-network-legal", children: [
1841
1963
  prefix,
@@ -2719,6 +2841,240 @@ function communityMapEmbeds(metadata) {
2719
2841
  });
2720
2842
  }
2721
2843
 
2844
+ // src/article-blocks.ts
2845
+ function normalizeUuid(id) {
2846
+ const clean = id.trim().toLowerCase().replace(/[^0-9a-f]/g, "");
2847
+ if (clean.length === 32) {
2848
+ return `${clean.slice(0, 8)}-${clean.slice(8, 12)}-${clean.slice(12, 16)}-${clean.slice(16, 20)}-${clean.slice(20)}`;
2849
+ }
2850
+ return id.trim().toLowerCase();
2851
+ }
2852
+ function escapeHtml(text) {
2853
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2854
+ }
2855
+ function sanitizeInlineMarkdown(raw) {
2856
+ let text = raw.trim();
2857
+ text = text.replace(/on\w+\s*=\s*(?:["'][^"']*["']|[^\s>]+)/gi, "");
2858
+ text = text.replace(/javascript\s*:/gi, "");
2859
+ text = escapeHtml(text);
2860
+ text = text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
2861
+ text = text.replace(/__(.+?)__/g, "<strong>$1</strong>");
2862
+ text = text.replace(/\*([^*]+?)\*/g, "<em>$1</em>");
2863
+ text = text.replace(/_([^_]+?)_/g, "<em>$1</em>");
2864
+ text = text.replace(/`([^`]+?)`/g, "<code>$1</code>");
2865
+ text = text.replace(/\[([^\]]+)\]\(((?:https?:\/\/|mailto:)[^)]+)\)/g, (_m, label, url) => {
2866
+ return `<a href="${escapeHtml(url)}" rel="noopener noreferrer nofollow">${label}</a>`;
2867
+ });
2868
+ return text;
2869
+ }
2870
+ function parseDocumentBlocks(doc) {
2871
+ const mediaList = doc.media || [];
2872
+ const mediaMap = /* @__PURE__ */ new Map();
2873
+ for (const m of mediaList) {
2874
+ mediaMap.set(normalizeUuid(m.file_id), m);
2875
+ if (m.contribution_id) {
2876
+ mediaMap.set(normalizeUuid(m.contribution_id), m);
2877
+ }
2878
+ }
2879
+ if (Array.isArray(doc.blocks) && doc.blocks.length > 0) {
2880
+ const validated = [];
2881
+ for (const b of doc.blocks) {
2882
+ if (b.type === "inline_media") {
2883
+ const norm = normalizeUuid(b.media_id);
2884
+ const resolved = mediaMap.get(norm);
2885
+ if (resolved) {
2886
+ validated.push({
2887
+ type: "inline_media",
2888
+ media_id: resolved.file_id,
2889
+ placement: "inline"
2890
+ });
2891
+ }
2892
+ } else {
2893
+ validated.push(b);
2894
+ }
2895
+ }
2896
+ return validated;
2897
+ }
2898
+ const md = (doc.content_markdown || "").trim();
2899
+ if (!md) return [];
2900
+ const rawParagraphs = md.split(/\n{2,}/);
2901
+ const blocks = [];
2902
+ for (let i = 0; i < rawParagraphs.length; i++) {
2903
+ const p = rawParagraphs[i].trim();
2904
+ if (!p) continue;
2905
+ let matchedMediaId = null;
2906
+ const directiveMatch = p.match(/^::inline_media\{id=["']?([0-9a-f-]{32,36})["']?\}/i) || p.match(/^<!--\s*inline_media:([0-9a-f-]{32,36})\s*-->/i);
2907
+ if (directiveMatch) {
2908
+ matchedMediaId = normalizeUuid(directiveMatch[1]);
2909
+ if (!mediaMap.has(matchedMediaId)) {
2910
+ continue;
2911
+ }
2912
+ } else {
2913
+ const imgMatch = p.match(/^!\[([^\]]*)\]\(([^)]+)\)/);
2914
+ if (imgMatch) {
2915
+ const src = imgMatch[2];
2916
+ const uuidMatch = src.match(/(?:media:|\/)([0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}|[0-9a-f]{32})(?:\/|$|\?)/i);
2917
+ if (uuidMatch) {
2918
+ const norm = normalizeUuid(uuidMatch[1]);
2919
+ if (mediaMap.has(norm)) {
2920
+ matchedMediaId = norm;
2921
+ }
2922
+ }
2923
+ }
2924
+ }
2925
+ if (matchedMediaId && mediaMap.has(matchedMediaId)) {
2926
+ const mediaItem = mediaMap.get(matchedMediaId);
2927
+ blocks.push({
2928
+ type: "inline_media",
2929
+ media_id: mediaItem.file_id,
2930
+ placement: "inline"
2931
+ });
2932
+ const lines2 = p.split("\n").filter((l) => l.trim().length > 0);
2933
+ if (lines2.length > 1 && !lines2[0].startsWith("::") && !lines2[0].startsWith("<!--")) {
2934
+ const remaining = lines2.slice(1).join("\n").trim();
2935
+ const isEcho = mediaItem.artwork_title && remaining.toLowerCase().includes(mediaItem.artwork_title.toLowerCase());
2936
+ if (!isEcho && remaining) {
2937
+ blocks.push({
2938
+ type: "paragraph",
2939
+ html: sanitizeInlineMarkdown(remaining)
2940
+ });
2941
+ }
2942
+ }
2943
+ continue;
2944
+ }
2945
+ const headingMatch = p.match(/^(#{1,6})\s+(.+)$/);
2946
+ if (headingMatch) {
2947
+ blocks.push({
2948
+ type: "heading",
2949
+ level: headingMatch[1].length,
2950
+ html: sanitizeInlineMarkdown(headingMatch[2])
2951
+ });
2952
+ continue;
2953
+ }
2954
+ if (/^(\*{3,}|-{3,})$/.test(p)) {
2955
+ blocks.push({ type: "divider" });
2956
+ continue;
2957
+ }
2958
+ if (p.startsWith(">")) {
2959
+ const quoteText = p.split("\n").map((l) => l.replace(/^>\s?/, "")).join(" ");
2960
+ blocks.push({
2961
+ type: "quote",
2962
+ html: sanitizeInlineMarkdown(quoteText)
2963
+ });
2964
+ continue;
2965
+ }
2966
+ const lines = p.split("\n");
2967
+ const isUnordered = lines.every((l) => /^\s*[-*]\s+/.test(l));
2968
+ const isOrdered = lines.every((l) => /^\s*\d+\.\s+/.test(l));
2969
+ if ((isUnordered || isOrdered) && lines.length > 0) {
2970
+ const items = lines.map(
2971
+ (l) => sanitizeInlineMarkdown(l.replace(/^\s*(?:[-*]|\d+\.)\s+/, ""))
2972
+ );
2973
+ blocks.push({
2974
+ type: "list",
2975
+ ordered: isOrdered,
2976
+ items
2977
+ });
2978
+ continue;
2979
+ }
2980
+ blocks.push({
2981
+ type: "paragraph",
2982
+ html: sanitizeInlineMarkdown(p)
2983
+ });
2984
+ }
2985
+ return blocks;
2986
+ }
2987
+ function formatArtworkCaption(media) {
2988
+ let title = media.artwork_title ? media.artwork_title.trim() : void 0;
2989
+ let artist = media.artist_name ? media.artist_name.trim() : void 0;
2990
+ let mediumAndDate;
2991
+ if (media.medium && media.artwork_date) {
2992
+ mediumAndDate = `${media.medium} (${media.artwork_date})`;
2993
+ } else if (media.medium) {
2994
+ mediumAndDate = media.medium;
2995
+ } else if (media.artwork_date) {
2996
+ mediumAndDate = media.artwork_date;
2997
+ }
2998
+ let credit;
2999
+ if (media.photographer) {
3000
+ credit = `Photo: ${media.photographer}`;
3001
+ } else if (media.credit) {
3002
+ credit = media.credit.startsWith("Photo:") ? media.credit : `Photo: ${media.credit}`;
3003
+ }
3004
+ let source = media.source ? `Source: ${media.source}` : void 0;
3005
+ let captionText = !title && media.caption ? media.caption : void 0;
3006
+ if (!title && media.caption) {
3007
+ const lines = media.caption.split("\n").map((l) => l.trim()).filter(Boolean);
3008
+ if (lines.length >= 2 && (lines[0].startsWith("\u201C") || lines[0].startsWith('"'))) {
3009
+ title = lines[0].replace(/^[“"']|[”"']$/g, "").trim();
3010
+ artist = lines[1];
3011
+ if (lines[2] && !lines[2].startsWith("Source:")) {
3012
+ mediumAndDate = lines[2];
3013
+ }
3014
+ for (const line of lines) {
3015
+ if (line.startsWith("Source:")) {
3016
+ source = line;
3017
+ }
3018
+ }
3019
+ captionText = void 0;
3020
+ }
3021
+ }
3022
+ return {
3023
+ title,
3024
+ artist,
3025
+ mediumAndDate,
3026
+ credit,
3027
+ source,
3028
+ captionText
3029
+ };
3030
+ }
3031
+ function renderArticleBlocksToHtml(blocks, mediaMap, options) {
3032
+ const parts = [];
3033
+ for (const block of blocks) {
3034
+ switch (block.type) {
3035
+ case "paragraph":
3036
+ parts.push(`<p>${block.html}</p>`);
3037
+ break;
3038
+ case "heading":
3039
+ parts.push(`<h${block.level}>${block.html}</h${block.level}>`);
3040
+ break;
3041
+ case "quote":
3042
+ parts.push(`<blockquote><p>${block.html}</p>${block.attribution ? `<cite>${escapeHtml(block.attribution)}</cite>` : ""}</blockquote>`);
3043
+ break;
3044
+ case "list":
3045
+ if (block.ordered) {
3046
+ parts.push(`<ol>${block.items.map((it) => `<li>${it}</li>`).join("")}</ol>`);
3047
+ } else {
3048
+ parts.push(`<ul>${block.items.map((it) => `<li>${it}</li>`).join("")}</ul>`);
3049
+ }
3050
+ break;
3051
+ case "divider":
3052
+ parts.push("<hr />");
3053
+ break;
3054
+ case "inline_media": {
3055
+ const media = mediaMap.get(normalizeUuid(block.media_id));
3056
+ if (!media) break;
3057
+ const info = formatArtworkCaption(media);
3058
+ const prefix = options?.publicMediaUrlPrefix || "";
3059
+ const cleanFileId = normalizeUuid(media.file_id).replace(/-/g, "");
3060
+ const src = prefix ? `${prefix}/${cleanFileId}/full_display.webp` : `/api/media/${media.file_id}`;
3061
+ let captionHtml = '<figcaption class="sm-article-caption">';
3062
+ if (info.title) captionHtml += `<strong class="sm-article-artwork-title">\u201C${escapeHtml(info.title)}\u201D</strong> `;
3063
+ if (info.artist) captionHtml += `<span class="sm-article-artist-name">${escapeHtml(info.artist)}</span> `;
3064
+ if (info.mediumAndDate) captionHtml += `<span class="sm-article-artwork-medium">${escapeHtml(info.mediumAndDate)}</span> `;
3065
+ if (info.captionText) captionHtml += `<span class="sm-article-caption-text">${escapeHtml(info.captionText)}</span> `;
3066
+ if (info.credit) captionHtml += `<span class="sm-article-photo-credit">${escapeHtml(info.credit)}</span>`;
3067
+ captionHtml += "</figcaption>";
3068
+ parts.push(
3069
+ `<figure class="sm-article-figure sm-inline-media" data-media-id="${escapeHtml(media.file_id)}"><img src="${escapeHtml(src)}" alt="${escapeHtml(media.alt || info.title || "Article artwork")}" loading="lazy" decoding="async" />` + captionHtml + `</figure>`
3070
+ );
3071
+ break;
3072
+ }
3073
+ }
3074
+ }
3075
+ return parts.join("\n");
3076
+ }
3077
+
2722
3078
  Object.defineProperty(exports, "COOKIE_CONSENT_BACKUP_KEY", {
2723
3079
  enumerable: true,
2724
3080
  get: function () { return compliance.COOKIE_CONSENT_BACKUP_KEY; }
@@ -2812,6 +3168,7 @@ exports.COOKIE_CATEGORIES = COOKIE_CATEGORIES;
2812
3168
  exports.CURATED_COMMUNITY_CARDS = CURATED_COMMUNITY_CARDS;
2813
3169
  exports.CookieConsentBanner = CookieConsentBanner;
2814
3170
  exports.CookieSettingsView = CookieSettingsView;
3171
+ exports.EDITORIAL_PRINCIPLES = EDITORIAL_PRINCIPLES;
2815
3172
  exports.EDITORIAL_SECTION_ORDER = EDITORIAL_SECTION_ORDER;
2816
3173
  exports.EVENTS = EVENTS;
2817
3174
  exports.FEATURE_NAMES = FEATURE_NAMES;
@@ -2868,6 +3225,7 @@ exports.feedCacheOptions = feedCacheOptions;
2868
3225
  exports.filterNews = filterNews;
2869
3226
  exports.findPublicationByHost = findPublicationByHost;
2870
3227
  exports.findPublicationBySlug = findPublicationBySlug;
3228
+ exports.formatArtworkCaption = formatArtworkCaption;
2871
3229
  exports.getArticleCards = getArticleCards;
2872
3230
  exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
2873
3231
  exports.getStoredTheme = getStoredTheme;
@@ -2888,6 +3246,7 @@ exports.nearbyUpdates = nearbyUpdates;
2888
3246
  exports.newsArticleJsonLd = newsArticleJsonLd;
2889
3247
  exports.normalizedText = normalizedText;
2890
3248
  exports.orderedSections = orderedSections;
3249
+ exports.parseDocumentBlocks = parseDocumentBlocks;
2891
3250
  exports.parseSnapshot = parseSnapshot;
2892
3251
  exports.prepareClassified = prepareClassified;
2893
3252
  exports.prepareJobPost = prepareJobPost;
@@ -2905,10 +3264,12 @@ exports.publicationRecord = publicationRecord;
2905
3264
  exports.readerSources = readerSources;
2906
3265
  exports.recordConsentToCompliance = recordConsentToCompliance;
2907
3266
  exports.referralCode = referralCode;
3267
+ exports.renderArticleBlocksToHtml = renderArticleBlocksToHtml;
2908
3268
  exports.resolveCoupon = resolveCoupon;
2909
3269
  exports.resolveEffectiveTheme = resolveEffectiveTheme;
2910
3270
  exports.resolveLayout = resolveLayout;
2911
3271
  exports.revalidateDecision = revalidateDecision;
3272
+ exports.sanitizeInlineMarkdown = sanitizeInlineMarkdown;
2912
3273
  exports.screenMessage = screenMessage;
2913
3274
  exports.secretsMatch = secretsMatch;
2914
3275
  exports.sectionDefinition = sectionDefinition;
package/dist/index.d.cts CHANGED
@@ -8,12 +8,13 @@ export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, CO
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.cjs';
9
9
  export { CookieSettingsView, CookieSettingsViewProps } from './cookie-settings-view.cjs';
10
10
  export { ThemeToggle, ThemeToggleProps } from './theme-toggle.cjs';
11
- export { NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications } from './network.cjs';
11
+ export { EDITORIAL_PRINCIPLES, EditorialPrinciple, NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications } from './network.cjs';
12
12
  export { LocalNetworkBar, LocalNetworkBarProps } from './network-bar.cjs';
13
13
  export { NetworkCopyrightNotice, NetworkCopyrightNoticeProps } from './network-copyright.cjs';
14
14
  import * as react from 'react';
15
15
  export { VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './verified-card.cjs';
16
16
  export { C as CommunityMapArticleEmbed, c as communityMapEmbeds } from './community-maps-DKcYhQYN.cjs';
17
+ export { C as ContentBlock, S as StructuredDocument, a as StructuredMedia, f as formatArtworkCaption, p as parseDocumentBlocks, r as renderArticleBlocksToHtml, s as sanitizeInlineMarkdown } from './article-blocks-DsabIDmH.cjs';
17
18
 
18
19
  /** Walnut Creek Times broadsheet colors. A paper overrides only what it changes. */
19
20
  declare const BROADSHEET_THEME: PublicationTheme;
package/dist/index.d.ts CHANGED
@@ -8,12 +8,13 @@ export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, CO
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.js';
9
9
  export { CookieSettingsView, CookieSettingsViewProps } from './cookie-settings-view.js';
10
10
  export { ThemeToggle, ThemeToggleProps } from './theme-toggle.js';
11
- export { NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications } from './network.js';
11
+ export { EDITORIAL_PRINCIPLES, EditorialPrinciple, NETWORK_PARENT, NetworkParent, PublicationNetworkItem, communityPublications, findPublicationByHost, findPublicationBySlug, getNetworkPublicationUrl, listedPublications, localPublications } from './network.js';
12
12
  export { LocalNetworkBar, LocalNetworkBarProps } from './network-bar.js';
13
13
  export { NetworkCopyrightNotice, NetworkCopyrightNoticeProps } from './network-copyright.js';
14
14
  import * as react from 'react';
15
15
  export { VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './verified-card.js';
16
16
  export { C as CommunityMapArticleEmbed, c as communityMapEmbeds } from './community-maps-DKcYhQYN.js';
17
+ export { C as ContentBlock, S as StructuredDocument, a as StructuredMedia, f as formatArtworkCaption, p as parseDocumentBlocks, r as renderArticleBlocksToHtml, s as sanitizeInlineMarkdown } from './article-blocks-DsabIDmH.js';
17
18
 
18
19
  /** Walnut Creek Times broadsheet colors. A paper overrides only what it changes. */
19
20
  declare const BROADSHEET_THEME: PublicationTheme;
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  export { CURATED_COMMUNITY_CARDS, VerifiedCardComponent, VerifiedCardSection, getArticleCards, isSafeActionUrl } from './chunk-FMCHQI7K.js';
2
- export { CookieConsentBanner } from './chunk-DC6RMIYL.js';
3
- export { CookieSettingsView } from './chunk-XDGBMMLD.js';
2
+ export { CookieConsentBanner } from './chunk-2IYKWGVM.js';
3
+ export { CookieSettingsView } from './chunk-72PKW2HM.js';
4
4
  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-EVAGI7C5.js';
6
- export { NetworkCopyrightNotice } from './chunk-VRDUSIVG.js';
7
- import { localPublications } from './chunk-56TU57OO.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, 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-56TU57OO.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';
9
9
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
10
- import { definePublication } from './chunk-C7JTEHTH.js';
11
- export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-C7JTEHTH.js';
10
+ export { formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-OISLCYER.js';
11
+ import { definePublication } from './chunk-57YVL6BD.js';
12
+ export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-57YVL6BD.js';
12
13
  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';
13
14
  export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-GB36YM56.js';
14
15
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -23,7 +24,8 @@ var walnutCreekTimes = definePublication({
23
24
  towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
24
25
  nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
25
26
  sections: ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health"],
26
- description: "Local news, independent businesses and community voices from Walnut Creek, Rossmoor, Saranap and Pleasant Hill.",
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.",
27
29
  layout: "broadsheet",
28
30
  layouts: ["broadsheet"],
29
31
  features: { smile: true, jobs: true, classifieds: true },