@scalemule/news-publication 0.0.91 → 0.0.93

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.
@@ -18,7 +18,9 @@ Credentials: SCALEMULE_API_KEY and SCALEMULE_ACCESS_TOKEN (normal authorized mem
18
18
  Platform admins may use their normal access token with --application-id ID.
19
19
  Optional --api-url URL defaults to https://api.scalemule.com.
20
20
 
21
- edit saves prose, excerpt, cards, citations and tenant-owned tag_ids in one immutable revision.
21
+ edit saves prose, excerpt, cards, citations, tenant-owned tag_ids and the headline set (title,
22
+ headline <= 120 chars with 70 recommended, fly_title <= 40, subtitle as the rubric, seo_title) in
23
+ one immutable revision.
22
24
  Unspecified fields and media are retained. Default: save for review.
23
25
  --release explicitly requests release under the existing policy; it never issues
24
26
  APPROVE or changes a policy. Legacy onboarding requires an explicit
@@ -157,7 +159,7 @@ try {
157
159
  while(!verified){
158
160
  const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,remaining()))});
159
161
  const html=await response.text();
160
- verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
162
+ verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,headline:article.headline,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
161
163
  if(!verified)await new Promise(r=>setTimeout(r,Math.min(300,remaining())));
162
164
  }
163
165
  }catch(error){reason=error.message;}
@@ -31,8 +31,9 @@ var text = (html) => decode(html.replace(/<[^>]*>/gu, " ")).replace(/\s+/gu, " "
31
31
  var plain = (value) => value.replace(/\s+/gu, " ").trim();
32
32
  function articleEditHtmlMatches(html, expected) {
33
33
  const visible = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/giu, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/giu, " ");
34
+ const shown = plain(expected.headline?.trim() || expected.title);
34
35
  const headline = [...visible.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/giu)].some(
35
- (m) => text(m[1]) === plain(expected.title)
36
+ (m) => text(m[1]) === shown
36
37
  );
37
38
  if (!headline) return false;
38
39
  const paragraphs = [
@@ -339,6 +340,29 @@ async function runArticlePhoto(t, input) {
339
340
  }
340
341
  }
341
342
 
343
+ // src/headline.ts
344
+ var HEADLINE_MAX_CHARS = 120;
345
+ var FLY_TITLE_MAX_CHARS = 40;
346
+ var RUBRIC_MAX_CHARS = 500;
347
+ var SEO_TITLE_MAX_CHARS = 200;
348
+ var headlineLength = (value) => Array.from(value).length;
349
+ function headlineProblems(fields) {
350
+ const problems = [];
351
+ const check = (name, value, max) => {
352
+ if (value === void 0 || value === null) return;
353
+ if (typeof value !== "string") {
354
+ problems.push(`${name} must be a string`);
355
+ return;
356
+ }
357
+ if (headlineLength(value.trim()) > max) problems.push(`${name} exceeds ${max} characters`);
358
+ };
359
+ check("headline", fields.headline, HEADLINE_MAX_CHARS);
360
+ check("fly_title", fields.fly_title, FLY_TITLE_MAX_CHARS);
361
+ check("subtitle", fields.subtitle, RUBRIC_MAX_CHARS);
362
+ check("seo_title", fields.seo_title, SEO_TITLE_MAX_CHARS);
363
+ return problems;
364
+ }
365
+
342
366
  // src/article-edit-operation.ts
