@fullstackdatasolutions/articles 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +324 -0
  2. package/dist/index.cjs +1027 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +232 -0
  5. package/dist/index.d.ts +232 -0
  6. package/dist/index.js +974 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/server.cjs +732 -0
  9. package/dist/server.cjs.map +1 -0
  10. package/dist/server.d.cts +127 -0
  11. package/dist/server.d.ts +127 -0
  12. package/dist/server.js +684 -0
  13. package/dist/server.js.map +1 -0
  14. package/package.json +82 -0
  15. package/src/ArticleCard.tsx +63 -0
  16. package/src/ArticleCategoryGrid.tsx +73 -0
  17. package/src/ArticleSchemas.tsx +82 -0
  18. package/src/ArticleSearchBar.tsx +50 -0
  19. package/src/ArticlesHero.tsx +33 -0
  20. package/src/ArticlesPage.tsx +167 -0
  21. package/src/CategoryArticlesPage.tsx +84 -0
  22. package/src/CommentForm.tsx +98 -0
  23. package/src/CommentItem.tsx +123 -0
  24. package/src/CommentThread.tsx +40 -0
  25. package/src/CommentsSection.tsx +84 -0
  26. package/src/FeaturedArticle.tsx +63 -0
  27. package/src/LatestArticles.tsx +42 -0
  28. package/src/LatestArticlesSection.tsx +68 -0
  29. package/src/__tests__/ArticleCard.test.tsx +78 -0
  30. package/src/__tests__/ArticleCategoryGrid.test.tsx +98 -0
  31. package/src/__tests__/ArticleSchemas.test.tsx +130 -0
  32. package/src/__tests__/ArticleSearchBar.test.tsx +79 -0
  33. package/src/__tests__/ArticlesHero.test.tsx +38 -0
  34. package/src/__tests__/ArticlesPage.test.tsx +155 -0
  35. package/src/__tests__/CategoryArticlesPage.test.tsx +156 -0
  36. package/src/__tests__/CommentForm.test.tsx +80 -0
  37. package/src/__tests__/CommentItem.test.tsx +149 -0
  38. package/src/__tests__/CommentsSection.test.tsx +118 -0
  39. package/src/__tests__/FeaturedArticle.test.tsx +85 -0
  40. package/src/__tests__/LatestArticles.test.tsx +157 -0
  41. package/src/__tests__/LatestArticlesSection.test.tsx +105 -0
  42. package/src/__tests__/jest-dom.d.ts +1 -0
  43. package/src/__tests__/seoUtils.test.ts +217 -0
  44. package/src/__tests__/useArticles.test.ts +207 -0
  45. package/src/articleTypes.ts +21 -0
  46. package/src/articlesConfig.ts +98 -0
  47. package/src/commentTypes.ts +14 -0
  48. package/src/index.ts +35 -0
  49. package/src/markdown.ts +314 -0
  50. package/src/seoUtils.ts +155 -0
  51. package/src/server-articles.ts +265 -0
  52. package/src/server.ts +25 -0
  53. package/src/useArticles.ts +93 -0
