@scalemule/news-publication 0.0.7
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/LICENSE +21 -0
- package/README.md +74 -0
- package/cookie-consent.css +419 -0
- package/dist/chunk-C7JTEHTH.js +158 -0
- package/dist/chunk-GB36YM56.js +72 -0
- package/dist/chunk-PYCU2T5O.js +93 -0
- package/dist/chunk-R33MJWND.js +261 -0
- package/dist/chunk-TJ7RL3PV.js +252 -0
- package/dist/chunk-YQXS2KQI.js +112 -0
- package/dist/cookie-consent-banner.cjs +210 -0
- package/dist/cookie-consent-banner.d.cts +19 -0
- package/dist/cookie-consent-banner.d.ts +19 -0
- package/dist/cookie-consent-banner.js +3 -0
- package/dist/cookie-settings-view.cjs +470 -0
- package/dist/cookie-settings-view.d.cts +15 -0
- package/dist/cookie-settings-view.d.ts +15 -0
- package/dist/cookie-settings-view.js +3 -0
- package/dist/index.cjs +1460 -0
- package/dist/index.d.cts +317 -0
- package/dist/index.d.ts +317 -0
- package/dist/index.js +441 -0
- package/dist/marketplace-Ch7mdJZv.d.cts +123 -0
- package/dist/marketplace-Ch7mdJZv.d.ts +123 -0
- package/dist/marketplace-forms.cjs +134 -0
- package/dist/marketplace-forms.d.cts +10 -0
- package/dist/marketplace-forms.d.ts +10 -0
- package/dist/marketplace-forms.js +115 -0
- package/dist/marketplace-view.cjs +110 -0
- package/dist/marketplace-view.d.cts +21 -0
- package/dist/marketplace-view.d.ts +21 -0
- package/dist/marketplace-view.js +85 -0
- package/dist/meet-5Ym5bxfh.d.cts +79 -0
- package/dist/meet-5Ym5bxfh.d.ts +79 -0
- package/dist/meet-forms.cjs +188 -0
- package/dist/meet-forms.d.cts +13 -0
- package/dist/meet-forms.d.ts +13 -0
- package/dist/meet-forms.js +166 -0
- package/dist/meet-view.cjs +82 -0
- package/dist/meet-view.d.cts +23 -0
- package/dist/meet-view.d.ts +23 -0
- package/dist/meet-view.js +59 -0
- package/dist/snapshot-BS4hn1dk.d.cts +234 -0
- package/dist/snapshot-BS4hn1dk.d.ts +234 -0
- package/dist/snapshot-file.cjs +192 -0
- package/dist/snapshot-file.d.cts +21 -0
- package/dist/snapshot-file.d.ts +21 -0
- package/dist/snapshot-file.js +38 -0
- package/marketplace.css +61 -0
- package/package.json +103 -0
- package/tokens/broadsheet.css +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { definePublication } from './chunk-C7JTEHTH.js';
|
|
2
|
+
export { BROADSHEET_THEME, FEATURE_NAMES, LAYOUT_COOKIE, acceptRemoteRecord, acceptSnapshot, definePublication, featureEnabled, parseSnapshot, publicationFromSnapshot, publicationRecord, resolveLayout, snapshotFromRecord, syncPublication } from './chunk-C7JTEHTH.js';
|
|
3
|
+
export { CLASSIFIED_CATEGORIES, JOB_EMPLOYMENT_TYPES, categoryLabel, classifiedPath, classifiedSearchPath, clipChars, employmentLabel, isClassifiedCategory, isEmploymentType, jobEmployerBody, jobOpeningBody, jobPostingBody, prepareClassified, prepareJobPost, publicJobPath, publicJobsPath } from './chunk-YQXS2KQI.js';
|
|
4
|
+
export { MEET_ACTIVITIES, activityLabel, canHostMeetup, isAdult, prepareMeetup, screenMessage } from './chunk-GB36YM56.js';
|
|
5
|
+
export { CookieConsentBanner } from './chunk-PYCU2T5O.js';
|
|
6
|
+
export { CookieSettingsView } from './chunk-R33MJWND.js';
|
|
7
|
+
export { COOKIE_CATEGORIES, COOKIE_CONSENT_BACKUP_KEY, COOKIE_CONSENT_EVENT, COOKIE_CONSENT_KEY, COOKIE_CONSENT_MAX_AGE_SECONDS, DEFAULT_PREFERENCES_ACCEPTED, DEFAULT_PREFERENCES_REJECTED, acceptAllCookies, clearCookieConsent, getCookieConsent, hasConsented, isCategoryAllowed, recordConsentToCompliance, rejectOptionalCookies, setCookieConsent } from './chunk-TJ7RL3PV.js';
|
|
8
|
+
|
|
9
|
+
// src/preset.ts
|
|
10
|
+
var walnutCreekTimes = definePublication({
|
|
11
|
+
slug: "walnutcreektimes",
|
|
12
|
+
name: "Walnut Creek Times",
|
|
13
|
+
masthead: ["Walnut", "Creek", "Times"],
|
|
14
|
+
region: "Walnut Creek",
|
|
15
|
+
regionLabel: "Walnut Creek",
|
|
16
|
+
towns: ["Walnut Creek", "Rossmoor", "Saranap", "Pleasant Hill"],
|
|
17
|
+
nearbyTowns: ["Martinez", "Concord", "Contra Costa"],
|
|
18
|
+
sections: ["Civic life", "Community", "Local businesses", "Events", "Food & places"],
|
|
19
|
+
description: "Local news, independent businesses and community voices from Walnut Creek, Rossmoor, Saranap and Pleasant Hill.",
|
|
20
|
+
layout: "broadsheet",
|
|
21
|
+
layouts: ["broadsheet"],
|
|
22
|
+
features: { smile: true, jobs: true, classifieds: true },
|
|
23
|
+
modules: { home: ["daily-smile"] }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/cache.ts
|
|
27
|
+
var NEWS_CACHE = {
|
|
28
|
+
feedRevalidateSeconds: 30,
|
|
29
|
+
articleRevalidateSeconds: 60,
|
|
30
|
+
edgeMaxAgeSeconds: 60,
|
|
31
|
+
staleWhileRevalidateSeconds: 300,
|
|
32
|
+
feedTag: "news-feed",
|
|
33
|
+
articleTag: "news-article",
|
|
34
|
+
inventoryTag: "public-news-discovery-v2",
|
|
35
|
+
paths: ["/", "/news"]
|
|
36
|
+
};
|
|
37
|
+
var feedCacheOptions = {
|
|
38
|
+
revalidate: NEWS_CACHE.feedRevalidateSeconds,
|
|
39
|
+
tags: [NEWS_CACHE.feedTag]
|
|
40
|
+
};
|
|
41
|
+
var articleCacheOptions = {
|
|
42
|
+
revalidate: NEWS_CACHE.articleRevalidateSeconds,
|
|
43
|
+
tags: [NEWS_CACHE.articleTag]
|
|
44
|
+
};
|
|
45
|
+
var inventoryCacheOptions = {
|
|
46
|
+
revalidate: NEWS_CACHE.feedRevalidateSeconds,
|
|
47
|
+
tags: [NEWS_CACHE.feedTag, NEWS_CACHE.inventoryTag]
|
|
48
|
+
};
|
|
49
|
+
function edgeCacheControl() {
|
|
50
|
+
return `public, s-maxage=${NEWS_CACHE.edgeMaxAgeSeconds}, stale-while-revalidate=${NEWS_CACHE.staleWhileRevalidateSeconds}`;
|
|
51
|
+
}
|
|
52
|
+
function feedCacheKey(publicationId, before, section) {
|
|
53
|
+
return `${publicationId}:${before ?? ""}:${section ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
function articleCacheKey(publicationId, slug) {
|
|
56
|
+
return `${publicationId}:${slug}`;
|
|
57
|
+
}
|
|
58
|
+
var NewsError = class extends Error {
|
|
59
|
+
constructor(status, message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.status = status;
|
|
62
|
+
this.name = "NewsError";
|
|
63
|
+
}
|
|
64
|
+
status;
|
|
65
|
+
};
|
|
66
|
+
function shouldServeLastKnown(error) {
|
|
67
|
+
const status = error instanceof NewsError ? error.status : error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
68
|
+
return status !== 404;
|
|
69
|
+
}
|
|
70
|
+
async function withLastKnown(store, key, load, accept = () => true) {
|
|
71
|
+
try {
|
|
72
|
+
const value = await load();
|
|
73
|
+
if (accept(value)) store.set(key, value);
|
|
74
|
+
return value;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!shouldServeLastKnown(error)) throw error;
|
|
77
|
+
const cached = store.get(key);
|
|
78
|
+
if (cached) return cached;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function secretsMatch(provided, secret) {
|
|
83
|
+
if (!provided || !secret) return false;
|
|
84
|
+
const left = Array.from(provided);
|
|
85
|
+
const right = Array.from(secret);
|
|
86
|
+
let mismatch = left.length === right.length ? 0 : 1;
|
|
87
|
+
const length = Math.max(left.length, right.length);
|
|
88
|
+
for (let index = 0; index < length; index += 1) {
|
|
89
|
+
mismatch |= (left[index]?.codePointAt(0) ?? 0) ^ (right[index]?.codePointAt(0) ?? 0);
|
|
90
|
+
}
|
|
91
|
+
return mismatch === 0;
|
|
92
|
+
}
|
|
93
|
+
function revalidateDecision(input) {
|
|
94
|
+
if (!secretsMatch(input.providedSecret, input.configuredSecret)) {
|
|
95
|
+
return { status: 401, body: { error: "Unauthorized" } };
|
|
96
|
+
}
|
|
97
|
+
const tag = input.tag?.trim() || `${NEWS_CACHE.feedTag}, ${NEWS_CACHE.articleTag}`;
|
|
98
|
+
const path = input.path?.trim() || NEWS_CACHE.paths.join(", ");
|
|
99
|
+
return { status: 200, body: { revalidated: true, tag, path, now: input.now ?? Date.now() } };
|
|
100
|
+
}
|
|
101
|
+
function defaultRevalidateTargets() {
|
|
102
|
+
return {
|
|
103
|
+
tags: [NEWS_CACHE.feedTag, NEWS_CACHE.articleTag, NEWS_CACHE.inventoryTag],
|
|
104
|
+
paths: [...NEWS_CACHE.paths]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/editorial.ts
|
|
109
|
+
var labels = {
|
|
110
|
+
"Local businesses": "Local business",
|
|
111
|
+
"Food & places": "Food & drink"
|
|
112
|
+
};
|
|
113
|
+
function sectionId(name) {
|
|
114
|
+
return "section-" + name.normalize("NFKD").replace(/\p{Mark}/gu, "").toLowerCase().replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-|-$/g, "");
|
|
115
|
+
}
|
|
116
|
+
function sectionPath(name) {
|
|
117
|
+
return name === "Local news" ? "/news" : "/sections/" + encodeURIComponent(sectionId(name).replace(/^section-/, ""));
|
|
118
|
+
}
|
|
119
|
+
function sectionDefinition(name) {
|
|
120
|
+
return { name, label: labels[name] ?? name, id: sectionId(name), items: [] };
|
|
121
|
+
}
|
|
122
|
+
function editorialSections(items) {
|
|
123
|
+
const groups = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
const name = item.section?.trim() || "Local news";
|
|
126
|
+
groups.set(name, [...groups.get(name) ?? [], item]);
|
|
127
|
+
}
|
|
128
|
+
return Array.from(groups, ([name, articles]) => ({ ...sectionDefinition(name), items: articles }));
|
|
129
|
+
}
|
|
130
|
+
function composeEdition(items, selection = {}, now = Date.now()) {
|
|
131
|
+
const seen = /* @__PURE__ */ new Set();
|
|
132
|
+
const inventory = items.filter((item) => {
|
|
133
|
+
if (!item.id || !item.slug || seen.has(item.id)) return false;
|
|
134
|
+
seen.add(item.id);
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
|
|
138
|
+
const lead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory[0];
|
|
139
|
+
const remaining = inventory.filter((item) => item !== lead);
|
|
140
|
+
const requested = new Set(pins.secondary_slugs ?? []);
|
|
141
|
+
const ordered = [
|
|
142
|
+
...(pins.secondary_slugs ?? []).flatMap((slug) => remaining.find((item) => item.slug === slug) ?? []),
|
|
143
|
+
...remaining.filter((item) => !requested.has(item.slug))
|
|
144
|
+
];
|
|
145
|
+
const secondary = Array.from(new Map(ordered.map((item) => [item.id, item])).values()).slice(0, 3);
|
|
146
|
+
for (const item of secondary) remaining.splice(remaining.indexOf(item), 1);
|
|
147
|
+
const eligible = remaining.filter((item) => {
|
|
148
|
+
const age = now - Date.parse(item.published_at);
|
|
149
|
+
return item.excerpt?.trim() && age >= 0 && age <= 7 * 864e5;
|
|
150
|
+
});
|
|
151
|
+
const selected = (pins.brief_slugs ?? []).flatMap((slug) => eligible.find((item) => item.slug === slug) ?? []);
|
|
152
|
+
const candidates = [...selected, ...editorialSections(eligible).map((group) => group.items[0]), ...eligible];
|
|
153
|
+
const briefIds = /* @__PURE__ */ new Set();
|
|
154
|
+
const briefs = candidates.filter((item) => {
|
|
155
|
+
if (!item || briefIds.has(item.id) || briefIds.size >= 4) return false;
|
|
156
|
+
briefIds.add(item.id);
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
|
|
160
|
+
const compactIds = new Set(compact.map((item) => item.id));
|
|
161
|
+
const sections = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
|
|
162
|
+
return { lead, secondary, briefs, compact, sections, navigation: editorialSections(inventory) };
|
|
163
|
+
}
|
|
164
|
+
function displayHeadline(article, selection = {}) {
|
|
165
|
+
return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
|
|
166
|
+
}
|
|
167
|
+
function briefSummary(article, selection = {}) {
|
|
168
|
+
const chosen = selection.stories?.[article.slug]?.brief?.trim();
|
|
169
|
+
if (chosen) return chosen;
|
|
170
|
+
const excerpt = article.excerpt?.trim();
|
|
171
|
+
if (!excerpt) return "";
|
|
172
|
+
const first = [...new Intl.Segmenter("en", { granularity: "sentence" }).segment(excerpt)][0]?.segment.trim() ?? "";
|
|
173
|
+
return Array.from(first).length <= 280 && /[.!?。!?][”’"']?$/u.test(first) ? first : "";
|
|
174
|
+
}
|
|
175
|
+
function normalizedText(value) {
|
|
176
|
+
return value.normalize("NFKD").replace(/\p{Mark}/gu, "").toLowerCase();
|
|
177
|
+
}
|
|
178
|
+
function townSlug(town) {
|
|
179
|
+
return sectionId(town).replace(/^section-/, "");
|
|
180
|
+
}
|
|
181
|
+
function storyTowns(article, towns, selection = {}) {
|
|
182
|
+
const explicit = selection.stories?.[article.slug]?.towns;
|
|
183
|
+
if (explicit) return towns.filter((town) => explicit.includes(town));
|
|
184
|
+
const words = ` ${normalizedText(`${article.title} ${article.excerpt ?? ""}`).replace(/[^\p{Letter}\p{Number}]+/gu, " ")} `;
|
|
185
|
+
return towns.filter((town) => words.includes(` ${normalizedText(town).replace(/[^\p{Letter}\p{Number}]+/gu, " ")} `));
|
|
186
|
+
}
|
|
187
|
+
function tagSlug(value) {
|
|
188
|
+
return value.normalize("NFKD").replace(/\p{Mark}/gu, "").toLowerCase().replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-|-$/g, "");
|
|
189
|
+
}
|
|
190
|
+
function matchesStoryTag(article, tag) {
|
|
191
|
+
if (!tag) return true;
|
|
192
|
+
const target = tagSlug(tag);
|
|
193
|
+
if ((article.tags ?? []).some((item) => tagSlug(item.slug) === target || tagSlug(item.name) === target)) return true;
|
|
194
|
+
const normalized = normalizedText(`${article.title} ${article.excerpt ?? ""} ${article.section ?? ""}`);
|
|
195
|
+
return normalized.includes(target.replace(/-/g, " ")) || normalized.includes(target);
|
|
196
|
+
}
|
|
197
|
+
function filterNews(items, filters, publication, selection = {}) {
|
|
198
|
+
const terms = normalizedText(filters.q ?? "").split(/\s+/u).filter(Boolean);
|
|
199
|
+
const matchTag = publication.matchTag ?? matchesStoryTag;
|
|
200
|
+
return items.filter((item) => (!filters.section || sectionId(item.section || "Local news") === `section-${filters.section}`) && (!filters.town || storyTowns(item, publication.towns, selection).some((town) => townSlug(town) === filters.town)) && (!filters.tag || matchTag(item, filters.tag)) && terms.every((term) => normalizedText(`${item.title} ${item.excerpt ?? ""} ${item.section ?? ""}`).includes(term)));
|
|
201
|
+
}
|
|
202
|
+
function validCalendarDate(value) {
|
|
203
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().startsWith(value);
|
|
204
|
+
}
|
|
205
|
+
function activeNearby(selection = {}, now = Date.now()) {
|
|
206
|
+
const today = new Intl.DateTimeFormat("en-CA", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" }).format(now);
|
|
207
|
+
const lastDay = new Date(Date.parse(today) + 6 * 864e5).toISOString().slice(0, 10);
|
|
208
|
+
return (selection.nearby ?? []).filter((update) => validCalendarDate(update.starts) && validCalendarDate(update.ends ?? update.starts) && (update.ends ?? update.starts) >= update.starts && update.starts <= lastDay && (update.ends ?? update.starts) >= today).sort((a, b) => a.starts.localeCompare(b.starts)).slice(0, 24);
|
|
209
|
+
}
|
|
210
|
+
function nearbyUpdates(items, selection = {}, now = Date.now()) {
|
|
211
|
+
const published = new Map(items.map((item) => [item.slug, item]));
|
|
212
|
+
return activeNearby(selection, now).filter((update) => published.has(update.slug)).map((update) => ({ ...update, article: published.get(update.slug) }));
|
|
213
|
+
}
|
|
214
|
+
function shortDeck(value, limit = 160) {
|
|
215
|
+
const text = value?.trim().replace(/\s+/gu, " ") ?? "";
|
|
216
|
+
const characters = Array.from(text);
|
|
217
|
+
if (characters.length <= limit) return text;
|
|
218
|
+
let sentences = "";
|
|
219
|
+
for (const { segment } of new Intl.Segmenter("en", { granularity: "sentence" }).segment(text)) {
|
|
220
|
+
if (Array.from(sentences + segment.trimEnd()).length > limit) break;
|
|
221
|
+
sentences += segment;
|
|
222
|
+
}
|
|
223
|
+
if (sentences.trim()) return sentences.trim();
|
|
224
|
+
const opening = characters.slice(0, limit).join("");
|
|
225
|
+
const space = opening.lastIndexOf(" ");
|
|
226
|
+
return (space > opening.length / 2 ? opening.slice(0, space) : opening).trimEnd() + "\u2026";
|
|
227
|
+
}
|
|
228
|
+
function storyDate(value) {
|
|
229
|
+
const date = new Date(value);
|
|
230
|
+
return Number.isNaN(date.getTime()) ? null : date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "America/Los_Angeles" });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/seo.ts
|
|
234
|
+
function publicHttps(value) {
|
|
235
|
+
if (!value) return void 0;
|
|
236
|
+
try {
|
|
237
|
+
const url = new URL(value);
|
|
238
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.toString() : void 0;
|
|
239
|
+
} catch {
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function articleUrl(canonical, slug) {
|
|
244
|
+
return `${canonical.replace(/\/$/, "")}/news/${encodeURIComponent(slug)}`;
|
|
245
|
+
}
|
|
246
|
+
function newsArticleJsonLd(publication, article, canonical, authorName) {
|
|
247
|
+
const url = articleUrl(canonical, article.slug);
|
|
248
|
+
const canonicalUrl = publicHttps(article.canonical_url) ?? url;
|
|
249
|
+
const image = publicHttps(article.cover_image_url);
|
|
250
|
+
return {
|
|
251
|
+
"@context": "https://schema.org",
|
|
252
|
+
"@type": "NewsArticle",
|
|
253
|
+
headline: article.title,
|
|
254
|
+
description: article.excerpt ?? void 0,
|
|
255
|
+
datePublished: article.published_at,
|
|
256
|
+
dateModified: article.updated_at,
|
|
257
|
+
image: image ? [image] : void 0,
|
|
258
|
+
mainEntityOfPage: canonicalUrl,
|
|
259
|
+
url,
|
|
260
|
+
author: { "@type": "Organization", name: authorName || publication.name },
|
|
261
|
+
publisher: { "@type": "NewsMediaOrganization", name: publication.name, url: canonical }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function publicationJsonLd(publication, canonical) {
|
|
265
|
+
return {
|
|
266
|
+
"@context": "https://schema.org",
|
|
267
|
+
"@type": "NewsMediaOrganization",
|
|
268
|
+
name: publication.name,
|
|
269
|
+
url: canonical,
|
|
270
|
+
description: publication.description,
|
|
271
|
+
areaServed: publication.towns
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function publicationMeta(publication, canonical, indexable) {
|
|
275
|
+
const title = `${publication.name} | Local news, business & community`;
|
|
276
|
+
return {
|
|
277
|
+
title,
|
|
278
|
+
description: publication.description,
|
|
279
|
+
canonical,
|
|
280
|
+
indexable,
|
|
281
|
+
openGraph: {
|
|
282
|
+
title: publication.name,
|
|
283
|
+
description: publication.description,
|
|
284
|
+
url: canonical,
|
|
285
|
+
siteName: publication.name,
|
|
286
|
+
type: "website"
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function articleMeta(publication, article, canonical, indexable) {
|
|
291
|
+
const url = articleUrl(canonical, article.slug);
|
|
292
|
+
const canonicalUrl = publicHttps(article.canonical_url) ?? url;
|
|
293
|
+
const image = publicHttps(article.og_image_url) ?? publicHttps(article.cover_image_url);
|
|
294
|
+
return {
|
|
295
|
+
title: article.seo_title ?? article.title,
|
|
296
|
+
description: article.seo_description ?? article.excerpt ?? publication.description,
|
|
297
|
+
canonical: canonicalUrl,
|
|
298
|
+
indexable: indexable && !article.no_index,
|
|
299
|
+
openGraph: {
|
|
300
|
+
type: "article",
|
|
301
|
+
title: article.title,
|
|
302
|
+
description: article.excerpt ?? void 0,
|
|
303
|
+
url,
|
|
304
|
+
publishedTime: article.published_at,
|
|
305
|
+
modifiedTime: article.updated_at,
|
|
306
|
+
images: image ? [image] : []
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function llmsText(publication, canonical) {
|
|
311
|
+
const origin = canonical.replace(/\/$/, "");
|
|
312
|
+
return [
|
|
313
|
+
`# ${publication.name}`,
|
|
314
|
+
publication.description,
|
|
315
|
+
`Towns: ${publication.towns.join(", ")}`,
|
|
316
|
+
`Site: ${origin}`,
|
|
317
|
+
`Latest: ${origin}/news`,
|
|
318
|
+
`Sitemap: ${origin}/sitemap.xml`
|
|
319
|
+
].join("\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/analytics.ts
|
|
323
|
+
var ANALYTICS_VERSION = 1;
|
|
324
|
+
var ANALYTICS_COOKIE = "napsite_analytics_v1";
|
|
325
|
+
var SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
326
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
327
|
+
var browser = (category) => ({ category, source: "browser" });
|
|
328
|
+
var server = () => ({ category: "conversion", source: "server" });
|
|
329
|
+
var EVENTS = {
|
|
330
|
+
page_viewed: browser("navigation"),
|
|
331
|
+
page_engaged: browser("engagement"),
|
|
332
|
+
article_read_completed: browser("content"),
|
|
333
|
+
article_shared: browser("engagement"),
|
|
334
|
+
promotion_viewed: browser("content"),
|
|
335
|
+
promotion_clicked: browser("engagement"),
|
|
336
|
+
newsletter_signup_started: browser("engagement"),
|
|
337
|
+
newsletter_subscription_confirmed: server(),
|
|
338
|
+
web_vital_recorded: browser("system"),
|
|
339
|
+
scroll_depth_reached: browser("content"),
|
|
340
|
+
navigation_clicked: browser("navigation"),
|
|
341
|
+
cta_clicked: browser("engagement"),
|
|
342
|
+
contact_link_clicked: browser("engagement"),
|
|
343
|
+
outbound_link_clicked: browser("engagement"),
|
|
344
|
+
pricing_plan_selected: browser("engagement"),
|
|
345
|
+
comparison_used: browser("engagement"),
|
|
346
|
+
mobile_menu_opened: browser("navigation"),
|
|
347
|
+
form_started: browser("engagement"),
|
|
348
|
+
form_submitted: browser("engagement"),
|
|
349
|
+
form_failed: browser("system"),
|
|
350
|
+
referral_landed: browser("engagement"),
|
|
351
|
+
coupon_applied: browser("engagement"),
|
|
352
|
+
visitor_session_created: browser("system"),
|
|
353
|
+
contact_submitted: server()
|
|
354
|
+
};
|
|
355
|
+
var UTM_FIELDS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"];
|
|
356
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
357
|
+
"/",
|
|
358
|
+
"/news",
|
|
359
|
+
"/this-week",
|
|
360
|
+
"/newsletter",
|
|
361
|
+
"/advertise",
|
|
362
|
+
"/pricing",
|
|
363
|
+
"/contact",
|
|
364
|
+
"/book",
|
|
365
|
+
"/preview",
|
|
366
|
+
"/about",
|
|
367
|
+
"/coverage",
|
|
368
|
+
"/story-tip",
|
|
369
|
+
"/corrections",
|
|
370
|
+
"/how-it-works",
|
|
371
|
+
"/privacy",
|
|
372
|
+
"/smile",
|
|
373
|
+
"/subscribe",
|
|
374
|
+
"/jobs",
|
|
375
|
+
"/jobs/post",
|
|
376
|
+
"/classifieds",
|
|
377
|
+
"/classifieds/post"
|
|
378
|
+
]);
|
|
379
|
+
function shortText(value, max = 100) {
|
|
380
|
+
if (typeof value !== "string") return void 0;
|
|
381
|
+
const text = value.trim();
|
|
382
|
+
if (!text || /[@\r\n\u0000-\u001f]/.test(text)) return void 0;
|
|
383
|
+
return Array.from(text).slice(0, max).join("");
|
|
384
|
+
}
|
|
385
|
+
function publicUrl(value, originOnly = false) {
|
|
386
|
+
if (typeof value !== "string") return void 0;
|
|
387
|
+
try {
|
|
388
|
+
const url = new URL(value);
|
|
389
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return void 0;
|
|
390
|
+
if (originOnly) return url.origin;
|
|
391
|
+
const path = PUBLIC_PATHS.has(url.pathname) ? url.pathname : /^\/sections\/[^/]+$/.test(url.pathname) ? "/news/section" : /^\/news\/[^/]+$/.test(url.pathname) && url.pathname !== "/news/unsubscribe" ? "/news/article" : /^\/smile\/[^/]+$/.test(url.pathname) ? "/smile" : /^\/jobs\/[^/]+$/.test(url.pathname) ? "/jobs/opening" : /^\/classifieds\/[^/]+$/.test(url.pathname) ? "/classifieds/listing" : "/other";
|
|
392
|
+
return url.origin + path;
|
|
393
|
+
} catch {
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function attribution(url, referrer = "", publication) {
|
|
398
|
+
const parsed = new URL(url);
|
|
399
|
+
const result = { landing_page: publicUrl(url) };
|
|
400
|
+
const referringOrigin = publicUrl(referrer, true);
|
|
401
|
+
if (referringOrigin && new URL(referringOrigin).hostname.replace(/^www\./, "") !== parsed.hostname.replace(/^www\./, "")) {
|
|
402
|
+
result.referrer = referringOrigin;
|
|
403
|
+
}
|
|
404
|
+
for (const key of UTM_FIELDS) {
|
|
405
|
+
const value = shortText(parsed.searchParams.get(key));
|
|
406
|
+
if (value) result[key] = value;
|
|
407
|
+
}
|
|
408
|
+
if (!result.utm_source && ["gclid", "gbraid", "wbraid"].some((key) => parsed.searchParams.has(key))) {
|
|
409
|
+
result.utm_source = "google";
|
|
410
|
+
result.utm_medium ??= "cpc";
|
|
411
|
+
}
|
|
412
|
+
if (!result.utm_source && parsed.searchParams.has("fbclid")) {
|
|
413
|
+
result.utm_source = "facebook";
|
|
414
|
+
result.utm_medium ??= "social";
|
|
415
|
+
}
|
|
416
|
+
if (publication?.referrals.enabled) {
|
|
417
|
+
const code = shortText(parsed.searchParams.get(publication.referrals.queryParam), 64);
|
|
418
|
+
if (code && /^[a-zA-Z0-9_-]+$/.test(code)) result.referral_code = code;
|
|
419
|
+
}
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/commerce.ts
|
|
424
|
+
function resolveCoupon(publication, code) {
|
|
425
|
+
if (!publication.coupons.enabled) return null;
|
|
426
|
+
const normalized = code?.trim();
|
|
427
|
+
if (!normalized) return null;
|
|
428
|
+
if (publication.coupons.resolve) return publication.coupons.resolve(normalized, publication.coupons.offers);
|
|
429
|
+
return publication.coupons.offers.find((offer) => offer.code.localeCompare(normalized, void 0, { sensitivity: "base" }) === 0) ?? null;
|
|
430
|
+
}
|
|
431
|
+
function referralCode(publication, url) {
|
|
432
|
+
if (!publication.referrals.enabled) return null;
|
|
433
|
+
try {
|
|
434
|
+
const value = new URL(url).searchParams.get(publication.referrals.queryParam)?.trim() ?? "";
|
|
435
|
+
return /^[a-zA-Z0-9_-]{1,64}$/.test(value) ? value : null;
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export { ANALYTICS_COOKIE, ANALYTICS_VERSION, EVENTS, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, composeEdition, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, nearbyUpdates, newsArticleJsonLd, normalizedText, publicHttps, publicUrl, publicationJsonLd, publicationMeta, referralCode, resolveCoupon, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Shared jobs and classifieds rules for a local paper. API calls stay in the site. */
|
|
2
|
+
declare const JOB_EMPLOYMENT_TYPES: readonly [{
|
|
3
|
+
readonly value: "full_time";
|
|
4
|
+
readonly label: "Full-time";
|
|
5
|
+
}, {
|
|
6
|
+
readonly value: "part_time";
|
|
7
|
+
readonly label: "Part-time";
|
|
8
|
+
}, {
|
|
9
|
+
readonly value: "contract";
|
|
10
|
+
readonly label: "Contract";
|
|
11
|
+
}, {
|
|
12
|
+
readonly value: "temporary";
|
|
13
|
+
readonly label: "Temporary";
|
|
14
|
+
}, {
|
|
15
|
+
readonly value: "internship";
|
|
16
|
+
readonly label: "Internship";
|
|
17
|
+
}];
|
|
18
|
+
declare const CLASSIFIED_CATEGORIES: readonly [{
|
|
19
|
+
readonly value: "for-sale";
|
|
20
|
+
readonly label: "For sale";
|
|
21
|
+
}, {
|
|
22
|
+
readonly value: "services";
|
|
23
|
+
readonly label: "Services";
|
|
24
|
+
}, {
|
|
25
|
+
readonly value: "housing";
|
|
26
|
+
readonly label: "Housing";
|
|
27
|
+
}, {
|
|
28
|
+
readonly value: "community";
|
|
29
|
+
readonly label: "Community";
|
|
30
|
+
}, {
|
|
31
|
+
readonly value: "wanted";
|
|
32
|
+
readonly label: "Wanted";
|
|
33
|
+
}];
|
|
34
|
+
type EmploymentType = (typeof JOB_EMPLOYMENT_TYPES)[number]["value"];
|
|
35
|
+
type ClassifiedCategory = (typeof CLASSIFIED_CATEGORIES)[number]["value"];
|
|
36
|
+
type PublicJob = {
|
|
37
|
+
id: string;
|
|
38
|
+
slug: string;
|
|
39
|
+
title: string;
|
|
40
|
+
body_md?: string;
|
|
41
|
+
employment_type?: string;
|
|
42
|
+
location_type?: string;
|
|
43
|
+
remote_allowed?: boolean;
|
|
44
|
+
published_at?: string | null;
|
|
45
|
+
};
|
|
46
|
+
type ClassifiedListing = {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
description: string;
|
|
50
|
+
category: string;
|
|
51
|
+
price?: number | null;
|
|
52
|
+
currency?: string;
|
|
53
|
+
location?: {
|
|
54
|
+
town?: string;
|
|
55
|
+
region?: string;
|
|
56
|
+
} | null;
|
|
57
|
+
created_at?: string;
|
|
58
|
+
};
|
|
59
|
+
type PreparedJob = {
|
|
60
|
+
title: string;
|
|
61
|
+
company: string;
|
|
62
|
+
town: string;
|
|
63
|
+
employment_type: EmploymentType;
|
|
64
|
+
description: string;
|
|
65
|
+
apply_email?: string;
|
|
66
|
+
};
|
|
67
|
+
type PreparedClassified = {
|
|
68
|
+
title: string;
|
|
69
|
+
description: string;
|
|
70
|
+
category: ClassifiedCategory;
|
|
71
|
+
price?: number;
|
|
72
|
+
currency: "USD";
|
|
73
|
+
location: {
|
|
74
|
+
town: string;
|
|
75
|
+
region: string;
|
|
76
|
+
};
|
|
77
|
+
expires_at: string;
|
|
78
|
+
};
|
|
79
|
+
declare function clipChars(value: string, max: number): string;
|
|
80
|
+
declare function isEmploymentType(value: string): value is EmploymentType;
|
|
81
|
+
declare function isClassifiedCategory(value: string): value is ClassifiedCategory;
|
|
82
|
+
declare function employmentLabel(value?: string): string;
|
|
83
|
+
declare function categoryLabel(value?: string): string;
|
|
84
|
+
declare function publicJobsPath(query?: string): string;
|
|
85
|
+
declare function publicJobPath(slug: string): string;
|
|
86
|
+
declare function classifiedSearchPath(options?: {
|
|
87
|
+
query?: string;
|
|
88
|
+
category?: string;
|
|
89
|
+
}): string;
|
|
90
|
+
declare function classifiedPath(id: string): string;
|
|
91
|
+
declare function prepareJobPost(input: {
|
|
92
|
+
title?: unknown;
|
|
93
|
+
company?: unknown;
|
|
94
|
+
town?: unknown;
|
|
95
|
+
employment_type?: unknown;
|
|
96
|
+
description?: unknown;
|
|
97
|
+
apply_email?: unknown;
|
|
98
|
+
}, towns: string[]): PreparedJob;
|
|
99
|
+
declare function jobEmployerBody(job: PreparedJob): {
|
|
100
|
+
company_name: string;
|
|
101
|
+
headline: string;
|
|
102
|
+
};
|
|
103
|
+
declare function jobOpeningBody(job: PreparedJob): {
|
|
104
|
+
title: string;
|
|
105
|
+
employment_type: "full_time" | "part_time" | "contract" | "temporary" | "internship";
|
|
106
|
+
location_type: string;
|
|
107
|
+
description_md: string;
|
|
108
|
+
};
|
|
109
|
+
declare function jobPostingBody(job: PreparedJob): {
|
|
110
|
+
channel: "partner_board";
|
|
111
|
+
title_override: string;
|
|
112
|
+
body_md: string;
|
|
113
|
+
language: string;
|
|
114
|
+
};
|
|
115
|
+
declare function prepareClassified(input: {
|
|
116
|
+
title?: unknown;
|
|
117
|
+
description?: unknown;
|
|
118
|
+
category?: unknown;
|
|
119
|
+
town?: unknown;
|
|
120
|
+
price?: unknown;
|
|
121
|
+
}, towns: string[], region: string, now?: Date): PreparedClassified;
|
|
122
|
+
|
|
123
|
+
export { CLASSIFIED_CATEGORIES as C, type EmploymentType as E, JOB_EMPLOYMENT_TYPES as J, type PreparedClassified as P, type ClassifiedCategory as a, type ClassifiedListing as b, type PreparedJob as c, type PublicJob as d, categoryLabel as e, classifiedPath as f, classifiedSearchPath as g, clipChars as h, employmentLabel as i, isClassifiedCategory as j, isEmploymentType as k, jobEmployerBody as l, jobOpeningBody as m, jobPostingBody as n, prepareJobPost as o, prepareClassified as p, publicJobPath as q, publicJobsPath as r };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Shared jobs and classifieds rules for a local paper. API calls stay in the site. */
|
|
2
|
+
declare const JOB_EMPLOYMENT_TYPES: readonly [{
|
|
3
|
+
readonly value: "full_time";
|
|
4
|
+
readonly label: "Full-time";
|
|
5
|
+
}, {
|
|
6
|
+
readonly value: "part_time";
|
|
7
|
+
readonly label: "Part-time";
|
|
8
|
+
}, {
|
|
9
|
+
readonly value: "contract";
|
|
10
|
+
readonly label: "Contract";
|
|
11
|
+
}, {
|
|
12
|
+
readonly value: "temporary";
|
|
13
|
+
readonly label: "Temporary";
|
|
14
|
+
}, {
|
|
15
|
+
readonly value: "internship";
|
|
16
|
+
readonly label: "Internship";
|
|
17
|
+
}];
|
|
18
|
+
declare const CLASSIFIED_CATEGORIES: readonly [{
|
|
19
|
+
readonly value: "for-sale";
|
|
20
|
+
readonly label: "For sale";
|
|
21
|
+
}, {
|
|
22
|
+
readonly value: "services";
|
|
23
|
+
readonly label: "Services";
|
|
24
|
+
}, {
|
|
25
|
+
readonly value: "housing";
|
|
26
|
+
readonly label: "Housing";
|
|
27
|
+
}, {
|
|
28
|
+
readonly value: "community";
|
|
29
|
+
readonly label: "Community";
|
|
30
|
+
}, {
|
|
31
|
+
readonly value: "wanted";
|
|
32
|
+
readonly label: "Wanted";
|
|
33
|
+
}];
|
|
34
|
+
type EmploymentType = (typeof JOB_EMPLOYMENT_TYPES)[number]["value"];
|
|
35
|
+
type ClassifiedCategory = (typeof CLASSIFIED_CATEGORIES)[number]["value"];
|
|
36
|
+
type PublicJob = {
|
|
37
|
+
id: string;
|
|
38
|
+
slug: string;
|
|
39
|
+
title: string;
|
|
40
|
+
body_md?: string;
|
|
41
|
+
employment_type?: string;
|
|
42
|
+
location_type?: string;
|
|
43
|
+
remote_allowed?: boolean;
|
|
44
|
+
published_at?: string | null;
|
|
45
|
+
};
|
|
46
|
+
type ClassifiedListing = {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
description: string;
|
|
50
|
+
category: string;
|
|
51
|
+
price?: number | null;
|
|
52
|
+
currency?: string;
|
|
53
|
+
location?: {
|
|
54
|
+
town?: string;
|
|
55
|
+
region?: string;
|
|
56
|
+
} | null;
|
|
57
|
+
created_at?: string;
|
|
58
|
+
};
|
|
59
|
+
type PreparedJob = {
|
|
60
|
+
title: string;
|
|
61
|
+
company: string;
|
|
62
|
+
town: string;
|
|
63
|
+
employment_type: EmploymentType;
|
|
64
|
+
description: string;
|
|
65
|
+
apply_email?: string;
|
|
66
|
+
};
|
|
67
|
+
type PreparedClassified = {
|
|
68
|
+
title: string;
|
|
69
|
+
description: string;
|
|
70
|
+
category: ClassifiedCategory;
|
|
71
|
+
price?: number;
|
|
72
|
+
currency: "USD";
|
|
73
|
+
location: {
|
|
74
|
+
town: string;
|
|
75
|
+
region: string;
|
|
76
|
+
};
|
|
77
|
+
expires_at: string;
|
|
78
|
+
};
|
|
79
|
+
declare function clipChars(value: string, max: number): string;
|
|
80
|
+
declare function isEmploymentType(value: string): value is EmploymentType;
|
|
81
|
+
declare function isClassifiedCategory(value: string): value is ClassifiedCategory;
|
|
82
|
+
declare function employmentLabel(value?: string): string;
|
|
83
|
+
declare function categoryLabel(value?: string): string;
|
|
84
|
+
declare function publicJobsPath(query?: string): string;
|
|
85
|
+
declare function publicJobPath(slug: string): string;
|
|
86
|
+
declare function classifiedSearchPath(options?: {
|
|
87
|
+
query?: string;
|
|
88
|
+
category?: string;
|
|
89
|
+
}): string;
|
|
90
|
+
declare function classifiedPath(id: string): string;
|
|
91
|
+
declare function prepareJobPost(input: {
|
|
92
|
+
title?: unknown;
|
|
93
|
+
company?: unknown;
|
|
94
|
+
town?: unknown;
|
|
95
|
+
employment_type?: unknown;
|
|
96
|
+
description?: unknown;
|
|
97
|
+
apply_email?: unknown;
|
|
98
|
+
}, towns: string[]): PreparedJob;
|
|
99
|
+
declare function jobEmployerBody(job: PreparedJob): {
|
|
100
|
+
company_name: string;
|
|
101
|
+
headline: string;
|
|
102
|
+
};
|
|
103
|
+
declare function jobOpeningBody(job: PreparedJob): {
|
|
104
|
+
title: string;
|
|
105
|
+
employment_type: "full_time" | "part_time" | "contract" | "temporary" | "internship";
|
|
106
|
+
location_type: string;
|
|
107
|
+
description_md: string;
|
|
108
|
+
};
|
|
109
|
+
declare function jobPostingBody(job: PreparedJob): {
|
|
110
|
+
channel: "partner_board";
|
|
111
|
+
title_override: string;
|
|
112
|
+
body_md: string;
|
|
113
|
+
language: string;
|
|
114
|
+
};
|
|
115
|
+
declare function prepareClassified(input: {
|
|
116
|
+
title?: unknown;
|
|
117
|
+
description?: unknown;
|
|
118
|
+
category?: unknown;
|
|
119
|
+
town?: unknown;
|
|
120
|
+
price?: unknown;
|
|
121
|
+
}, towns: string[], region: string, now?: Date): PreparedClassified;
|
|
122
|
+
|
|
123
|
+
export { CLASSIFIED_CATEGORIES as C, type EmploymentType as E, JOB_EMPLOYMENT_TYPES as J, type PreparedClassified as P, type ClassifiedCategory as a, type ClassifiedListing as b, type PreparedJob as c, type PublicJob as d, categoryLabel as e, classifiedPath as f, classifiedSearchPath as g, clipChars as h, employmentLabel as i, isClassifiedCategory as j, isEmploymentType as k, jobEmployerBody as l, jobOpeningBody as m, jobPostingBody as n, prepareJobPost as o, prepareClassified as p, publicJobPath as q, publicJobsPath as r };
|