343
367
  var ArticleEditOperationError = class extends Error {
344
368
  constructor(code, stage, message, status, revisionId) {
@@ -378,7 +402,10 @@ var digest2 = async (v) => [
378
402
  ].map((v2) => v2.toString(16).padStart(2, "0")).join("");
379
403
  var patchKeys = /* @__PURE__ */ new Set([
380
404
  "title",
405
+ "headline",
406
+ "fly_title",
381
407
  "subtitle",
408
+ "seo_title",
382
409
  "content_markdown",
383
410
  "excerpt",
384
411
  "section",
@@ -407,6 +434,8 @@ function validate(input) {
407
434
  } else if (typeof value !== "string")
408
435
  throw Error(key + " must be a string");
409
436
  }
437
+ const problems = headlineProblems(input.patch);
438
+ if (problems.length) throw Error(problems.join("; "));
410
439
  if (input.expectedGeneration !== void 0 && (!Number.isSafeInteger(input.expectedGeneration) || input.expectedGeneration < 1))
411
440
  throw Error("Expected generation must be positive");
412
441
  if (input.enableReviewPolicy && ![
@@ -6,6 +6,7 @@ declare function articlePhotoHtmlMatches(html: string, lead: string | string[],
6
6
  /** Requires visible body and actual card asides; serialized page data is not delivery. */
7
7
  declare function articleEditHtmlMatches(html: string, expected: {
8
8
  title: string;
9
+ headline?: string | null;
9
10
  content_html: string;
10
11
  card_titles: string[];
11
12
  tags?: {
@@ -52,7 +53,7 @@ declare class ArticlePhotoOperationError extends Error {
52
53
  constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
53
54
  }
54
55
 
55
- type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
56
+ type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "headline" | "fly_title" | "subtitle" | "seo_title" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
56
57
  type ArticleEditCheckpoint = {
57
58
  version: 1;
58
59
  identity: string;
@@ -92,7 +93,14 @@ declare class ArticleEditOperationError extends Error {
92
93
 
93
94
  type ArticleDocument = {
94
95
  title: string;
96
+ /** Short display headline (<= 120 characters, 70 recommended); see `headlineSet`. */
97
+ headline?: string;
98
+ /** Kicker above the headline (<= 40 characters). */
99
+ fly_title?: string;
100
+ /** The rubric (deck). */
95
101
  subtitle?: string;
102
+ /** Search title for <title> and previews. */
103
+ seo_title?: string;
96
104
  content_markdown: string;
97
105
  excerpt?: string;
98
106
  cards?: unknown[];
@@ -6,6 +6,7 @@ declare function articlePhotoHtmlMatches(html: string, lead: string | string[],
6
6
  /** Requires visible body and actual card asides; serialized page data is not delivery. */
7
7
  declare function articleEditHtmlMatches(html: string, expected: {
8
8
  title: string;
9
+ headline?: string | null;
9
10
  content_html: string;
10
11
  card_titles: string[];
11
12
  tags?: {
@@ -52,7 +53,7 @@ declare class ArticlePhotoOperationError extends Error {
52
53
  constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
53
54
  }
54
55
 
55
- type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
56
+ type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "headline" | "fly_title" | "subtitle" | "seo_title" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
56
57
  type ArticleEditCheckpoint = {
57
58
  version: 1;
58
59
  identity: string;
@@ -92,7 +93,14 @@ declare class ArticleEditOperationError extends Error {
92
93
 
93
94
  type ArticleDocument = {
94
95
  title: string;
96
+ /** Short display headline (<= 120 characters, 70 recommended); see `headlineSet`. */
97
+ headline?: string;
98
+ /** Kicker above the headline (<= 40 characters). */
99
+ fly_title?: string;
100
+ /** The rubric (deck). */
95
101
  subtitle?: string;
102
+ /** Search title for <title> and previews. */
103
+ seo_title?: string;
96
104
  content_markdown: string;
97
105
  excerpt?: string;
98
106
  cards?: unknown[];
@@ -1,3 +1,5 @@
1
+ import { headlineProblems } from './chunk-SNQKSUWB.js';
2
+
1
3
  // src/article-photo-verification.ts
2
4
  function articlePhotoHtmlMatches(html, lead, social) {
3
5
  const contains = (tag, url) => tag.includes(url) || tag.includes(encodeURIComponent(url));
@@ -29,8 +31,9 @@ var text = (html) => decode(html.replace(/<[^>]*>/gu, " ")).replace(/\s+/gu, " "
29
31
  var plain = (value) => value.replace(/\s+/gu, " ").trim();
30
32
  function articleEditHtmlMatches(html, expected) {
31
33
  const visible = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/giu, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/giu, " ");
34
+ const shown = plain(expected.headline?.trim() || expected.title);
32
35
  const headline = [...visible.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/giu)].some(
33
- (m) => text(m[1]) === plain(expected.title)
36
+ (m) => text(m[1]) === shown
34
37
  );
35
38
  if (!headline) return false;
36
39
  const paragraphs = [
@@ -376,7 +379,10 @@ var digest2 = async (v) => [
376
379
  ].map((v2) => v2.toString(16).padStart(2, "0")).join("");
377
380
  var patchKeys = /* @__PURE__ */ new Set([
378
381
  "title",
382
+ "headline",
383
+ "fly_title",
379
384
  "subtitle",
385
+ "seo_title",
380
386
  "content_markdown",
381
387
  "excerpt",
382
388
  "section",
@@ -405,6 +411,8 @@ function validate(input) {
405
411
  } else if (typeof value !== "string")
406
412
  throw Error(key + " must be a string");
407
413
  }
414
+ const problems = headlineProblems(input.patch);
415
+ if (problems.length) throw Error(problems.join("; "));
408
416
  if (input.expectedGeneration !== void 0 && (!Number.isSafeInteger(input.expectedGeneration) || input.expectedGeneration < 1))
409
417
  throw Error("Expected generation must be positive");
410
418
  if (input.enableReviewPolicy && ![
@@ -0,0 +1,42 @@
1
+ // src/headline.ts
2
+ var HEADLINE_RECOMMENDED_CHARS = 70;
3
+ var HEADLINE_MAX_CHARS = 120;
4
+ var FLY_TITLE_MAX_CHARS = 40;
5
+ var RUBRIC_MAX_CHARS = 500;
6
+ var SEO_TITLE_MAX_CHARS = 200;
7
+ var text = (value) => {
8
+ if (typeof value !== "string") return void 0;
9
+ const trimmed = value.replace(/\s+/gu, " ").trim();
10
+ return trimmed || void 0;
11
+ };
12
+ var headlineLength = (value) => Array.from(value).length;
13
+ function headlineSet(article, selection = {}) {
14
+ const display = selection.stories?.[article.slug];
15
+ const title = text(article.title) ?? "";
16
+ const headline = text(display?.display_title) ?? text(article.headline) ?? title;
17
+ return {
18
+ flyTitle: text(article.fly_title) ?? text(article.section),
19
+ headline,
20
+ rubric: text(display?.deck) ?? text(article.subtitle),
21
+ seoTitle: text(article.seo_title) ?? title,
22
+ title
23
+ };
24
+ }
25
+ function headlineProblems(fields) {
26
+ const problems = [];
27
+ const check = (name, value, max) => {
28
+ if (value === void 0 || value === null) return;
29
+ if (typeof value !== "string") {
30
+ problems.push(`${name} must be a string`);
31
+ return;
32
+ }
33
+ if (headlineLength(value.trim()) > max) problems.push(`${name} exceeds ${max} characters`);
34
+ };
35
+ check("headline", fields.headline, HEADLINE_MAX_CHARS);
36
+ check("fly_title", fields.fly_title, FLY_TITLE_MAX_CHARS);
37
+ check("subtitle", fields.subtitle, RUBRIC_MAX_CHARS);
38
+ check("seo_title", fields.seo_title, SEO_TITLE_MAX_CHARS);
39
+ return problems;
40
+ }
41
+
42
+ export { FLY_TITLE_MAX_CHARS, HEADLINE_MAX_CHARS, HEADLINE_RECOMMENDED_CHARS, RUBRIC_MAX_CHARS, SEO_TITLE_MAX_CHARS, headlineLength, headlineProblems, headlineSet };
package/dist/index.cjs CHANGED
@@ -122,14 +122,14 @@ function resolveSection(item) {
122
122
  if (/\b(food|dining|restaurant|restaurants|wine|winery|brewery|bakery|cafe|coffee|cuisine|chef)\b/i.test(desc)) return "Food & places";
123
123
  if (/\b(community|volunteer|volunteers|library|libraries|neighborhood|nonprofit|charity|youth|senior|seniors)\b/i.test(desc)) return "Community";
124
124
  }
125
- const text = `${item.title} ${item.excerpt ?? ""}`;
126
- if (/\b(health|hospitals?|clinics?|medical|doctors?|pharmac(?:y|ies)|wellness|veterinar(?:y|ian)|pet care|animal welfare|vaccinat(?:ion|ions)|patients?)\b/iu.test(text)) return "Health";
127
- if (/\b(real estate|homeownership|mortgage|housing|tenants?|apartments?|below-market rate|home sales?|condos?)\b/iu.test(text)) return "Real Estate";
128
- if (/\b(city council|planning commission|board of supervisors|ballot|measure [a-z0-9]|election|transit|traffic|police|firefighter|wildfire|evacuation|pedestrian safety|city manager|public works)\b/iu.test(text)) return "Civic life";
129
- if (/\b(storefront|retail|downtown business|chamber of commerce|opens? doors?|boutique|merchants?|shoppers?|commercial space)\b/iu.test(text)) return "Local businesses";
130
- if (/\b(festival|parade|fair|concert|workshop|exhibition|showcase|celebration|weigh-off|halloween|trunk-or-treat)\b/iu.test(text)) return "Events";
131
- if (/\b(schools?|students?|teachers?|campuses?|classrooms?|high school|elementary|school district)\b/iu.test(text)) return "Schools";
132
- if (/\b(restaurants?|cafes?|baker(?:y|ies)|winer(?:y|ies)|brewer(?:y|ies)|dining|chefs?|menus?)\b/iu.test(text)) return "Food & places";
125
+ const text2 = `${item.title} ${item.excerpt ?? ""}`;
126
+ if (/\b(health|hospitals?|clinics?|medical|doctors?|pharmac(?:y|ies)|wellness|veterinar(?:y|ian)|pet care|animal welfare|vaccinat(?:ion|ions)|patients?)\b/iu.test(text2)) return "Health";
127
+ if (/\b(real estate|homeownership|mortgage|housing|tenants?|apartments?|below-market rate|home sales?|condos?)\b/iu.test(text2)) return "Real Estate";
128
+ if (/\b(city council|planning commission|board of supervisors|ballot|measure [a-z0-9]|election|transit|traffic|police|firefighter|wildfire|evacuation|pedestrian safety|city manager|public works)\b/iu.test(text2)) return "Civic life";
129
+ if (/\b(storefront|retail|downtown business|chamber of commerce|opens? doors?|boutique|merchants?|shoppers?|commercial space)\b/iu.test(text2)) return "Local businesses";
130
+ if (/\b(festival|parade|fair|concert|workshop|exhibition|showcase|celebration|weigh-off|halloween|trunk-or-treat)\b/iu.test(text2)) return "Events";
131
+ if (/\b(schools?|students?|teachers?|campuses?|classrooms?|high school|elementary|school district)\b/iu.test(text2)) return "Schools";
132
+ if (/\b(restaurants?|cafes?|baker(?:y|ies)|winer(?:y|ies)|brewer(?:y|ies)|dining|chefs?|menus?)\b/iu.test(text2)) return "Food & places";
133
133
  return "Community";
134
134
  }
135
135
  function orderedSections(sections2, order = EDITORIAL_SECTION_ORDER) {
@@ -234,7 +234,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
234
234
  return { lead, secondary, briefs, compact, sections: sections2, pendingImages, navigation: editorialSections(inventory) };
235
235
  }
236
236
  function displayHeadline(article, selection = {}) {
237
- return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
237
+ return selection.stories?.[article.slug]?.display_title?.trim() || article.headline?.trim() || article.title;
238
238
  }
239
239
  function briefSummary(article, selection = {}) {
240
240
  const chosen = selection.stories?.[article.slug]?.brief?.trim();
@@ -284,11 +284,11 @@ function nearbyUpdates(items, selection = {}, now = Date.now()) {
284
284
  return activeNearby(selection, now).filter((update) => published.has(update.slug)).map((update) => ({ ...update, article: published.get(update.slug) }));
285
285
  }
286
286
  function shortDeck(value, limit = 160) {
287
- const text = value?.trim().replace(/\s+/gu, " ") ?? "";
288
- const characters = Array.from(text);
289
- if (characters.length <= limit) return text;
287
+ const text2 = value?.trim().replace(/\s+/gu, " ") ?? "";
288
+ const characters = Array.from(text2);
289
+ if (characters.length <= limit) return text2;
290
290
  let sentences = "";
291
- for (const { segment } of new Intl.Segmenter("en", { granularity: "sentence" }).segment(text)) {
291
+ for (const { segment } of new Intl.Segmenter("en", { granularity: "sentence" }).segment(text2)) {
292
292
  if (Array.from(sentences + segment.trimEnd()).length > limit) break;
293
293
  sentences += segment;
294
294
  }
@@ -322,8 +322,9 @@ function newsArticleJsonLd(publication, article, canonical, authorName) {
322
322
  return {
323
323
  "@context": "https://schema.org",
324
324
  "@type": "NewsArticle",
325
- headline: article.title,
326
- description: article.excerpt ?? void 0,
325
+ headline: article.headline?.trim() || article.title,
326
+ ...article.headline?.trim() && article.headline.trim() !== article.title ? { alternativeHeadline: article.title } : {},
327
+ description: article.subtitle ?? article.excerpt ?? void 0,
327
328
  datePublished: article.published_at,
328
329
  ...article.metadata?.editorial?.modified_at ? { dateModified: article.metadata.editorial.modified_at } : {},
329
330
  // updated_at also changes for media/operational maintenance. A trusted
@@ -370,15 +371,15 @@ function articleMeta(publication, article, canonical, indexable) {
370
371
  const canonicalUrl = publicHttps(article.canonical_url) ?? url;
371
372
  const image = publicHttps(article.og_image_url) ?? publicHttps(article.cover_image_url);
372
373
  return {
373
- title: article.title,
374
- description: article.seo_description ?? article.excerpt ?? publication.description,
374
+ title: article.seo_title?.trim() || article.title,
375
+ description: article.seo_description ?? article.subtitle ?? article.excerpt ?? publication.description,
375
376
  canonical: canonicalUrl,
376
377
  indexable: indexable && !article.no_index,
377
378
  openGraph: {
378
379
  type: "article",
379
380
  siteName: publication.name,
380
- title: article.title,
381
- description: article.excerpt ?? void 0,
381
+ title: article.headline?.trim() || article.title,
382
+ description: article.subtitle ?? article.excerpt ?? void 0,
382
383
  url,
383
384
  publishedTime: article.published_at,
384
385
  images: image ? [image] : []
@@ -807,6 +808,47 @@ function resolveNavigation(context) {
807
808
  return { primary, groups, featured: activeFeatured(context.config, now) };
808
809
  }
809
810
 
811
+ // src/headline.ts
812
+ var HEADLINE_RECOMMENDED_CHARS = 70;
813
+ var HEADLINE_MAX_CHARS = 120;
814
+ var FLY_TITLE_MAX_CHARS = 40;
815
+ var RUBRIC_MAX_CHARS = 500;
816
+ var SEO_TITLE_MAX_CHARS = 200;
817
+ var text = (value) => {
818
+ if (typeof value !== "string") return void 0;
819
+ const trimmed = value.replace(/\s+/gu, " ").trim();
820
+ return trimmed || void 0;
821
+ };
822
+ var headlineLength = (value) => Array.from(value).length;
823
+ function headlineSet(article, selection = {}) {
824
+ const display = selection.stories?.[article.slug];
825
+ const title = text(article.title) ?? "";
826
+ const headline = text(display?.display_title) ?? text(article.headline) ?? title;
827
+ return {
828
+ flyTitle: text(article.fly_title) ?? text(article.section),
829
+ headline,
830
+ rubric: text(display?.deck) ?? text(article.subtitle),
831
+ seoTitle: text(article.seo_title) ?? title,
832
+ title
833
+ };
834
+ }
835
+ function headlineProblems(fields) {
836
+ const problems = [];
837
+ const check = (name, value, max) => {
838
+ if (value === void 0 || value === null) return;
839
+ if (typeof value !== "string") {
840
+ problems.push(`${name} must be a string`);
841
+ return;
842
+ }
843
+ if (headlineLength(value.trim()) > max) problems.push(`${name} exceeds ${max} characters`);
844
+ };
845
+ check("headline", fields.headline, HEADLINE_MAX_CHARS);
846
+ check("fly_title", fields.fly_title, FLY_TITLE_MAX_CHARS);
847
+ check("subtitle", fields.subtitle, RUBRIC_MAX_CHARS);
848
+ check("seo_title", fields.seo_title, SEO_TITLE_MAX_CHARS);
849
+ return problems;
850
+ }
851
+
810
852
  // src/analytics.ts
811
853
  var ANALYTICS_VERSION = 1;
812
854
  var ANALYTICS_COOKIE = "napsite_analytics_v1";
@@ -869,9 +911,9 @@ var PUBLIC_PATHS = /* @__PURE__ */ new Set([
869
911
  ]);
870
912
  function shortText(value, max = 100) {
871
913
  if (typeof value !== "string") return void 0;
872
- const text = value.trim();
873
- if (!text || /[@\r\n\u0000-\u001f]/.test(text)) return void 0;
874
- return Array.from(text).slice(0, max).join("");
914
+ const text2 = value.trim();
915
+ if (!text2 || /[@\r\n\u0000-\u001f]/.test(text2)) return void 0;
916
+ return Array.from(text2).slice(0, max).join("");
875
917
  }
876
918
  function publicUrl(value, originOnly = false) {
877
919
  if (typeof value !== "string") return void 0;
@@ -979,13 +1021,13 @@ function canHostMeetup(accountCreatedAt, now = /* @__PURE__ */ new Date()) {
979
1021
  return Number.isFinite(created) && now.getTime() - created >= HOST_WAIT_MS;
980
1022
  }
981
1023
  function screenMessage(value) {
982
- const text = Array.from(value.trim()).slice(0, 1e3).join("");
983
- if (Array.from(text).length < 1) return { ok: false, reason: "Write a short message." };
984
- if (/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(text)) return { ok: false, reason: "Share an email in person, not in the chat." };
985
- if (/(https?:\/\/|www\.)\S+/i.test(text)) return { ok: false, reason: "Links are not allowed in the chat." };
986
- const digits = text.replace(/\D/g, "");
1024
+ const text2 = Array.from(value.trim()).slice(0, 1e3).join("");
1025
+ if (Array.from(text2).length < 1) return { ok: false, reason: "Write a short message." };
1026
+ if (/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(text2)) return { ok: false, reason: "Share an email in person, not in the chat." };
1027
+ if (/(https?:\/\/|www\.)\S+/i.test(text2)) return { ok: false, reason: "Links are not allowed in the chat." };
1028
+ const digits = text2.replace(/\D/g, "");
987
1029
  if (digits.length >= 10) return { ok: false, reason: "Share a phone number in person, not in the chat." };
988
- return { ok: true, text };
1030
+ return { ok: true, text: text2 };
989
1031
  }
990
1032
  function activityLabel(value, otherLabel = "") {
991
1033
  if (value === "other") return Array.from(otherLabel.trim()).slice(0, 40).join("") || "Something else";
@@ -1088,9 +1130,9 @@ function classifiedPath(id) {
1088
1130
  return `/v1/listings/${encodeURIComponent(id)}`;
1089
1131
  }
1090
1132
  function requireText(value, label, max, min = 1) {
1091
- const text = clipChars(typeof value === "string" ? value : "", max);
1092
- if (Array.from(text).length < min) throw new Error(`Please complete the ${label}.`);
1093
- return text;
1133
+ const text2 = clipChars(typeof value === "string" ? value : "", max);
1134
+ if (Array.from(text2).length < min) throw new Error(`Please complete the ${label}.`);
1135
+ return text2;
1094
1136
  }
1095
1137
  function prepareJobPost(input, towns) {
1096
1138
  const title = requireText(input.title, "job title", 120);
@@ -1623,7 +1665,7 @@ var COOKIE_CATEGORIES = [
1623
1665
  ];
1624
1666
  function CookieConsentBanner({
1625
1667
  title = "Your privacy choices",
1626
- text,
1668
+ text: text2,
1627
1669
  settingsUrl = "/cookies",
1628
1670
  className = "",
1629
1671
  publicationSlug,
@@ -1692,8 +1734,8 @@ function CookieConsentBanner({
1692
1734
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-cookie-banner-card", children: [
1693
1735
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-cookie-banner-content", children: [
1694
1736
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "sm-cookie-banner-title", children: title }),
1695
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-cookie-banner-text", children: text ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1696
- text,
1737
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-cookie-banner-text", children: text2 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1738
+ text2,
1697
1739
  " ",
1698
1740
  /* @__PURE__ */ jsxRuntime.jsx("a", { href: settingsUrl, className: "sm-cookie-banner-link", children: "Cookie Settings" })
1699
1741
  ] }) : defaultText })
@@ -2398,12 +2440,12 @@ function readerSources(input) {
2398
2440
  const url = new URL(value.url);
2399
2441
  if (url.protocol !== "https:" || url.username || url.password || seen.has(url.href)) continue;
2400
2442
  seen.add(url.href);
2401
- const text = (raw, limit) => {
2443
+ const text2 = (raw, limit) => {
2402
2444
  if (typeof raw !== "string") return void 0;
2403
2445
  const trimmed = raw.trim();
2404
2446
  return trimmed ? Array.from(trimmed).slice(0, limit).join("") : void 0;
2405
2447
  };
2406
- output.push({ url: url.href, title: text(value.title, 500), publisher: text(value.publisher, 255) });
2448
+ output.push({ url: url.href, title: text2(value.title, 500), publisher: text2(value.publisher, 255) });
2407
2449
  if (output.length === 5) break;
2408
2450
  } catch {
2409
2451
  continue;
@@ -3254,23 +3296,23 @@ function normalizeUuid(id) {
3254
3296
  }
3255
3297
  return id.trim().toLowerCase();
3256
3298
  }
3257
- function escapeHtml(text) {
3258
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3299
+ function escapeHtml(text2) {
3300
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3259
3301
  }
3260
3302
  function sanitizeInlineMarkdown(raw) {
3261
- let text = raw.trim();
3262
- text = text.replace(/on\w+\s*=\s*(?:["'][^"']*["']|[^\s>]+)/gi, "");
3263
- text = text.replace(/javascript\s*:/gi, "");
3264
- text = escapeHtml(text);
3265
- text = text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
3266
- text = text.replace(/__(.+?)__/g, "<strong>$1</strong>");
3267
- text = text.replace(/\*([^*]+?)\*/g, "<em>$1</em>");
3268
- text = text.replace(/_([^_]+?)_/g, "<em>$1</em>");
3269
- text = text.replace(/`([^`]+?)`/g, "<code>$1</code>");
3270
- text = text.replace(/\[([^\]]+)\]\(((?:https?:\/\/|mailto:)[^)]+)\)/g, (_m, label, url) => {
3303
+ let text2 = raw.trim();
3304
+ text2 = text2.replace(/on\w+\s*=\s*(?:["'][^"']*["']|[^\s>]+)/gi, "");
3305
+ text2 = text2.replace(/javascript\s*:/gi, "");
3306
+ text2 = escapeHtml(text2);
3307
+ text2 = text2.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
3308
+ text2 = text2.replace(/__(.+?)__/g, "<strong>$1</strong>");
3309
+ text2 = text2.replace(/\*([^*]+?)\*/g, "<em>$1</em>");
3310
+ text2 = text2.replace(/_([^_]+?)_/g, "<em>$1</em>");
3311
+ text2 = text2.replace(/`([^`]+?)`/g, "<code>$1</code>");
3312
+ text2 = text2.replace(/\[([^\]]+)\]\(((?:https?:\/\/|mailto:)[^)]+)\)/g, (_m, label, url) => {
3271
3313
  return `<a href="${escapeHtml(url)}" rel="noopener noreferrer nofollow">${label}</a>`;
3272
3314
  });
3273
- return text;
3315
+ return text2;
3274
3316
  }
3275
3317
  function parseDocumentBlocks(doc) {
3276
3318
  const mediaList = doc.media || [];
@@ -3563,7 +3605,7 @@ function measurementConsent(headers, sponsorship = false, now = Date.now()) {
3563
3605
  }
3564
3606
 
3565
3607
  // src/sponsorship-report.ts
3566
- var escape = (text) => String(text ?? "").replace(
3608
+ var escape = (text2) => String(text2 ?? "").replace(
3567
3609
  /[&<>"']/g,
3568
3610
  (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
3569
3611
  );
@@ -3934,6 +3976,9 @@ exports.EDITORIAL_PRINCIPLES = EDITORIAL_PRINCIPLES;
3934
3976
  exports.EDITORIAL_SECTION_ORDER = EDITORIAL_SECTION_ORDER;
3935
3977
  exports.EVENTS = EVENTS;
3936
3978
  exports.FEATURE_NAMES = FEATURE_NAMES;
3979
+ exports.FLY_TITLE_MAX_CHARS = FLY_TITLE_MAX_CHARS;
3980
+ exports.HEADLINE_MAX_CHARS = HEADLINE_MAX_CHARS;
3981
+ exports.HEADLINE_RECOMMENDED_CHARS = HEADLINE_RECOMMENDED_CHARS;
3937
3982
  exports.JOB_EMPLOYMENT_TYPES = JOB_EMPLOYMENT_TYPES;
3938
3983
  exports.LAYOUT_COOKIE = LAYOUT_COOKIE;
3939
3984
  exports.LOCAL_BRIEFING_POLICY_VERSION = LOCAL_BRIEFING_POLICY_VERSION;
@@ -3947,6 +3992,8 @@ exports.NetworkCopyrightNotice = NetworkCopyrightNotice;
3947
3992
  exports.NewsError = NewsError;
3948
3993
  exports.READER_SSO_EVENTS = READER_SSO_EVENTS;
3949
3994
  exports.READER_SSO_PROPERTY_KEYS = READER_SSO_PROPERTY_KEYS;
3995
+ exports.RUBRIC_MAX_CHARS = RUBRIC_MAX_CHARS;
3996
+ exports.SEO_TITLE_MAX_CHARS = SEO_TITLE_MAX_CHARS;
3950
3997
  exports.SESSION_IDLE_MS = SESSION_IDLE_MS;
3951
3998
  exports.SPONSORSHIP_OFFERINGS = SPONSORSHIP_OFFERINGS;
3952
3999
  exports.SPONSORSHIP_POLICY = SPONSORSHIP_POLICY;
@@ -4008,6 +4055,9 @@ exports.getNetworkPublicationUrl = getNetworkPublicationUrl;
4008
4055
  exports.getNetworkReaderHandoffUrl = getNetworkReaderHandoffUrl;
4009
4056
  exports.getStoredTheme = getStoredTheme;
4010
4057
  exports.getSystemTheme = getSystemTheme;
4058
+ exports.headlineLength = headlineLength;
4059
+ exports.headlineProblems = headlineProblems;
4060
+ exports.headlineSet = headlineSet;
4011
4061
  exports.inventoryCacheOptions = inventoryCacheOptions;
4012
4062
  exports.isAdult = isAdult;
4013
4063
  exports.isClassifiedCategory = isClassifiedCategory;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-22lEf_UU.cjs';
2
- export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-22lEf_UU.cjs';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-aYDyFCGs.cjs';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-aYDyFCGs.cjs';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-CO9Unz5M.cjs';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.cjs';
5
- export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-BBTow5Bn.cjs';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-C3kD6oFZ.cjs';
6
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
7
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, consentModel, decorateNetworkUrl, effectivePreferences, getCookieConsent, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.cjs';
@@ -249,6 +249,7 @@ declare function composeEdition(items: NewsItem[], selection?: HomepageSelection
249
249
  pendingImages: NewsItem[];
250
250
  navigation: EditorialSection[];
251
251
  };
252
+ /** The paper's override, else the article's short headline, else its long title (see `headlineSet`). */
252
253
  declare function displayHeadline(article: NewsItem, selection?: HomepageSelection): string;
253
254
  /** A complete existing sentence, not a cut-off or generated summary. */
254
255
  declare function briefSummary(article: NewsItem, selection?: HomepageSelection): string;
@@ -404,11 +405,12 @@ declare function newsArticleJsonLd(publication: NewsPublication, article: NewsAr
404
405
  mainEntityOfPage: string;
405
406
  url: string;
406
407
  dateModified?: string | undefined;
408
+ description: string | undefined;
409
+ datePublished: string;
410
+ alternativeHeadline?: string | undefined;
407
411
  "@context": string;
408
412
  "@type": string;
409
413
  headline: string;
410
- description: string | undefined;
411
- datePublished: string;
412
414
  };
413
415
  declare function publicationJsonLd(publication: NewsPublication, canonical: string): {
414
416
  "@context": string;
@@ -453,6 +455,48 @@ declare function articleMeta(publication: NewsPublication, article: NewsArticle,
453
455
  /** One line a paper can serve at /llms.txt. Towns and the article path stay explicit. */
454
456
  declare function llmsText(publication: NewsPublication, canonical: string): string;
455
457
 
458
+ /**
459
+ * The newspaper headline set. Every article may carry four pieces of title text, each with a job:
460
+ *
461
+ * - `fly_title` the kicker above the headline: a beat, column or section label (<= 40 characters);
462
+ * - `headline` the short display headline, written as a line, not a summary (recommended <= 70,
463
+ * hard limit 120); front pages and the article h1 show it;
464
+ * - `subtitle` the rubric (deck): one sentence that carries the facts;
465
+ * - `seo_title` the search title for <title> and link previews.
466
+ *
467
+ * `title` is the long descriptive title that every article already has; it is the fallback for the
468
+ * headline and the search title, so nothing changes for an article that carries only a title.
469
+ * A per-paper homepage override (`StoryDisplay.display_title`/`deck`) still wins where an editor set one.
470
+ */
471
+ declare const HEADLINE_RECOMMENDED_CHARS = 70;
472
+ declare const HEADLINE_MAX_CHARS = 120;
473
+ declare const FLY_TITLE_MAX_CHARS = 40;
474
+ declare const RUBRIC_MAX_CHARS = 500;
475
+ declare const SEO_TITLE_MAX_CHARS = 200;
476
+ type Headlined = Pick<NewsItem, "slug" | "title" | "section"> & Partial<Pick<NewsItem, "headline" | "fly_title" | "excerpt">> & Partial<Pick<NewsArticle, "subtitle" | "seo_title">>;
477
+ /** Characters, never bytes: Cyrillic, CJK and emoji count once each. */
478
+ declare const headlineLength: (value: string) => number;
479
+ type HeadlineSet = {
480
+ /** Kicker: the editor's fly title, else the section. */
481
+ flyTitle?: string;
482
+ /** Display headline: the paper's override, else the article's headline, else its title. */
483
+ headline: string;
484
+ /** Rubric: the paper's deck override, else the subtitle. Never a cut sentence. */
485
+ rubric?: string;
486
+ /** Search title for <title> and previews. */
487
+ seoTitle: string;
488
+ /** The long descriptive title, for alternativeHeadline when it differs from the headline. */
489
+ title: string;
490
+ };
491
+ declare function headlineSet(article: Headlined, selection?: HomepageSelection): HeadlineSet;
492
+ /** Validation shared by the edit command and the editors: hard limits match the service. */
493
+ declare function headlineProblems(fields: {
494
+ headline?: unknown;
495
+ fly_title?: unknown;
496
+ subtitle?: unknown;
497
+ seo_title?: unknown;
498
+ }): string[];
499
+
456
500
  /** Bounded event contract from Walnut Creek Times. Never accept raw form or chat text. */
457
501
  declare const ANALYTICS_VERSION = 1;
458
502
  declare const ANALYTICS_COOKIE = "napsite_analytics_v1";
@@ -675,4 +719,4 @@ declare function isSponsoredStoryItem(item: NewsItem): boolean;
675
719
  */
676
720
  declare function selectRelevantLocalStories(currentStory: NewsItem, candidates: NewsItem[], publication: Pick<NewsPublication, "regionLabel" | "towns">, limit?: number): NewsItem[];
677
721
 
678
- 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 };
722
+ 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, FLY_TITLE_MAX_CHARS, FeatureName, type FeaturedDestination, HEADLINE_MAX_CHARS, HEADLINE_RECOMMENDED_CHARS, type HeadlineSet, 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, RUBRIC_MAX_CHARS, type ResolvedNavigation, SEO_TITLE_MAX_CHARS, 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, headlineLength, headlineProblems, headlineSet, 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
@@ -1,8 +1,8 @@
1
- import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-22lEf_UU.js';
2
- export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-22lEf_UU.js';
1
+ import { a as PublicationTheme, P as PublicationInput, N as NewsPublication, F as FeatureName, b as NewsItem, H as HomepageSelection, c as NearbyUpdate, E as EditorialSection, d as NewsArticle, C as CouponOffer } from './types-aYDyFCGs.js';
2
+ export { A as ArticleCitation, e as CardType, f as CategoryRef, g as CouponSettings, h as CoverImageMetadata, i as FEATURE_NAMES, j as FeatureFlags, k as NewsFeed, l as NewsLayoutId, m as PublicationModules, R as ReferralSettings, S as StoryDisplay, T as TagRef, V as VerifiedCard, n as VerifiedCardAction, o as VerifiedCardFact, p as VerifiedCardImage } from './types-aYDyFCGs.js';
3
3
  export { M as MEET_ACTIVITIES, b as MeetActivity, c as MeetupInput, a as PreparedMeetup, P as PublicPlace, d as activityLabel, e as canHostMeetup, i as isAdult, p as prepareMeetup, s as screenMessage } from './meet-BWpxY20X.js';
4
4
  export { b as CLASSIFIED_CATEGORIES, c as ClassifiedCategory, C as ClassifiedListing, E as EmploymentType, J as JOB_EMPLOYMENT_TYPES, d as PreparedClassified, e as PreparedJob, P as PublicDeal, f as PublicDealOffer, a as PublicJob, g as categoryLabel, h as classifiedPath, i as classifiedSearchPath, j as clipChars, k as dealDollars, l as dealGoPath, m as dealsFeedPath, n as employmentLabel, o as isClassifiedCategory, p as isEmploymentType, q as jobEmployerBody, r as jobOpeningBody, s as jobPostingBody, t as prepareClassified, u as prepareJobPost, v as publicJobPath, w as publicJobsPath } from './marketplace-29i5w4LS.js';
5
- export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-CHdsfUnU.js';
5
+ export { b as PublicationRecord, P as PublicationSnapshot, S as SnapshotHooks, c as SyncSource, a as SyncedPublication, d as acceptRemoteRecord, e as acceptSnapshot, p as parseSnapshot, f as publicationFromSnapshot, g as publicationRecord, s as snapshotFromRecord, h as syncPublication } from './snapshot-CEtTFD_h.js';
6
6
  import { CookieCategoryDefinition, CookieConsentRecord } from '@scalemule/compliance';
7
7
  export { COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, CookieCategory, CookieCategoryDefinition, CookieConsentRecord, CookieDisclosure, CookiePreferences, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, consentModel, decorateNetworkUrl, effectivePreferences, getCookieConsent, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, queryNetworkConsentHub, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub } from '@scalemule/compliance';
8
8
  export { CookieConsentBanner, CookieConsentBannerProps } from './cookie-consent-banner.js';
@@ -249,6 +249,7 @@ declare function composeEdition(items: NewsItem[], selection?: HomepageSelection
249
249
  pendingImages: NewsItem[];
250
250
  navigation: EditorialSection[];
251
251
  };
252
+ /** The paper's override, else the article's short headline, else its long title (see `headlineSet`). */
252
253
  declare function displayHeadline(article: NewsItem, selection?: HomepageSelection): string;
253
254
  /** A complete existing sentence, not a cut-off or generated summary. */
254
255
  declare function briefSummary(article: NewsItem, selection?: HomepageSelection): string;
@@ -404,11 +405,12 @@ declare function newsArticleJsonLd(publication: NewsPublication, article: NewsAr
404
405
  mainEntityOfPage: string;
405
406
  url: string;
406
407
  dateModified?: string | undefined;
408
+ description: string | undefined;
409
+ datePublished: string;
410
+ alternativeHeadline?: string | undefined;
407
411
  "@context": string;
408
412
  "@type": string;
409
413
  headline: string;
410
- description: string | undefined;
411
- datePublished: string;
412
414
  };
413
415
  declare function publicationJsonLd(publication: NewsPublication, canonical: string): {
414
416
  "@context": string;
@@ -453,6 +455,48 @@ declare function articleMeta(publication: NewsPublication, article: NewsArticle,
453
455
  /** One line a paper can serve at /llms.txt. Towns and the article path stay explicit. */
454
456
  declare function llmsText(publication: NewsPublication, canonical: string): string;
455
457
 
458
+ /**
459
+ * The newspaper headline set. Every article may carry four pieces of title text, each with a job:
460
+ *
461
+ * - `fly_title` the kicker above the headline: a beat, column or section label (<= 40 characters);
462
+ * - `headline` the short display headline, written as a line, not a summary (recommended <= 70,
463
+ * hard limit 120); front pages and the article h1 show it;
464
+ * - `subtitle` the rubric (deck): one sentence that carries the facts;
465
+ * - `seo_title` the search title for <title> and link previews.
466
+ *
467
+ * `title` is the long descriptive title that every article already has; it is the fallback for the
468
+ * headline and the search title, so nothing changes for an article that carries only a title.
469
+ * A per-paper homepage override (`StoryDisplay.display_title`/`deck`) still wins where an editor set one.
470
+ */
471
+ declare const HEADLINE_RECOMMENDED_CHARS = 70;
472
+ declare const HEADLINE_MAX_CHARS = 120;
473
+ declare const FLY_TITLE_MAX_CHARS = 40;
474
+ declare const RUBRIC_MAX_CHARS = 500;
475
+ declare const SEO_TITLE_MAX_CHARS = 200;
476
+ type Headlined = Pick<NewsItem, "slug" | "title" | "section"> & Partial<Pick<NewsItem, "headline" | "fly_title" | "excerpt">> & Partial<Pick<NewsArticle, "subtitle" | "seo_title">>;
477
+ /** Characters, never bytes: Cyrillic, CJK and emoji count once each. */
478
+ declare const headlineLength: (value: string) => number;
479
+ type HeadlineSet = {
480
+ /** Kicker: the editor's fly title, else the section. */
481
+ flyTitle?: string;
482
+ /** Display headline: the paper's override, else the article's headline, else its title. */
483
+ headline: string;
484
+ /** Rubric: the paper's deck override, else the subtitle. Never a cut sentence. */
485
+ rubric?: string;
486
+ /** Search title for <title> and previews. */
487
+ seoTitle: string;
488
+ /** The long descriptive title, for alternativeHeadline when it differs from the headline. */
489
+ title: string;
490
+ };
491
+ declare function headlineSet(article: Headlined, selection?: HomepageSelection): HeadlineSet;
492
+ /** Validation shared by the edit command and the editors: hard limits match the service. */
493
+ declare function headlineProblems(fields: {
494
+ headline?: unknown;
495
+ fly_title?: unknown;
496
+ subtitle?: unknown;
497
+ seo_title?: unknown;
498
+ }): string[];
499
+
456
500
  /** Bounded event contract from Walnut Creek Times. Never accept raw form or chat text. */
457
501
  declare const ANALYTICS_VERSION = 1;
458
502
  declare const ANALYTICS_COOKIE = "napsite_analytics_v1";
@@ -675,4 +719,4 @@ declare function isSponsoredStoryItem(item: NewsItem): boolean;
675
719
  */
676
720
  declare function selectRelevantLocalStories(currentStory: NewsItem, candidates: NewsItem[], publication: Pick<NewsPublication, "regionLabel" | "towns">, limit?: number): NewsItem[];
677
721
 
678
- 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 };
722
+ 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, FLY_TITLE_MAX_CHARS, FeatureName, type FeaturedDestination, HEADLINE_MAX_CHARS, HEADLINE_RECOMMENDED_CHARS, type HeadlineSet, 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, RUBRIC_MAX_CHARS, type ResolvedNavigation, SEO_TITLE_MAX_CHARS, 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, headlineLength, headlineProblems, headlineSet, 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
@@ -8,6 +8,7 @@ export { CookieSettingsView } from './chunk-PBRDIXQI.js';
8
8
  import { localPublications } from './chunk-NNEY6VBX.js';
9
9
  export { COOKIE_CATEGORIES, COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, DEFAULT_CONSENT_HUB_URL, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, EDITORIAL_PRINCIPLES, EU_EEA_UK_COUNTRIES, NETWORK_CONSENT_STORAGE_KEY, NETWORK_PARENT, acceptAllCookies, checkUrlConsentBridge, clearCookieConsent, communityPublications, consentModel, decorateNetworkUrl, effectivePreferences, findPublicationByHost, findPublicationBySlug, getCookieConsent, getNetworkPublicationUrl, getNetworkReaderHandoffUrl, hasConsented, impliedPreferences, isCategoryAllowed, isGeoConsentRequired, listedPublications, localPublications, queryNetworkConsentHub, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent, setNetworkConsentHub, topicalPublications } from './chunk-NNEY6VBX.js';
10
10
  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';
11
+ export { FLY_TITLE_MAX_CHARS, HEADLINE_MAX_CHARS, HEADLINE_RECOMMENDED_CHARS, RUBRIC_MAX_CHARS, SEO_TITLE_MAX_CHARS, headlineLength, headlineProblems, headlineSet } from './chunk-SNQKSUWB.js';
11
12
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
12
13
  export { articleFormatLabel, articleHeroRatio, articlePresentation, formatArtworkCaption, parseDocumentBlocks, renderArticleBlocksToHtml, sanitizeInlineMarkdown } from './chunk-DN3TBZLK.js';
13
14
  import { definePublication } from './chunk-57YVL6BD.js';
@@ -178,7 +179,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
178
179
  return { lead, secondary, briefs, compact, sections: sections2, pendingImages, navigation: editorialSections(inventory) };
179
180
  }
180
181
  function displayHeadline(article, selection = {}) {
181
- return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
182
+ return selection.stories?.[article.slug]?.display_title?.trim() || article.headline?.trim() || article.title;
182
183
  }
183
184
  function briefSummary(article, selection = {}) {
184
185
  const chosen = selection.stories?.[article.slug]?.brief?.trim();
@@ -266,8 +267,9 @@ function newsArticleJsonLd(publication, article, canonical, authorName) {
266
267
  return {
267
268
  "@context": "https://schema.org",
268
269
  "@type": "NewsArticle",
269
- headline: article.title,
270
- description: article.excerpt ?? void 0,
270
+ headline: article.headline?.trim() || article.title,
271
+ ...article.headline?.trim() && article.headline.trim() !== article.title ? { alternativeHeadline: article.title } : {},
272
+ description: article.subtitle ?? article.excerpt ?? void 0,
271
273
  datePublished: article.published_at,
272
274
  ...article.metadata?.editorial?.modified_at ? { dateModified: article.metadata.editorial.modified_at } : {},
273
275
  // updated_at also changes for media/operational maintenance. A trusted
@@ -314,15 +316,15 @@ function articleMeta(publication, article, canonical, indexable) {
314
316
  const canonicalUrl = publicHttps(article.canonical_url) ?? url;
315
317
  const image = publicHttps(article.og_image_url) ?? publicHttps(article.cover_image_url);
316
318
  return {
317
- title: article.title,
318
- description: article.seo_description ?? article.excerpt ?? publication.description,
319
+ title: article.seo_title?.trim() || article.title,
320
+ description: article.seo_description ?? article.subtitle ?? article.excerpt ?? publication.description,
319
321
  canonical: canonicalUrl,
320
322
  indexable: indexable && !article.no_index,
321
323
  openGraph: {
322
324
  type: "article",
323
325
  siteName: publication.name,
324
- title: article.title,
325
- description: article.excerpt ?? void 0,
326
+ title: article.headline?.trim() || article.title,
327
+ description: article.subtitle ?? article.excerpt ?? void 0,
326
328
  url,
327
329
  publishedTime: article.published_at,
328
330
  images: image ? [image] : []
@@ -225,7 +225,7 @@ async function startSignIn(config, input) {
225
225
  }
226
226
  }
227
227
  if (mode === "silent") {
228
- if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked) === "1") {
228
+ if (input.cookies(c.names.signedOut)) {
229
229
  return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
230
230
  }
231
231
  }
@@ -210,7 +210,7 @@ async function startSignIn(config, input) {
210
210
  }
211
211
  }
212
212
  if (mode === "silent") {
213
- if (input.cookies(c.names.signedOut) || input.cookies(c.names.ssoChecked) === "1") {
213
+ if (input.cookies(c.names.signedOut)) {
214
214
  return { ok: false, status: 204, error: "", reason: "suppressed", redirect, cookies: staleSession };
215
215
  }
216
216
  }
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-22lEf_UU.cjs';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-aYDyFCGs.cjs';
2
2
 
3
3
  /**
4
4
  * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
@@ -1,4 +1,4 @@
1
- import { P as PublicationInput, N as NewsPublication } from './types-22lEf_UU.js';
1
+ import { P as PublicationInput, N as NewsPublication } from './types-aYDyFCGs.js';
2
2
 
3
3
  /**
4
4
  * Serializable application config. Code hooks (`matchTag`, `coupons.resolve`)
@@ -1,5 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-BBTow5Bn.cjs';
2
- import './types-22lEf_UU.cjs';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-C3kD6oFZ.cjs';
2
+ import './types-aYDyFCGs.cjs';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-CHdsfUnU.js';
2
- import './types-22lEf_UU.js';
1
+ import { P as PublicationSnapshot, S as SnapshotHooks, a as SyncedPublication } from './snapshot-CEtTFD_h.js';
2
+ import './types-aYDyFCGs.js';
3
3
 
4
4
  declare function readSnapshotFile(file: string): Promise<PublicationSnapshot | null>;
5
5
  /**
@@ -97,7 +97,12 @@ type CategoryRef = {
97
97
  type NewsItem = {
98
98
  id: string;
99
99
  slug: string;
100
+ /** The long descriptive title; the fallback for `headline` and `seo_title`. */
100
101
  title: string;
102
+ /** Short display headline for front pages and the article h1 (see `headlineSet`). */
103
+ headline?: string | null;
104
+ /** Kicker above the headline; the section is the fallback. */
105
+ fly_title?: string | null;
101
106
  excerpt: string | null;
102
107
  cover_image_url: string | null;
103
108
  /** Versioned canonical pixel identity supplied by the publication owner. */
@@ -222,11 +227,13 @@ type NewsArticle = NewsItem & {
222
227
  cards?: VerifiedCard[] | unknown[];
223
228
  };
224
229
  };
230
+ /** A paper's own homepage overrides for one story; `fly_title` here overrides the article's kicker on that paper only. */
225
231
  type StoryDisplay = {
226
232
  display_title?: string;
227
233
  deck?: string;
228
234
  brief?: string;
229
235
  towns?: string[];
236
+ fly_title?: string;
230
237
  };
231
238
  type NearbyUpdate = {
232
239
  slug: string;
@@ -97,7 +97,12 @@ type CategoryRef = {
97
97
  type NewsItem = {
98
98
  id: string;
99
99
  slug: string;
100
+ /** The long descriptive title; the fallback for `headline` and `seo_title`. */
100
101
  title: string;
102
+ /** Short display headline for front pages and the article h1 (see `headlineSet`). */
103
+ headline?: string | null;
104
+ /** Kicker above the headline; the section is the fallback. */
105
+ fly_title?: string | null;
101
106
  excerpt: string | null;
102
107
  cover_image_url: string | null;
103
108
  /** Versioned canonical pixel identity supplied by the publication owner. */
@@ -222,11 +227,13 @@ type NewsArticle = NewsItem & {
222
227
  cards?: VerifiedCard[] | unknown[];
223
228
  };
224
229
  };
230
+ /** A paper's own homepage overrides for one story; `fly_title` here overrides the article's kicker on that paper only. */
225
231
  type StoryDisplay = {
226
232
  display_title?: string;
227
233
  deck?: string;
228
234
  brief?: string;
229
235
  towns?: string[];
236
+ fly_title?: string;
230
237
  };
231
238
  type NearbyUpdate = {
232
239
  slug: string;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { V as VerifiedCard } from './types-22lEf_UU.cjs';
2
+ import { V as VerifiedCard } from './types-aYDyFCGs.cjs';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { V as VerifiedCard } from './types-22lEf_UU.js';
2
+ import { V as VerifiedCard } from './types-aYDyFCGs.js';
3
3
 
4
4
  /**
5
5
  * Validate that an action link is safe for readers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.91",
3
+ "version": "0.0.93",
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": {