@@ -0,0 +1,732 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __defProps = Object.defineProperties;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __spreadValues = (a, b) => {
14
+ for (var prop in b || (b = {}))
15
+ if (__hasOwnProp.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ if (__getOwnPropSymbols)
18
+ for (var prop of __getOwnPropSymbols(b)) {
19
+ if (__propIsEnum.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ }
22
+ return a;
23
+ };
24
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __export = (target, all) => {
26
+ for (var name in all)
27
+ __defProp(target, name, { get: all[name], enumerable: true });
28
+ };
29
+ var __copyProps = (to, from, except, desc) => {
30
+ if (from && typeof from === "object" || typeof from === "function") {
31
+ for (let key of __getOwnPropNames(from))
32
+ if (!__hasOwnProp.call(to, key) && key !== except)
33
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
34
+ }
35
+ return to;
36
+ };
37
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
38
+ // If the importer is in node compatibility mode or this is not an ESM
39
+ // file that has been converted to a CommonJS file using a Babel-
40
+ // compatible transform (i.e. "__esModule" has not been set), then set
41
+ // "default" to the CommonJS "module.exports" for node compatibility.
42
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
43
+ mod
44
+ ));
45
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
46
+ var __async = (__this, __arguments, generator) => {
47
+ return new Promise((resolve, reject) => {
48
+ var fulfilled = (value) => {
49
+ try {
50
+ step(generator.next(value));
51
+ } catch (e) {
52
+ reject(e);
53
+ }
54
+ };
55
+ var rejected = (value) => {
56
+ try {
57
+ step(generator.throw(value));
58
+ } catch (e) {
59
+ reject(e);
60
+ }
61
+ };
62
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
63
+ step((generator = generator.apply(__this, __arguments)).next());
64
+ });
65
+ };
66
+
67
+ // src/server.ts
68
+ var server_exports = {};
69
+ __export(server_exports, {
70
+ categoryToSlug: () => categoryToSlug,
71
+ generateArticleMetadata: () => generateArticleMetadata,
72
+ generateArticleStaticParams: () => generateArticleStaticParams,
73
+ generateCategoryMetadata: () => generateCategoryMetadata,
74
+ generateCategoryStaticParams: () => generateCategoryStaticParams,
75
+ getAdjacentArticles: () => getAdjacentArticles,
76
+ getAllArticles: () => getAllArticles,
77
+ getAllCategories: () => getAllCategories,
78
+ getArticleMetadata: () => getArticleMetadata,
79
+ getArticleSitemapEntries: () => getArticleSitemapEntries,
80
+ getArticlesByCategory: () => getArticlesByCategory,
81
+ getAvailableArticleSlugs: () => getAvailableArticleSlugs,
82
+ markdownToHtml: () => markdownToHtml,
83
+ sanitizeImagePath: () => sanitizeImagePath2,
84
+ searchArticles: () => searchArticles
85
+ });
86
+ module.exports = __toCommonJS(server_exports);
87
+
88
+ // src/server-articles.ts
89
+ var import_react = require("react");
90
+ var import_gray_matter = __toESM(require("gray-matter"), 1);
91
+ var import_node_fs = __toESM(require("fs"), 1);
92
+ var import_node_path = __toESM(require("path"), 1);
93
+ var import_reading_time = __toESM(require("reading-time"), 1);
94
+
95
+ // src/markdown.ts
96
+ var import_rehype_prism_plus = __toESM(require("rehype-prism-plus"), 1);
97
+ var import_rehype_sanitize = __toESM(require("rehype-sanitize"), 1);
98
+ var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
99
+ var import_remark = require("remark");
100
+ var import_remark_gfm = __toESM(require("remark-gfm"), 1);
101
+ var import_remark_parse = __toESM(require("remark-parse"), 1);
102
+ var import_remark_rehype = __toESM(require("remark-rehype"), 1);
103
+ var import_unist_util_visit = require("unist-util-visit");
104
+ function sanitizeImagePath(rawPath, articleSlug) {
105
+ if (!rawPath || typeof rawPath !== "string") {
106
+ return null;
107
+ }
108
+ const cleanPath = rawPath.replaceAll(/[\x00-\x1f\x7f-\x9f]/g, "");
109
+ if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) {
110
+ return cleanPath;
111
+ }
112
+ if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
113
+ console.warn(`Potentially unsafe image path detected: ${cleanPath}`);
114
+ return null;
115
+ }
116
+ if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {
117
+ console.warn(`Invalid characters in image path: ${cleanPath}`);
118
+ return null;
119
+ }
120
+ if (cleanPath.includes("/")) {
121
+ const normalizedPath = cleanPath.replaceAll("\\", "/");
122
+ if (normalizedPath.startsWith("..") || normalizedPath.includes("../")) {
123
+ console.warn(`Path traversal attempt detected: ${cleanPath}`);
124
+ return null;
125
+ }
126
+ return `/articles/${articleSlug}/${normalizedPath}`;
127
+ } else {
128
+ return `/articles/${articleSlug}/${cleanPath}`;
129
+ }
130
+ }
131
+ var customRenderer = () => {
132
+ return (tree) => {
133
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
134
+ if (node.tagName) {
135
+ const props = node.properties || {};
136
+ switch (node.tagName) {
137
+ case "h1":
138
+ props.className = "text-3xl font-bold text-foreground mt-8 mb-4 scroll-mt-20";
139
+ break;
140
+ case "h2":
141
+ props.className = "text-2xl font-semibold text-foreground mt-6 mb-3 scroll-mt-20";
142
+ break;
143
+ case "h3":
144
+ props.className = "text-xl font-semibold text-foreground mt-4 mb-2 scroll-mt-20";
145
+ break;
146
+ case "h4":
147
+ props.className = "text-lg font-semibold text-foreground mt-3 mb-2 scroll-mt-20";
148
+ break;
149
+ case "p":
150
+ props.className = "text-muted-foreground leading-relaxed mb-4";
151
+ break;
152
+ case "a":
153
+ props.className = "text-primary hover:underline transition-colors duration-200";
154
+ props.target = "_blank";
155
+ props.rel = "noopener noreferrer";
156
+ break;
157
+ case "ul":
158
+ props.className = "list-disc list-inside mb-4 space-y-2 ml-4";
159
+ break;
160
+ case "ol":
161
+ props.className = "list-decimal list-inside mb-4 space-y-2 ml-4";
162
+ break;
163
+ case "li":
164
+ props.className = "mb-1";
165
+ break;
166
+ case "blockquote":
167
+ props.className = "border-l-4 border-primary pl-4 italic my-4 text-muted-foreground";
168
+ break;
169
+ case "code":
170
+ props.className = (props.className ? props.className + " " : "") + "bg-muted px-1 py-0.5 rounded text-sm font-mono";
171
+ break;
172
+ case "pre":
173
+ props.className = "bg-muted rounded-lg p-4 overflow-x-auto my-4";
174
+ break;
175
+ case "img":
176
+ props.className = "rounded-lg my-6 w-full max-w-2xl mx-auto";
177
+ break;
178
+ case "table":
179
+ props.className = "border-collapse border border-border my-4 w-full";
180
+ break;
181
+ case "th":
182
+ props.className = "border border-border px-2 py-1 bg-muted font-semibold";
183
+ break;
184
+ case "td":
185
+ props.className = "border border-border px-2 py-1";
186
+ break;
187
+ case "hr":
188
+ props.className = "my-8 border-border";
189
+ break;
190
+ default:
191
+ break;
192
+ }
193
+ node.properties = props;
194
+ }
195
+ });
196
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
197
+ var _a, _b, _c, _d, _e, _f, _g, _h;
198
+ if (node.tagName === "sup" && ((_b = (_a = node.children) == null ? void 0 : _a[0]) == null ? void 0 : _b.type) === "element" && node.children[0].tagName === "a") {
199
+ const link = node.children[0];
200
+ if (((_c = link.properties) == null ? void 0 : _c.href) && typeof link.properties.href === "string" && link.properties.href.startsWith("#user-content-fn-")) {
201
+ if (link.properties) {
202
+ delete link.properties.target;
203
+ delete link.properties.rel;
204
+ link.properties.className = "text-primary hover:underline";
205
+ }
206
+ link.properties.href = link.properties.href.replaceAll("#user-content-fn-", "#footnote-");
207
+ if (((_d = node.properties) == null ? void 0 : _d.id) && typeof node.properties.id === "string") {
208
+ const newId = node.properties.id.replaceAll("user-content-fnref-", "ref-");
209
+ link.properties.id = newId;
210
+ delete node.properties.id;
211
+ }
212
+ if (((_f = (_e = link.children) == null ? void 0 : _e[0]) == null ? void 0 : _f.type) === "text") {
213
+ link.children[0].value = `[${link.children[0].value}]`;
214
+ }
215
+ }
216
+ }
217
+ if (node.tagName === "section" && (Array.isArray((_g = node.properties) == null ? void 0 : _g.className) ? node.properties.className.includes("footnotes") : ((_h = node.properties) == null ? void 0 : _h.className) === "footnotes")) {
218
+ const olCandidate = node.children.find(
219
+ (child) => child.type === "element" && child.tagName === "ol"
220
+ );
221
+ if ((olCandidate == null ? void 0 : olCandidate.type) === "element") {
222
+ const ol = olCandidate;
223
+ ol.properties.className = "list-decimal ml-6 space-y-2 text-sm text-muted-foreground";
224
+ ol.children.forEach((li) => {
225
+ if (li.type === "element" && li.tagName === "li") {
226
+ const liEl = li;
227
+ if (liEl.properties) {
228
+ liEl.properties.className = "pl-2";
229
+ if (typeof liEl.properties.id === "string") {
230
+ liEl.properties.id = liEl.properties.id.replaceAll(
231
+ "user-content-fn-",
232
+ "footnote-"
233
+ );
234
+ }
235
+ }
236
+ const pIndex = liEl.children.findIndex(
237
+ (child) => child.type === "element" && child.tagName === "p"
238
+ );
239
+ if (pIndex !== -1) {
240
+ const p = liEl.children[pIndex];
241
+ liEl.children.splice(pIndex, 1, ...p.children);
242
+ }
243
+ const styleLinks = (nodes) => {
244
+ nodes.forEach((n) => {
245
+ var _a2, _b2;
246
+ if (n.type !== "element")
247
+ return;
248
+ const el = n;
249
+ if (el.tagName === "a") {
250
+ const isBackRef = ((_a2 = el.properties) == null ? void 0 : _a2["dataFootnoteBackref"]) !== void 0 || ((_b2 = el.children[0]) == null ? void 0 : _b2.type) === "text" && el.children[0].value === "\u21A9";
251
+ if (isBackRef) {
252
+ el.properties.className = "text-primary hover:underline ml-1";
253
+ if (typeof el.properties.href === "string") {
254
+ el.properties.href = el.properties.href.replaceAll(
255
+ "#user-content-fnref-",
256
+ "#ref-"
257
+ );
258
+ }
259
+ } else {
260
+ el.properties.className = "text-primary hover:underline break-all";
261
+ }
262
+ }
263
+ if (el.children)
264
+ styleLinks(el.children);
265
+ });
266
+ };
267
+ styleLinks(liEl.children);
268
+ }
269
+ });
270
+ const hr = {
271
+ type: "element",
272
+ tagName: "hr",
273
+ properties: { className: "my-8 border-border" },
274
+ children: []
275
+ };
276
+ const h3 = {
277
+ type: "element",
278
+ tagName: "h3",
279
+ properties: { className: "text-lg font-semibold mb-4" },
280
+ children: [{ type: "text", value: "References" }]
281
+ };
282
+ node.children = [hr, h3, ol];
283
+ if (node.properties) {
284
+ node.properties.className = void 0;
285
+ }
286
+ }
287
+ }
288
+ });
289
+ };
290
+ };
291
+ var rehypeProcessImages = (options = {}) => {
292
+ return (tree) => {
293
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
294
+ if (node.tagName === "img" && node.properties) {
295
+ const src = node.properties.src;
296
+ if (src && typeof src === "string" && options.articleSlug) {
297
+ const sanitizedSrc = sanitizeImagePath(src, options.articleSlug);
298
+ if (sanitizedSrc) {
299
+ node.properties.src = sanitizedSrc;
300
+ } else {
301
+ console.warn(`Using placeholder for unsafe image path: ${src}`);
302
+ node.properties.src = "/placeholder-logo.png";
303
+ }
304
+ }
305
+ }
306
+ });
307
+ };
308
+ };
309
+ function markdownToHtml(markdown, articleSlug) {
310
+ return __async(this, null, function* () {
311
+ try {
312
+ let processor = (0, import_remark.remark)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(customRenderer).use(import_rehype_prism_plus.default).use(import_rehype_sanitize.default, {
313
+ attributes: {
314
+ "*": ["className", "class", "id"],
315
+ a: ["href", "target", "rel", "id"],
316
+ img: ["src", "alt"]
317
+ }
318
+ });
319
+ if (articleSlug) {
320
+ processor = processor.use(rehypeProcessImages, { articleSlug });
321
+ }
322
+ const result = yield processor.use(import_rehype_stringify.default).process(markdown);
323
+ return result.toString();
324
+ } catch (error) {
325
+ console.error("Markdown conversion error:", error);
326
+ return markdown;
327
+ }
328
+ });
329
+ }
330
+
331
+ // src/server-articles.ts
332
+ var articlesDirectory = import_node_path.default.join(process.cwd(), "public/articles");
333
+ function getReadingTime(content) {
334
+ return (0, import_reading_time.default)(content).text;
335
+ }
336
+ function findArticleImage(slug) {
337
+ try {
338
+ const articleDir = import_node_path.default.join(articlesDirectory, slug);
339
+ const entries = import_node_fs.default.readdirSync(articleDir, { withFileTypes: true });
340
+ const names = entries.map((entry) => {
341
+ if (typeof entry === "string")
342
+ return entry;
343
+ if (entry && typeof entry.name === "string")
344
+ return entry.name;
345
+ return null;
346
+ }).filter((name) => Boolean(name));
347
+ const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
348
+ const imageFileName = names.find(
349
+ (name) => imageExtensions.some((ext) => name.toLowerCase().endsWith(ext))
350
+ );
351
+ return imageFileName ? sanitizeImagePath2(imageFileName, slug) : null;
352
+ } catch (e) {
353
+ return null;
354
+ }
355
+ }
356
+ function sanitizeImagePath2(rawPath, articleSlug) {
357
+ if (!rawPath || typeof rawPath !== "string")
358
+ return null;
359
+ const cleanPath = rawPath.replaceAll(/[\x00-\x1f\x7f-\x9f]/g, "");
360
+ if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://"))
361
+ return cleanPath;
362
+ if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
363
+ console.warn(`Potentially unsafe image path detected: ${cleanPath}`);
364
+ return null;
365
+ }
366
+ if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {
367
+ console.warn(`Invalid characters in image path: ${cleanPath}`);
368
+ return null;
369
+ }
370
+ if (cleanPath.includes("/")) {
371
+ const normalizedPath = import_node_path.default.normalize(cleanPath);
372
+ if (normalizedPath.startsWith("..") || normalizedPath.includes("../")) {
373
+ console.warn(`Path traversal attempt detected: ${cleanPath}`);
374
+ return null;
375
+ }
376
+ return `/articles/${articleSlug}/${cleanPath}`;
377
+ }
378
+ return `/articles/${articleSlug}/${cleanPath}`;
379
+ }
380
+ function getAvailableArticleSlugs() {
381
+ try {
382
+ if (!import_node_fs.default.existsSync(articlesDirectory))
383
+ return [];
384
+ const items = import_node_fs.default.readdirSync(articlesDirectory, { withFileTypes: true });
385
+ return items.filter((item) => item.isDirectory()).filter((dir) => {
386
+ const articleDir = import_node_path.default.join(articlesDirectory, dir.name);
387
+ try {
388
+ const entries = import_node_fs.default.readdirSync(articleDir, {
389
+ withFileTypes: true
390
+ });
391
+ return entries.some((entry) => {
392
+ const name = typeof entry === "string" ? entry : entry == null ? void 0 : entry.name;
393
+ if (name !== "article.md")
394
+ return false;
395
+ if (typeof entry === "string")
396
+ return true;
397
+ if (typeof (entry == null ? void 0 : entry.isFile) === "function")
398
+ return entry.isFile();
399
+ return true;
400
+ });
401
+ } catch (e) {
402
+ return false;
403
+ }
404
+ }).map((dir) => dir.name);
405
+ } catch (error) {
406
+ console.error("Error reading articles directory:", error);
407
+ return [];
408
+ }
409
+ }
410
+ function getArticleSummary(slug) {
411
+ return __async(this, null, function* () {
412
+ try {
413
+ const articlePath = import_node_path.default.join(articlesDirectory, slug, "article.md");
414
+ if (!import_node_fs.default.existsSync(articlePath))
415
+ return null;
416
+ const fileContent = import_node_fs.default.readFileSync(articlePath, "utf8");
417
+ const { data, content: markdownContent } = (0, import_gray_matter.default)(fileContent);
418
+ const readTime = getReadingTime(markdownContent);
419
+ let featuredImage = data.featuredImage || "";
420
+ if (featuredImage && !featuredImage.startsWith("http")) {
421
+ const sanitizedPath = sanitizeImagePath2(featuredImage, slug);
422
+ featuredImage = sanitizedPath || "/placeholder-logo.png";
423
+ } else if (!featuredImage) {
424
+ featuredImage = findArticleImage(slug) || "/placeholder-logo.png";
425
+ }
426
+ let articleDate;
427
+ if (data.date) {
428
+ try {
429
+ const parsedDate = new Date(data.date);
430
+ if (!Number.isNaN(parsedDate.getTime())) {
431
+ articleDate = parsedDate.toISOString().split("T")[0];
432
+ }
433
+ } catch (e) {
434
+ console.warn(`Invalid date for article ${slug}: ${data.date}`);
435
+ }
436
+ }
437
+ const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
438
+ const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
439
+ return {
440
+ slug,
441
+ title: data.title || slug.replaceAll("-", " "),
442
+ excerpt: data.excerpt || "",
443
+ date: articleDate,
444
+ author: data.author || "Andrew Blase",
445
+ category: categories[0],
446
+ categories,
447
+ readTime,
448
+ featuredImage,
449
+ tags: data.tags || []
450
+ };
451
+ } catch (error) {
452
+ console.error(`Error loading article ${slug}:`, error);
453
+ return null;
454
+ }
455
+ });
456
+ }
457
+ var getArticleMetadata = (0, import_react.cache)((slug) => __async(void 0, null, function* () {
458
+ try {
459
+ const articlePath = import_node_path.default.join(articlesDirectory, slug, "article.md");
460
+ if (!import_node_fs.default.existsSync(articlePath))
461
+ return null;
462
+ const fileContent = import_node_fs.default.readFileSync(articlePath, "utf8");
463
+ const { data, content: markdownContent } = (0, import_gray_matter.default)(fileContent);
464
+ const htmlContent = yield markdownToHtml(markdownContent, slug);
465
+ const readTime = getReadingTime(markdownContent);
466
+ let featuredImage = data.featuredImage || "";
467
+ if (featuredImage && !featuredImage.startsWith("http")) {
468
+ const sanitizedPath = sanitizeImagePath2(featuredImage, slug);
469
+ featuredImage = sanitizedPath || "/placeholder-logo.png";
470
+ } else if (!featuredImage) {
471
+ featuredImage = findArticleImage(slug) || "/placeholder-logo.png";
472
+ }
473
+ let articleDate;
474
+ if (data.date) {
475
+ try {
476
+ const parsedDate = new Date(data.date);
477
+ if (!Number.isNaN(parsedDate.getTime())) {
478
+ articleDate = parsedDate.toISOString().split("T")[0];
479
+ }
480
+ } catch (e) {
481
+ console.warn(`Invalid date for article ${slug}: ${data.date}`);
482
+ }
483
+ }
484
+ const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
485
+ const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
486
+ return {
487
+ slug,
488
+ title: data.title || slug.replaceAll("-", " "),
489
+ excerpt: data.excerpt || "",
490
+ date: articleDate,
491
+ author: data.author || "Andrew Blase",
492
+ category: categories[0],
493
+ categories,
494
+ readTime,
495
+ featuredImage,
496
+ tags: data.tags || [],
497
+ content: markdownContent,
498
+ htmlContent
499
+ };
500
+ } catch (error) {
501
+ console.error(`Error loading article ${slug}:`, error);
502
+ return null;
503
+ }
504
+ }));
505
+ var getAllArticles = (0, import_react.cache)(() => __async(void 0, null, function* () {
506
+ const slugs = getAvailableArticleSlugs();
507
+ const articles = yield Promise.all(slugs.map((slug) => getArticleSummary(slug)));
508
+ const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
509
+ return articles.filter((article) => article !== null).filter((article) => !article.date || article.date <= currentDate).sort((a, b) => {
510
+ if (!a.date && !b.date)
511
+ return 0;
512
+ if (!a.date)
513
+ return 1;
514
+ if (!b.date)
515
+ return -1;
516
+ return new Date(b.date).getTime() - new Date(a.date).getTime();
517
+ });
518
+ }));
519
+ function getAdjacentArticles(currentSlug) {
520
+ return __async(this, null, function* () {
521
+ const allArticles = yield getAllArticles();
522
+ const currentIndex = allArticles.findIndex((article) => article.slug === currentSlug);
523
+ if (currentIndex === -1)
524
+ return { previous: null, next: null };
525
+ const previous = currentIndex < allArticles.length - 1 ? allArticles[currentIndex + 1] : null;
526
+ const next = currentIndex > 0 ? allArticles[currentIndex - 1] : null;
527
+ return { previous, next };
528
+ });
529
+ }
530
+ function searchArticles(query) {
531
+ return __async(this, null, function* () {
532
+ if (!(query == null ? void 0 : query.trim()))
533
+ return getAllArticles();
534
+ const articles = yield getAllArticles();
535
+ const searchTerm = query.toLowerCase().trim();
536
+ return articles.filter((article) => {
537
+ var _a;
538
+ const matchesTitle = article.title.toLowerCase().includes(searchTerm);
539
+ const matchesExcerpt = article.excerpt.toLowerCase().includes(searchTerm);
540
+ const matchesAuthor = article.author.toLowerCase().includes(searchTerm);
541
+ const matchesCategory = article.categories.some((cat) => cat.toLowerCase().includes(searchTerm));
542
+ const matchesTags = (_a = article.tags) == null ? void 0 : _a.some((tag) => tag.toLowerCase().includes(searchTerm));
543
+ return matchesTitle || matchesExcerpt || matchesAuthor || matchesCategory || Boolean(matchesTags);
544
+ });
545
+ });
546
+ }
547
+ function categoryToSlug(category) {
548
+ return category.toLowerCase().replaceAll(/\s+/g, "-").replaceAll(/[^a-z0-9-]/g, "");
549
+ }
550
+ function getAllCategories() {
551
+ return __async(this, null, function* () {
552
+ const articles = yield getAllArticles();
553
+ const categoryMap = /* @__PURE__ */ new Map();
554
+ for (const article of articles) {
555
+ for (const cat of article.categories) {
556
+ if (!categoryMap.has(cat)) {
557
+ categoryMap.set(cat, { count: 0, featuredImage: article.featuredImage });
558
+ }
559
+ categoryMap.get(cat).count++;
560
+ }
561
+ }
562
+ return Array.from(categoryMap.entries()).map(([name, { count, featuredImage }]) => ({
563
+ name,
564
+ slug: categoryToSlug(name),
565
+ count,
566
+ featuredImage
567
+ })).sort((a, b) => b.count - a.count);
568
+ });
569
+ }
570
+ function getArticlesByCategory(categorySlug) {
571
+ return __async(this, null, function* () {
572
+ const articles = yield getAllArticles();
573
+ return articles.filter(
574
+ (article) => article.categories.some((cat) => categoryToSlug(cat) === categorySlug)
575
+ );
576
+ });
577
+ }
578
+
579
+ // src/seoUtils.ts
580
+ function generateArticleStaticParams() {
581
+ return getAvailableArticleSlugs().map((slug) => ({ slug }));
582
+ }
583
+ function generateCategoryStaticParams() {
584
+ return __async(this, null, function* () {
585
+ const categories = yield getAllCategories();
586
+ return categories.map((cat) => ({ category: cat.slug }));
587
+ });
588
+ }
589
+ function resolveImageUrl(featuredImage, siteUrl) {
590
+ const base = siteUrl.replace(/\/$/, "");
591
+ if (featuredImage.startsWith("http://") || featuredImage.startsWith("https://")) {
592
+ return featuredImage;
593
+ }
594
+ return `${base}/${featuredImage.replace(/^\/+/, "")}`;
595
+ }
596
+ function generateArticleMetadata(slug, config) {
597
+ return __async(this, null, function* () {
598
+ var _a, _b, _c, _d, _e, _f;
599
+ const article = yield getArticleMetadata(slug);
600
+ if (!article) {
601
+ return {
602
+ title: "Article Not Found",
603
+ description: "The requested article could not be found."
604
+ };
605
+ }
606
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
607
+ const articleUrl = `${siteUrl}/articles/${slug}`;
608
+ const imageUrl = article.featuredImage ? resolveImageUrl(article.featuredImage, siteUrl) : `${siteUrl}/placeholder-logo.png`;
609
+ const description = (_a = article.excerpt) != null ? _a : `Read ${article.title} on ${config.siteName}.`;
610
+ return {
611
+ title: `${article.title} | ${config.siteName}`,
612
+ description,
613
+ keywords: [
614
+ "volunteer management software",
615
+ "political campaign software",
616
+ "campaign operations",
617
+ "civic tech",
618
+ ...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())
619
+ ].join(", "),
620
+ openGraph: __spreadProps(__spreadValues({
621
+ title: article.title,
622
+ description,
623
+ url: articleUrl,
624
+ siteName: config.siteName,
625
+ images: [{ url: imageUrl, width: 1200, height: 630, alt: article.title }],
626
+ locale: "en_US",
627
+ type: "article"
628
+ }, article.date && { publishedTime: article.date }), {
629
+ authors: [article.author],
630
+ tags: (_c = article.tags) != null ? _c : []
631
+ }),
632
+ twitter: {
633
+ card: "summary_large_image",
634
+ title: article.title,
635
+ description,
636
+ images: [imageUrl]
637
+ },
638
+ alternates: {
639
+ canonical: articleUrl
640
+ },
641
+ robots: {
642
+ index: true,
643
+ follow: true,
644
+ googleBot: {
645
+ index: true,
646
+ follow: true,
647
+ "max-video-preview": -1,
648
+ "max-image-preview": "large",
649
+ "max-snippet": -1
650
+ }
651
+ },
652
+ other: __spreadProps(__spreadValues({
653
+ "article:author": article.author
654
+ }, article.date && {
655
+ "article:published_time": new Date(article.date).toISOString()
656
+ }), {
657
+ "article:section": article.category,
658
+ "article:tag": (_e = (_d = article.tags) == null ? void 0 : _d.join(",")) != null ? _e : "",
659
+ "linkedin:owner": (_f = process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID) != null ? _f : ""
660
+ })
661
+ };
662
+ });
663
+ }
664
+ function generateCategoryMetadata(categorySlug, config) {
665
+ return __async(this, null, function* () {
666
+ var _a, _b;
667
+ const articles = yield getArticlesByCategory(categorySlug);
668
+ if (articles.length === 0)
669
+ return { title: "Category Not Found" };
670
+ const categoryName = articles[0].category;
671
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
672
+ const categoryUrl = `${siteUrl}/articles/category/${categorySlug}`;
673
+ const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
674
+ const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
675
+ const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
676
+ return {
677
+ title: `${categoryName} Articles | ${config.siteName}`,
678
+ description,
679
+ openGraph: {
680
+ title: `${categoryName} Articles`,
681
+ description,
682
+ url: categoryUrl,
683
+ siteName: config.siteName,
684
+ images: [{ url: articles[0].featuredImage }]
685
+ },
686
+ alternates: {
687
+ canonical: categoryUrl
688
+ }
689
+ };
690
+ });
691
+ }
692
+ function getArticleSitemapEntries(baseUrl) {
693
+ return __async(this, null, function* () {
694
+ try {
695
+ const [articles, categories] = yield Promise.all([getAllArticles(), getAllCategories()]);
696
+ const articleEntries = articles.map((article) => ({
697
+ url: `${baseUrl}/articles/${article.slug}`,
698
+ lastModified: article.date ? new Date(article.date) : void 0,
699
+ changeFrequency: "weekly",
700
+ priority: 0.8
701
+ }));
702
+ const categoryEntries = categories.map((cat) => ({
703
+ url: `${baseUrl}/articles/category/${cat.slug}`,
704
+ lastModified: /* @__PURE__ */ new Date(),
705
+ changeFrequency: "weekly",
706
+ priority: 0.7
707
+ }));
708
+ return [...articleEntries, ...categoryEntries];
709
+ } catch (e) {
710
+ return [];
711
+ }
712
+ });
713
+ }
714
+ // Annotate the CommonJS export names for ESM import in node:
715
+ 0 && (module.exports = {
716
+ categoryToSlug,
717
+ generateArticleMetadata,
718
+ generateArticleStaticParams,
719
+ generateCategoryMetadata,
720
+ generateCategoryStaticParams,
721
+ getAdjacentArticles,
722
+ getAllArticles,
723
+ getAllCategories,
724
+ getArticleMetadata,
725
+ getArticleSitemapEntries,
726
+ getArticlesByCategory,
727
+ getAvailableArticleSlugs,
728
+ markdownToHtml,
729
+ sanitizeImagePath,
730
+ searchArticles
731
+ });
732
+ //# sourceMappingURL=server.cjs.map