@fullstackdatasolutions/articles 0.4.3 → 0.5.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.
- package/README.md +119 -2
- package/dist/index.cjs +182 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +241 -110
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +221 -188
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +36 -1
- package/dist/server.d.ts +36 -1
- package/dist/server.js +219 -188
- package/dist/server.js.map +1 -1
- package/package.json +5 -1
- package/src/ArticleContent.tsx +17 -0
- package/src/ArticleSchemas.tsx +97 -1
- package/src/ArticleSocialShare.tsx +1 -1
- package/src/ArticleTOC.tsx +29 -0
- package/src/ScrollToTop.tsx +25 -0
- package/src/__tests__/ArticleContent.test.tsx +91 -0
- package/src/__tests__/ArticleSchemas.test.tsx +212 -1
- package/src/__tests__/ArticleSocialShare.test.tsx +7 -7
- package/src/__tests__/ArticleTOC.test.tsx +75 -0
- package/src/__tests__/ScrollToTop.test.tsx +87 -0
- package/src/__tests__/seoUtils.test.ts +256 -0
- package/src/__tests__/server-articles.test.ts +98 -0
- package/src/articleTypes.ts +26 -0
- package/src/articlesConfig.ts +4 -0
- package/src/index.ts +10 -2
- package/src/markdown.ts +143 -130
- package/src/seoUtils.ts +20 -14
- package/src/server-articles.ts +81 -78
- package/src/server.ts +3 -2
package/dist/server.js
CHANGED
|
@@ -46,11 +46,14 @@ import path from "node:path";
|
|
|
46
46
|
import readingTime from "reading-time";
|
|
47
47
|
|
|
48
48
|
// src/markdown.ts
|
|
49
|
+
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
49
50
|
import rehypePrism from "rehype-prism-plus";
|
|
50
51
|
import rehypeSanitize from "rehype-sanitize";
|
|
52
|
+
import rehypeSlug from "rehype-slug";
|
|
51
53
|
import rehypeStringify from "rehype-stringify";
|
|
52
54
|
import { remark } from "remark";
|
|
53
55
|
import remarkGfm from "remark-gfm";
|
|
56
|
+
import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
|
|
54
57
|
import remarkParse from "remark-parse";
|
|
55
58
|
import remarkRehype from "remark-rehype";
|
|
56
59
|
import { visit } from "unist-util-visit";
|
|
@@ -81,6 +84,88 @@ function sanitizeImagePath(rawPath, articleSlug) {
|
|
|
81
84
|
return `/articles/${articleSlug}/${cleanPath}`;
|
|
82
85
|
}
|
|
83
86
|
}
|
|
87
|
+
function styleFootnoteLinks(nodes) {
|
|
88
|
+
nodes.forEach((n) => {
|
|
89
|
+
var _a, _b;
|
|
90
|
+
if (n.type !== "element") return;
|
|
91
|
+
const el = n;
|
|
92
|
+
if (el.tagName === "a") {
|
|
93
|
+
const isBackRef = ((_a = el.properties) == null ? void 0 : _a["dataFootnoteBackref"]) !== void 0 || ((_b = el.children[0]) == null ? void 0 : _b.type) === "text" && el.children[0].value === "\u21A9";
|
|
94
|
+
if (isBackRef) {
|
|
95
|
+
el.properties.className = "text-primary hover:underline ml-1";
|
|
96
|
+
if (typeof el.properties.href === "string") {
|
|
97
|
+
el.properties.href = el.properties.href.replaceAll("#user-content-fnref-", "#ref-");
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
el.properties.className = "text-primary hover:underline break-all";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (el.children) styleFootnoteLinks(el.children);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function processFootnoteRef(node) {
|
|
107
|
+
var _a, _b, _c, _d, _e, _f;
|
|
108
|
+
if (node.tagName !== "sup" || ((_b = (_a = node.children) == null ? void 0 : _a[0]) == null ? void 0 : _b.type) !== "element" || node.children[0].tagName !== "a")
|
|
109
|
+
return;
|
|
110
|
+
const link = node.children[0];
|
|
111
|
+
const href = (_c = link.properties) == null ? void 0 : _c.href;
|
|
112
|
+
if (typeof href !== "string" || !href.startsWith("#user-content-fn-")) return;
|
|
113
|
+
delete link.properties.target;
|
|
114
|
+
delete link.properties.rel;
|
|
115
|
+
link.properties.className = "text-primary hover:underline";
|
|
116
|
+
link.properties.href = href.replaceAll("#user-content-fn-", "#footnote-");
|
|
117
|
+
if (typeof ((_d = node.properties) == null ? void 0 : _d.id) === "string") {
|
|
118
|
+
link.properties.id = node.properties.id.replaceAll("user-content-fnref-", "ref-");
|
|
119
|
+
delete node.properties.id;
|
|
120
|
+
}
|
|
121
|
+
if (((_f = (_e = link.children) == null ? void 0 : _e[0]) == null ? void 0 : _f.type) === "text") {
|
|
122
|
+
link.children[0].value = `[${link.children[0].value}]`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function processFootnotesSection(node) {
|
|
126
|
+
var _a;
|
|
127
|
+
const cls = (_a = node.properties) == null ? void 0 : _a.className;
|
|
128
|
+
const isFootnotes = node.tagName === "section" && (Array.isArray(cls) ? cls.includes("footnotes") : cls === "footnotes");
|
|
129
|
+
if (!isFootnotes) return;
|
|
130
|
+
const olCandidate = node.children.find(
|
|
131
|
+
(child) => child.type === "element" && child.tagName === "ol"
|
|
132
|
+
);
|
|
133
|
+
if ((olCandidate == null ? void 0 : olCandidate.type) !== "element") return;
|
|
134
|
+
const ol = olCandidate;
|
|
135
|
+
ol.properties.className = "list-decimal ml-6 space-y-2 text-sm text-muted-foreground";
|
|
136
|
+
ol.children.forEach((li) => {
|
|
137
|
+
if (li.type !== "element" || li.tagName !== "li") return;
|
|
138
|
+
const liEl = li;
|
|
139
|
+
if (liEl.properties) {
|
|
140
|
+
liEl.properties.className = "pl-2";
|
|
141
|
+
if (typeof liEl.properties.id === "string") {
|
|
142
|
+
liEl.properties.id = liEl.properties.id.replaceAll("user-content-fn-", "footnote-");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const pIndex = liEl.children.findIndex(
|
|
146
|
+
(child) => child.type === "element" && child.tagName === "p"
|
|
147
|
+
);
|
|
148
|
+
if (pIndex !== -1) {
|
|
149
|
+
const p = liEl.children[pIndex];
|
|
150
|
+
liEl.children.splice(pIndex, 1, ...p.children);
|
|
151
|
+
}
|
|
152
|
+
styleFootnoteLinks(liEl.children);
|
|
153
|
+
});
|
|
154
|
+
const hr = {
|
|
155
|
+
type: "element",
|
|
156
|
+
tagName: "hr",
|
|
157
|
+
properties: { className: "my-8 border-border" },
|
|
158
|
+
children: []
|
|
159
|
+
};
|
|
160
|
+
const h3 = {
|
|
161
|
+
type: "element",
|
|
162
|
+
tagName: "h3",
|
|
163
|
+
properties: { className: "text-lg font-semibold mb-4" },
|
|
164
|
+
children: [{ type: "text", value: "References" }]
|
|
165
|
+
};
|
|
166
|
+
node.children = [hr, h3, ol];
|
|
167
|
+
if (node.properties) node.properties.className = void 0;
|
|
168
|
+
}
|
|
84
169
|
var customRenderer = () => {
|
|
85
170
|
return (tree) => {
|
|
86
171
|
visit(tree, "element", (node) => {
|
|
@@ -147,95 +232,8 @@ var customRenderer = () => {
|
|
|
147
232
|
}
|
|
148
233
|
});
|
|
149
234
|
visit(tree, "element", (node) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const link = node.children[0];
|
|
153
|
-
if (((_c = link.properties) == null ? void 0 : _c.href) && typeof link.properties.href === "string" && link.properties.href.startsWith("#user-content-fn-")) {
|
|
154
|
-
if (link.properties) {
|
|
155
|
-
delete link.properties.target;
|
|
156
|
-
delete link.properties.rel;
|
|
157
|
-
link.properties.className = "text-primary hover:underline";
|
|
158
|
-
}
|
|
159
|
-
link.properties.href = link.properties.href.replaceAll("#user-content-fn-", "#footnote-");
|
|
160
|
-
if (((_d = node.properties) == null ? void 0 : _d.id) && typeof node.properties.id === "string") {
|
|
161
|
-
const newId = node.properties.id.replaceAll("user-content-fnref-", "ref-");
|
|
162
|
-
link.properties.id = newId;
|
|
163
|
-
delete node.properties.id;
|
|
164
|
-
}
|
|
165
|
-
if (((_f = (_e = link.children) == null ? void 0 : _e[0]) == null ? void 0 : _f.type) === "text") {
|
|
166
|
-
link.children[0].value = `[${link.children[0].value}]`;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
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")) {
|
|
171
|
-
const olCandidate = node.children.find(
|
|
172
|
-
(child) => child.type === "element" && child.tagName === "ol"
|
|
173
|
-
);
|
|
174
|
-
if ((olCandidate == null ? void 0 : olCandidate.type) === "element") {
|
|
175
|
-
const ol = olCandidate;
|
|
176
|
-
ol.properties.className = "list-decimal ml-6 space-y-2 text-sm text-muted-foreground";
|
|
177
|
-
ol.children.forEach((li) => {
|
|
178
|
-
if (li.type === "element" && li.tagName === "li") {
|
|
179
|
-
const liEl = li;
|
|
180
|
-
if (liEl.properties) {
|
|
181
|
-
liEl.properties.className = "pl-2";
|
|
182
|
-
if (typeof liEl.properties.id === "string") {
|
|
183
|
-
liEl.properties.id = liEl.properties.id.replaceAll(
|
|
184
|
-
"user-content-fn-",
|
|
185
|
-
"footnote-"
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
const pIndex = liEl.children.findIndex(
|
|
190
|
-
(child) => child.type === "element" && child.tagName === "p"
|
|
191
|
-
);
|
|
192
|
-
if (pIndex !== -1) {
|
|
193
|
-
const p = liEl.children[pIndex];
|
|
194
|
-
liEl.children.splice(pIndex, 1, ...p.children);
|
|
195
|
-
}
|
|
196
|
-
const styleLinks = (nodes) => {
|
|
197
|
-
nodes.forEach((n) => {
|
|
198
|
-
var _a2, _b2;
|
|
199
|
-
if (n.type !== "element") return;
|
|
200
|
-
const el = n;
|
|
201
|
-
if (el.tagName === "a") {
|
|
202
|
-
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";
|
|
203
|
-
if (isBackRef) {
|
|
204
|
-
el.properties.className = "text-primary hover:underline ml-1";
|
|
205
|
-
if (typeof el.properties.href === "string") {
|
|
206
|
-
el.properties.href = el.properties.href.replaceAll(
|
|
207
|
-
"#user-content-fnref-",
|
|
208
|
-
"#ref-"
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
} else {
|
|
212
|
-
el.properties.className = "text-primary hover:underline break-all";
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (el.children) styleLinks(el.children);
|
|
216
|
-
});
|
|
217
|
-
};
|
|
218
|
-
styleLinks(liEl.children);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
const hr = {
|
|
222
|
-
type: "element",
|
|
223
|
-
tagName: "hr",
|
|
224
|
-
properties: { className: "my-8 border-border" },
|
|
225
|
-
children: []
|
|
226
|
-
};
|
|
227
|
-
const h3 = {
|
|
228
|
-
type: "element",
|
|
229
|
-
tagName: "h3",
|
|
230
|
-
properties: { className: "text-lg font-semibold mb-4" },
|
|
231
|
-
children: [{ type: "text", value: "References" }]
|
|
232
|
-
};
|
|
233
|
-
node.children = [hr, h3, ol];
|
|
234
|
-
if (node.properties) {
|
|
235
|
-
node.properties.className = void 0;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
235
|
+
processFootnoteRef(node);
|
|
236
|
+
processFootnotesSection(node);
|
|
239
237
|
});
|
|
240
238
|
};
|
|
241
239
|
};
|
|
@@ -260,7 +258,7 @@ var rehypeProcessImages = (options = {}) => {
|
|
|
260
258
|
function markdownToHtml(markdown, articleSlug) {
|
|
261
259
|
return __async(this, null, function* () {
|
|
262
260
|
try {
|
|
263
|
-
let processor = remark().use(remarkParse).use(remarkGfm).use(remarkRehype).use(customRenderer).use(rehypePrism).use(rehypeSanitize, {
|
|
261
|
+
let processor = remark().use(remarkParse).use(remarkGfm).use(remarkGithubBlockquoteAlert).use(remarkRehype).use(customRenderer).use(rehypeSlug).use(rehypeAutolinkHeadings, { behavior: "wrap" }).use(rehypePrism).use(rehypeSanitize, {
|
|
264
262
|
attributes: {
|
|
265
263
|
"*": ["className", "class", "id"],
|
|
266
264
|
a: ["href", "target", "rel", "id"],
|
|
@@ -278,6 +276,31 @@ function markdownToHtml(markdown, articleSlug) {
|
|
|
278
276
|
}
|
|
279
277
|
});
|
|
280
278
|
}
|
|
279
|
+
function nodeTextValue(c) {
|
|
280
|
+
return c.type === "text" ? c.value : "";
|
|
281
|
+
}
|
|
282
|
+
function extractHeadingItem(node) {
|
|
283
|
+
var _a;
|
|
284
|
+
const match = /^h([1-6])$/.exec(node.tagName);
|
|
285
|
+
if (!match) return null;
|
|
286
|
+
const id = typeof ((_a = node.properties) == null ? void 0 : _a.id) === "string" ? node.properties.id : "";
|
|
287
|
+
const text = node.children.map(nodeTextValue).join("");
|
|
288
|
+
if (!id || !text) return null;
|
|
289
|
+
return { id, depth: Number.parseInt(match[1], 10), text };
|
|
290
|
+
}
|
|
291
|
+
function extractToc(markdown) {
|
|
292
|
+
return __async(this, null, function* () {
|
|
293
|
+
const headings = [];
|
|
294
|
+
const collectHeadings = () => (tree) => {
|
|
295
|
+
visit(tree, "element", (node) => {
|
|
296
|
+
const item = extractHeadingItem(node);
|
|
297
|
+
if (item) headings.push(item);
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
yield remark().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSlug).use(collectHeadings).use(rehypeStringify).process(markdown);
|
|
301
|
+
return headings;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
281
304
|
|
|
282
305
|
// src/server-articles.ts
|
|
283
306
|
var articlesDirectory = path.join(process.cwd(), "public/articles");
|
|
@@ -324,23 +347,20 @@ function sanitizeImagePath2(rawPath, articleSlug) {
|
|
|
324
347
|
}
|
|
325
348
|
return `/articles/${articleSlug}/${cleanPath}`;
|
|
326
349
|
}
|
|
350
|
+
function findArticleFile(slug) {
|
|
351
|
+
const mdPath = path.join(articlesDirectory, slug, "article.md");
|
|
352
|
+
const mdxPath = path.join(articlesDirectory, slug, "article.mdx");
|
|
353
|
+
if (fs.existsSync(mdPath)) return { filePath: mdPath, contentType: "md" };
|
|
354
|
+
if (fs.existsSync(mdxPath)) return { filePath: mdxPath, contentType: "mdx" };
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
327
357
|
function getAvailableArticleSlugs() {
|
|
328
358
|
try {
|
|
329
359
|
if (!fs.existsSync(articlesDirectory)) return [];
|
|
330
360
|
const items = fs.readdirSync(articlesDirectory, { withFileTypes: true });
|
|
331
361
|
return items.filter((item) => item.isDirectory()).filter((dir) => {
|
|
332
|
-
const articleDir = path.join(articlesDirectory, dir.name);
|
|
333
362
|
try {
|
|
334
|
-
|
|
335
|
-
withFileTypes: true
|
|
336
|
-
});
|
|
337
|
-
return entries.some((entry) => {
|
|
338
|
-
const name = typeof entry === "string" ? entry : entry == null ? void 0 : entry.name;
|
|
339
|
-
if (name !== "article.md") return false;
|
|
340
|
-
if (typeof entry === "string") return true;
|
|
341
|
-
if (typeof (entry == null ? void 0 : entry.isFile) === "function") return entry.isFile();
|
|
342
|
-
return true;
|
|
343
|
-
});
|
|
363
|
+
return findArticleFile(dir.name) !== null;
|
|
344
364
|
} catch (e) {
|
|
345
365
|
return false;
|
|
346
366
|
}
|
|
@@ -350,45 +370,64 @@ function getAvailableArticleSlugs() {
|
|
|
350
370
|
return [];
|
|
351
371
|
}
|
|
352
372
|
}
|
|
373
|
+
function parseDateField(rawDate) {
|
|
374
|
+
if (!rawDate) return void 0;
|
|
375
|
+
try {
|
|
376
|
+
const parsed = new Date(rawDate);
|
|
377
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString().split("T")[0];
|
|
378
|
+
} catch (e) {
|
|
379
|
+
}
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function resolveFeaturedImage(rawImage, slug) {
|
|
383
|
+
const img = typeof rawImage === "string" ? rawImage : "";
|
|
384
|
+
if (img && !img.startsWith("http")) return sanitizeImagePath2(img, slug) || "/placeholder-logo.png";
|
|
385
|
+
if (!img) return findArticleImage(slug) || "/placeholder-logo.png";
|
|
386
|
+
return img;
|
|
387
|
+
}
|
|
388
|
+
function parseFaqItems(raw) {
|
|
389
|
+
if (!Array.isArray(raw)) return void 0;
|
|
390
|
+
const items = raw.filter(
|
|
391
|
+
(item) => typeof item === "object" && item !== null && typeof item.question === "string" && typeof item.answer === "string"
|
|
392
|
+
);
|
|
393
|
+
return items.length ? items : void 0;
|
|
394
|
+
}
|
|
395
|
+
function parseHowToSteps(raw) {
|
|
396
|
+
if (!Array.isArray(raw)) return void 0;
|
|
397
|
+
const steps = raw.filter(
|
|
398
|
+
(item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.text === "string"
|
|
399
|
+
);
|
|
400
|
+
return steps.length ? steps : void 0;
|
|
401
|
+
}
|
|
353
402
|
function getArticleSummary(slug) {
|
|
354
403
|
return __async(this, null, function* () {
|
|
355
404
|
try {
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
358
|
-
const fileContent = fs.readFileSync(
|
|
405
|
+
const found = findArticleFile(slug);
|
|
406
|
+
if (!found) return null;
|
|
407
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
359
408
|
const { data, content: markdownContent } = matter(fileContent);
|
|
360
409
|
const readTime = getReadingTime(markdownContent);
|
|
361
|
-
let featuredImage = data.featuredImage || "";
|
|
362
|
-
if (featuredImage && !featuredImage.startsWith("http")) {
|
|
363
|
-
const sanitizedPath = sanitizeImagePath2(featuredImage, slug);
|
|
364
|
-
featuredImage = sanitizedPath || "/placeholder-logo.png";
|
|
365
|
-
} else if (!featuredImage) {
|
|
366
|
-
featuredImage = findArticleImage(slug) || "/placeholder-logo.png";
|
|
367
|
-
}
|
|
368
|
-
let articleDate;
|
|
369
|
-
if (data.date) {
|
|
370
|
-
try {
|
|
371
|
-
const parsedDate = new Date(data.date);
|
|
372
|
-
if (!Number.isNaN(parsedDate.getTime())) {
|
|
373
|
-
articleDate = parsedDate.toISOString().split("T")[0];
|
|
374
|
-
}
|
|
375
|
-
} catch (e) {
|
|
376
|
-
console.warn(`Invalid date for article ${slug}: ${data.date}`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
410
|
const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
|
|
380
411
|
const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
|
|
381
412
|
return {
|
|
382
413
|
slug,
|
|
383
414
|
title: data.title || slug.replaceAll("-", " "),
|
|
384
415
|
excerpt: data.excerpt || "",
|
|
385
|
-
date:
|
|
416
|
+
date: parseDateField(data.date),
|
|
417
|
+
lastmod: parseDateField(data.lastmod),
|
|
386
418
|
author: data.author || "Andrew Blase",
|
|
387
419
|
category: categories[0],
|
|
388
420
|
categories,
|
|
389
421
|
readTime,
|
|
390
|
-
featuredImage,
|
|
391
|
-
tags: data.tags || []
|
|
422
|
+
featuredImage: resolveFeaturedImage(data.featuredImage, slug),
|
|
423
|
+
tags: data.tags || [],
|
|
424
|
+
contentType: found.contentType,
|
|
425
|
+
draft: data.draft === true,
|
|
426
|
+
faq: parseFaqItems(data.faq),
|
|
427
|
+
howTo: parseHowToSteps(data.howTo),
|
|
428
|
+
canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
|
|
429
|
+
articleType: typeof data.articleType === "string" ? data.articleType : void 0,
|
|
430
|
+
series: typeof data.series === "string" ? data.series : void 0
|
|
392
431
|
};
|
|
393
432
|
} catch (error) {
|
|
394
433
|
console.error(`Error loading article ${slug}:`, error);
|
|
@@ -398,46 +437,21 @@ function getArticleSummary(slug) {
|
|
|
398
437
|
}
|
|
399
438
|
var getArticleMetadata = cache((slug) => __async(void 0, null, function* () {
|
|
400
439
|
try {
|
|
401
|
-
const
|
|
402
|
-
if (!
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
440
|
+
const summary = yield getArticleSummary(slug);
|
|
441
|
+
if (!summary) return null;
|
|
442
|
+
const found = findArticleFile(slug);
|
|
443
|
+
if (!found) return null;
|
|
444
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
445
|
+
const { content: markdownContent } = matter(fileContent);
|
|
446
|
+
const toc = yield extractToc(markdownContent);
|
|
447
|
+
let htmlContent;
|
|
448
|
+
let mdxSource;
|
|
449
|
+
if (found.contentType === "mdx") {
|
|
450
|
+
mdxSource = markdownContent;
|
|
451
|
+
} else {
|
|
452
|
+
htmlContent = yield markdownToHtml(markdownContent, slug);
|
|
413
453
|
}
|
|
414
|
-
|
|
415
|
-
if (data.date) {
|
|
416
|
-
try {
|
|
417
|
-
const parsedDate = new Date(data.date);
|
|
418
|
-
if (!Number.isNaN(parsedDate.getTime())) {
|
|
419
|
-
articleDate = parsedDate.toISOString().split("T")[0];
|
|
420
|
-
}
|
|
421
|
-
} catch (e) {
|
|
422
|
-
console.warn(`Invalid date for article ${slug}: ${data.date}`);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
|
|
426
|
-
const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
|
|
427
|
-
return {
|
|
428
|
-
slug,
|
|
429
|
-
title: data.title || slug.replaceAll("-", " "),
|
|
430
|
-
excerpt: data.excerpt || "",
|
|
431
|
-
date: articleDate,
|
|
432
|
-
author: data.author || "Andrew Blase",
|
|
433
|
-
category: categories[0],
|
|
434
|
-
categories,
|
|
435
|
-
readTime,
|
|
436
|
-
featuredImage,
|
|
437
|
-
tags: data.tags || [],
|
|
438
|
-
content: markdownContent,
|
|
439
|
-
htmlContent
|
|
440
|
-
};
|
|
454
|
+
return __spreadProps(__spreadValues({}, summary), { content: markdownContent, htmlContent, mdxSource, toc });
|
|
441
455
|
} catch (error) {
|
|
442
456
|
console.error(`Error loading article ${slug}:`, error);
|
|
443
457
|
return null;
|
|
@@ -447,7 +461,7 @@ var getAllArticles = cache(() => __async(void 0, null, function* () {
|
|
|
447
461
|
const slugs = getAvailableArticleSlugs();
|
|
448
462
|
const articles = yield Promise.all(slugs.map((slug) => getArticleSummary(slug)));
|
|
449
463
|
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
450
|
-
return articles.filter((article) => article !== null).filter((article) => !article.date || article.date <= currentDate).sort((a, b) => {
|
|
464
|
+
return articles.filter((article) => article !== null).filter((article) => !article.date || article.date <= currentDate).filter((article) => !(article.draft && process.env.NODE_ENV === "production")).sort((a, b) => {
|
|
451
465
|
if (!a.date && !b.date) return 0;
|
|
452
466
|
if (!a.date) return 1;
|
|
453
467
|
if (!b.date) return -1;
|
|
@@ -531,7 +545,7 @@ function resolveImageUrl(featuredImage, siteUrl) {
|
|
|
531
545
|
}
|
|
532
546
|
function generateArticleMetadata(slug, config) {
|
|
533
547
|
return __async(this, null, function* () {
|
|
534
|
-
var _a, _b, _c, _d, _e, _f;
|
|
548
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
535
549
|
const article = yield getArticleMetadata(slug);
|
|
536
550
|
if (!article) {
|
|
537
551
|
return {
|
|
@@ -541,19 +555,14 @@ function generateArticleMetadata(slug, config) {
|
|
|
541
555
|
}
|
|
542
556
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
543
557
|
const articleUrl = `${siteUrl}/articles/${slug}`;
|
|
558
|
+
const canonicalUrl = (_a = article.canonicalUrl) != null ? _a : articleUrl;
|
|
544
559
|
const imageUrl = article.featuredImage ? resolveImageUrl(article.featuredImage, siteUrl) : `${siteUrl}/placeholder-logo.png`;
|
|
545
|
-
const description = (
|
|
560
|
+
const description = (_b = article.excerpt) != null ? _b : `Read ${article.title} on ${config.siteName}.`;
|
|
546
561
|
return {
|
|
547
562
|
title: `${article.title} | ${config.siteName}`,
|
|
548
563
|
description,
|
|
549
|
-
keywords: [
|
|
550
|
-
|
|
551
|
-
"political campaign software",
|
|
552
|
-
"campaign operations",
|
|
553
|
-
"civic tech",
|
|
554
|
-
...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())
|
|
555
|
-
].join(", "),
|
|
556
|
-
openGraph: __spreadProps(__spreadValues({
|
|
564
|
+
keywords: [...((_c = article.tags) != null ? _c : []).map((tag) => tag.toLowerCase())].join(", "),
|
|
565
|
+
openGraph: __spreadProps(__spreadValues(__spreadValues({
|
|
557
566
|
title: article.title,
|
|
558
567
|
description,
|
|
559
568
|
url: articleUrl,
|
|
@@ -561,9 +570,9 @@ function generateArticleMetadata(slug, config) {
|
|
|
561
570
|
images: [{ url: imageUrl, width: 1200, height: 630, alt: article.title }],
|
|
562
571
|
locale: "en_US",
|
|
563
572
|
type: "article"
|
|
564
|
-
}, article.date && { publishedTime: article.date }), {
|
|
573
|
+
}, article.date && { publishedTime: article.date }), article.lastmod && { modifiedTime: new Date(article.lastmod).toISOString() }), {
|
|
565
574
|
authors: [article.author],
|
|
566
|
-
tags: (
|
|
575
|
+
tags: (_d = article.tags) != null ? _d : []
|
|
567
576
|
}),
|
|
568
577
|
twitter: {
|
|
569
578
|
card: "summary_large_image",
|
|
@@ -572,7 +581,7 @@ function generateArticleMetadata(slug, config) {
|
|
|
572
581
|
images: [imageUrl]
|
|
573
582
|
},
|
|
574
583
|
alternates: {
|
|
575
|
-
canonical:
|
|
584
|
+
canonical: canonicalUrl
|
|
576
585
|
},
|
|
577
586
|
robots: {
|
|
578
587
|
index: true,
|
|
@@ -585,14 +594,16 @@ function generateArticleMetadata(slug, config) {
|
|
|
585
594
|
"max-snippet": -1
|
|
586
595
|
}
|
|
587
596
|
},
|
|
588
|
-
other: __spreadProps(__spreadValues({
|
|
597
|
+
other: __spreadProps(__spreadValues(__spreadValues({
|
|
589
598
|
"article:author": article.author
|
|
590
599
|
}, article.date && {
|
|
591
600
|
"article:published_time": new Date(article.date).toISOString()
|
|
601
|
+
}), article.lastmod && {
|
|
602
|
+
"article:modified_time": new Date(article.lastmod).toISOString()
|
|
592
603
|
}), {
|
|
593
604
|
"article:section": article.category,
|
|
594
|
-
"article:tag": (
|
|
595
|
-
"linkedin:owner": (
|
|
605
|
+
"article:tag": (_f = (_e = article.tags) == null ? void 0 : _e.join(",")) != null ? _f : "",
|
|
606
|
+
"linkedin:owner": (_g = process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID) != null ? _g : ""
|
|
596
607
|
})
|
|
597
608
|
};
|
|
598
609
|
});
|
|
@@ -620,7 +631,10 @@ function generateArticlesIndexMetadata(config) {
|
|
|
620
631
|
description
|
|
621
632
|
},
|
|
622
633
|
alternates: {
|
|
623
|
-
canonical: indexUrl
|
|
634
|
+
canonical: indexUrl,
|
|
635
|
+
types: {
|
|
636
|
+
"application/rss+xml": `${siteUrl}/articles/feed.xml`
|
|
637
|
+
}
|
|
624
638
|
},
|
|
625
639
|
robots: {
|
|
626
640
|
index: true,
|
|
@@ -686,12 +700,17 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
|
|
|
686
700
|
const baseUrl = (typeof baseUrlOrConfig === "string" ? baseUrlOrConfig : baseUrlOrConfig.siteUrl).replace(/\/$/, "");
|
|
687
701
|
try {
|
|
688
702
|
const [articles, categories] = yield Promise.all([getAllArticles(), getAllCategories()]);
|
|
689
|
-
const articleEntries = articles.map((article) =>
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
703
|
+
const articleEntries = articles.map((article) => {
|
|
704
|
+
var _a;
|
|
705
|
+
const dateStr = (_a = article.lastmod) != null ? _a : article.date;
|
|
706
|
+
const lastModified = dateStr ? new Date(dateStr) : void 0;
|
|
707
|
+
return {
|
|
708
|
+
url: `${baseUrl}/articles/${article.slug}`,
|
|
709
|
+
lastModified,
|
|
710
|
+
changeFrequency: "weekly",
|
|
711
|
+
priority: 0.8
|
|
712
|
+
};
|
|
713
|
+
});
|
|
695
714
|
const categoryEntries = categories.map((cat) => ({
|
|
696
715
|
url: `${baseUrl}/articles/category/${cat.slug}`,
|
|
697
716
|
lastModified: /* @__PURE__ */ new Date(),
|
|
@@ -704,8 +723,20 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
|
|
|
704
723
|
}
|
|
705
724
|
});
|
|
706
725
|
}
|
|
726
|
+
|
|
727
|
+
// src/ArticleContent.tsx
|
|
728
|
+
import { MDXRemote } from "next-mdx-remote/rsc";
|
|
729
|
+
import { jsx } from "react/jsx-runtime";
|
|
730
|
+
function ArticleContent({ article, className }) {
|
|
731
|
+
if (article.contentType === "mdx" && article.mdxSource) {
|
|
732
|
+
return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(MDXRemote, { source: article.mdxSource }) });
|
|
733
|
+
}
|
|
734
|
+
return /* @__PURE__ */ jsx("div", { className, dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
|
|
735
|
+
}
|
|
707
736
|
export {
|
|
737
|
+
ArticleContent,
|
|
708
738
|
categoryToSlug,
|
|
739
|
+
extractToc,
|
|
709
740
|
generateArticleMetadata,
|
|
710
741
|
generateArticleStaticParams,
|
|
711
742
|
generateArticlesIndexMetadata,
|