@fullstackdatasolutions/articles 0.8.0 → 0.8.2
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 +211 -11
- package/dist/nextjs-middleware.cjs +61 -0
- package/dist/nextjs-middleware.cjs.map +1 -0
- package/dist/nextjs-middleware.d.cts +25 -0
- package/dist/nextjs-middleware.d.ts +25 -0
- package/dist/nextjs-middleware.js +33 -0
- package/dist/nextjs-middleware.js.map +1 -0
- package/dist/nextjs.cjs +652 -0
- package/dist/nextjs.cjs.map +1 -0
- package/dist/nextjs.d.cts +112 -0
- package/dist/nextjs.d.ts +112 -0
- package/dist/nextjs.js +618 -0
- package/dist/nextjs.js.map +1 -0
- package/package.json +26 -3
- package/src/__tests__/nextjs-middleware.test.ts +183 -0
- package/src/__tests__/nextjs.test.ts +137 -0
- package/src/nextjs-middleware.ts +48 -0
- package/src/nextjs.ts +27 -0
package/dist/nextjs.js
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
var __async = (__this, __arguments, generator) => {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
var fulfilled = (value) => {
|
|
23
|
+
try {
|
|
24
|
+
step(generator.next(value));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
reject(e);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var rejected = (value) => {
|
|
30
|
+
try {
|
|
31
|
+
step(generator.throw(value));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
reject(e);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
37
|
+
step((generator = generator.apply(__this, __arguments)).next());
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/server-articles.ts
|
|
42
|
+
import { cache } from "react";
|
|
43
|
+
import matter from "gray-matter";
|
|
44
|
+
import fs from "node:fs";
|
|
45
|
+
import path from "node:path";
|
|
46
|
+
import readingTime from "reading-time";
|
|
47
|
+
|
|
48
|
+
// src/markdown.ts
|
|
49
|
+
import rehypePrism from "rehype-prism-plus";
|
|
50
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
51
|
+
import rehypeSlug from "rehype-slug";
|
|
52
|
+
import rehypeStringify from "rehype-stringify";
|
|
53
|
+
import { remark } from "remark";
|
|
54
|
+
import remarkGfm from "remark-gfm";
|
|
55
|
+
import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
|
|
56
|
+
import remarkParse from "remark-parse";
|
|
57
|
+
import remarkRehype from "remark-rehype";
|
|
58
|
+
import { visit } from "unist-util-visit";
|
|
59
|
+
|
|
60
|
+
// src/errorReporting.ts
|
|
61
|
+
function defaultArticlesErrorHandler(report) {
|
|
62
|
+
if (process.env.NODE_ENV === "production") return;
|
|
63
|
+
const context = report.context ? ` ${JSON.stringify(report.context)}` : "";
|
|
64
|
+
const detail = report.error instanceof Error ? `: ${report.error.message}` : "";
|
|
65
|
+
console.warn(`[articles:${report.code}] ${report.message}${context}${detail}`);
|
|
66
|
+
}
|
|
67
|
+
var articlesErrorHandler = defaultArticlesErrorHandler;
|
|
68
|
+
function reportArticlesError(report) {
|
|
69
|
+
articlesErrorHandler(report);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/markdown.ts
|
|
73
|
+
function sanitizeImagePath(rawPath, articleSlug) {
|
|
74
|
+
if (!rawPath || typeof rawPath !== "string") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const cleanPath = rawPath.replaceAll(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
78
|
+
if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) {
|
|
79
|
+
return cleanPath;
|
|
80
|
+
}
|
|
81
|
+
if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
|
|
82
|
+
reportArticlesError({
|
|
83
|
+
code: "unsafe-image-path",
|
|
84
|
+
message: "Rejected unsafe markdown image path.",
|
|
85
|
+
context: { articleSlug, path: cleanPath }
|
|
86
|
+
});
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {
|
|
90
|
+
reportArticlesError({
|
|
91
|
+
code: "unsafe-image-path",
|
|
92
|
+
message: "Rejected markdown image path with invalid characters.",
|
|
93
|
+
context: { articleSlug, path: cleanPath }
|
|
94
|
+
});
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (cleanPath.includes("/")) {
|
|
98
|
+
const normalizedPath = cleanPath.replaceAll("\\", "/");
|
|
99
|
+
if (normalizedPath.startsWith("..") || normalizedPath.includes("../")) {
|
|
100
|
+
reportArticlesError({
|
|
101
|
+
code: "unsafe-image-path",
|
|
102
|
+
message: "Rejected markdown image path traversal attempt.",
|
|
103
|
+
context: { articleSlug, path: cleanPath }
|
|
104
|
+
});
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return `/articles/${articleSlug}/${normalizedPath}`;
|
|
108
|
+
} else {
|
|
109
|
+
return `/articles/${articleSlug}/${cleanPath}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function styleFootnoteLinks(nodes) {
|
|
113
|
+
nodes.forEach((n) => {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
if (n.type !== "element") return;
|
|
116
|
+
const el = n;
|
|
117
|
+
if (el.tagName === "a") {
|
|
118
|
+
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";
|
|
119
|
+
if (isBackRef) {
|
|
120
|
+
el.properties.className = "text-primary hover:underline ml-1";
|
|
121
|
+
if (typeof el.properties.href === "string") {
|
|
122
|
+
el.properties.href = el.properties.href.replaceAll("#user-content-fnref-", "#ref-");
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
el.properties.className = "text-primary hover:underline break-all";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (el.children) styleFootnoteLinks(el.children);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function processFootnoteRef(node) {
|
|
132
|
+
var _a, _b, _c, _d, _e, _f;
|
|
133
|
+
if (node.tagName !== "sup" || ((_b = (_a = node.children) == null ? void 0 : _a[0]) == null ? void 0 : _b.type) !== "element" || node.children[0].tagName !== "a")
|
|
134
|
+
return;
|
|
135
|
+
const link = node.children[0];
|
|
136
|
+
const href = (_c = link.properties) == null ? void 0 : _c.href;
|
|
137
|
+
if (typeof href !== "string" || !href.startsWith("#user-content-fn-")) return;
|
|
138
|
+
delete link.properties.target;
|
|
139
|
+
delete link.properties.rel;
|
|
140
|
+
link.properties.className = "text-primary hover:underline";
|
|
141
|
+
link.properties.href = href.replaceAll("#user-content-fn-", "#footnote-");
|
|
142
|
+
if (typeof ((_d = node.properties) == null ? void 0 : _d.id) === "string") {
|
|
143
|
+
link.properties.id = node.properties.id.replaceAll("user-content-fnref-", "ref-");
|
|
144
|
+
delete node.properties.id;
|
|
145
|
+
}
|
|
146
|
+
if (((_f = (_e = link.children) == null ? void 0 : _e[0]) == null ? void 0 : _f.type) === "text") {
|
|
147
|
+
link.children[0].value = `[${link.children[0].value}]`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function processFootnotesSection(node) {
|
|
151
|
+
var _a;
|
|
152
|
+
const cls = (_a = node.properties) == null ? void 0 : _a.className;
|
|
153
|
+
const isFootnotes = node.tagName === "section" && (Array.isArray(cls) ? cls.includes("footnotes") : cls === "footnotes");
|
|
154
|
+
if (!isFootnotes) return;
|
|
155
|
+
const olCandidate = node.children.find(
|
|
156
|
+
(child) => child.type === "element" && child.tagName === "ol"
|
|
157
|
+
);
|
|
158
|
+
if ((olCandidate == null ? void 0 : olCandidate.type) !== "element") return;
|
|
159
|
+
const ol = olCandidate;
|
|
160
|
+
ol.properties.className = "list-decimal ml-6 space-y-2 text-sm text-muted-foreground";
|
|
161
|
+
ol.children.forEach((li) => {
|
|
162
|
+
if (li.type !== "element" || li.tagName !== "li") return;
|
|
163
|
+
const liEl = li;
|
|
164
|
+
if (liEl.properties) {
|
|
165
|
+
liEl.properties.className = "pl-2";
|
|
166
|
+
if (typeof liEl.properties.id === "string") {
|
|
167
|
+
liEl.properties.id = liEl.properties.id.replaceAll("user-content-fn-", "footnote-");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const pIndex = liEl.children.findIndex(
|
|
171
|
+
(child) => child.type === "element" && child.tagName === "p"
|
|
172
|
+
);
|
|
173
|
+
if (pIndex !== -1) {
|
|
174
|
+
const p = liEl.children[pIndex];
|
|
175
|
+
liEl.children.splice(pIndex, 1, ...p.children);
|
|
176
|
+
}
|
|
177
|
+
styleFootnoteLinks(liEl.children);
|
|
178
|
+
});
|
|
179
|
+
const hr = {
|
|
180
|
+
type: "element",
|
|
181
|
+
tagName: "hr",
|
|
182
|
+
properties: { className: "my-8 border-border" },
|
|
183
|
+
children: []
|
|
184
|
+
};
|
|
185
|
+
const h3 = {
|
|
186
|
+
type: "element",
|
|
187
|
+
tagName: "h3",
|
|
188
|
+
properties: { className: "text-lg font-semibold mb-4" },
|
|
189
|
+
children: [{ type: "text", value: "References" }]
|
|
190
|
+
};
|
|
191
|
+
node.children = [hr, h3, ol];
|
|
192
|
+
if (node.properties) node.properties.className = void 0;
|
|
193
|
+
}
|
|
194
|
+
var customRenderer = () => {
|
|
195
|
+
return (tree) => {
|
|
196
|
+
visit(tree, "element", (node) => {
|
|
197
|
+
if (node.tagName) {
|
|
198
|
+
const props = node.properties || {};
|
|
199
|
+
switch (node.tagName) {
|
|
200
|
+
case "h1":
|
|
201
|
+
props.className = "text-3xl font-bold text-foreground mt-8 mb-4 scroll-mt-20";
|
|
202
|
+
break;
|
|
203
|
+
case "h2":
|
|
204
|
+
props.className = "text-2xl font-semibold text-foreground mt-6 mb-3 scroll-mt-20";
|
|
205
|
+
break;
|
|
206
|
+
case "h3":
|
|
207
|
+
props.className = "text-xl font-semibold text-foreground mt-4 mb-2 scroll-mt-20";
|
|
208
|
+
break;
|
|
209
|
+
case "h4":
|
|
210
|
+
props.className = "text-lg font-semibold text-foreground mt-3 mb-2 scroll-mt-20";
|
|
211
|
+
break;
|
|
212
|
+
case "p":
|
|
213
|
+
props.className = "text-muted-foreground leading-relaxed mb-4";
|
|
214
|
+
break;
|
|
215
|
+
case "a": {
|
|
216
|
+
props.className = "text-primary hover:underline transition-colors duration-200";
|
|
217
|
+
const href = typeof props.href === "string" ? props.href : "";
|
|
218
|
+
if (!href.startsWith("#")) {
|
|
219
|
+
props.target = "_blank";
|
|
220
|
+
props.rel = "noopener noreferrer";
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "ul":
|
|
225
|
+
props.className = "list-disc list-inside mb-4 space-y-2 ml-4";
|
|
226
|
+
break;
|
|
227
|
+
case "ol":
|
|
228
|
+
props.className = "list-decimal list-inside mb-4 space-y-2 ml-4";
|
|
229
|
+
break;
|
|
230
|
+
case "li":
|
|
231
|
+
props.className = "mb-1";
|
|
232
|
+
break;
|
|
233
|
+
case "blockquote":
|
|
234
|
+
props.className = "border-l-4 border-primary pl-4 italic my-4 text-muted-foreground";
|
|
235
|
+
break;
|
|
236
|
+
case "code":
|
|
237
|
+
props.className = (props.className ? props.className + " " : "") + "bg-muted px-1 py-0.5 rounded text-sm font-mono";
|
|
238
|
+
break;
|
|
239
|
+
case "pre":
|
|
240
|
+
props.className = "bg-muted rounded-lg p-4 overflow-x-auto my-4";
|
|
241
|
+
break;
|
|
242
|
+
case "img":
|
|
243
|
+
props.className = "rounded-lg my-6 w-full max-w-2xl mx-auto";
|
|
244
|
+
break;
|
|
245
|
+
case "table":
|
|
246
|
+
props.className = "border-collapse border border-border my-4 w-full";
|
|
247
|
+
break;
|
|
248
|
+
case "th":
|
|
249
|
+
props.className = "border border-border px-2 py-1 bg-muted font-semibold";
|
|
250
|
+
break;
|
|
251
|
+
case "td":
|
|
252
|
+
props.className = "border border-border px-2 py-1";
|
|
253
|
+
break;
|
|
254
|
+
case "hr":
|
|
255
|
+
props.className = "my-8 border-border";
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
node.properties = props;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
visit(tree, "element", (node) => {
|
|
264
|
+
processFootnoteRef(node);
|
|
265
|
+
processFootnotesSection(node);
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
var rehypeProcessImages = (options = {}) => {
|
|
270
|
+
return (tree) => {
|
|
271
|
+
visit(tree, "element", (node) => {
|
|
272
|
+
if (node.tagName === "img" && node.properties) {
|
|
273
|
+
const src = node.properties.src;
|
|
274
|
+
if (src && typeof src === "string" && options.articleSlug) {
|
|
275
|
+
const sanitizedSrc = sanitizeImagePath(src, options.articleSlug);
|
|
276
|
+
if (sanitizedSrc) {
|
|
277
|
+
node.properties.src = sanitizedSrc;
|
|
278
|
+
} else {
|
|
279
|
+
reportArticlesError({
|
|
280
|
+
code: "unsafe-image-path",
|
|
281
|
+
message: "Using placeholder for unsafe markdown image path.",
|
|
282
|
+
context: { articleSlug: options.articleSlug, path: src }
|
|
283
|
+
});
|
|
284
|
+
node.properties.src = "/placeholder-logo.png";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
function markdownToHtml(markdown, articleSlug) {
|
|
292
|
+
return __async(this, null, function* () {
|
|
293
|
+
try {
|
|
294
|
+
let processor = remark().use(remarkParse).use(remarkGfm).use(remarkGithubBlockquoteAlert).use(remarkRehype).use(customRenderer).use(rehypeSlug).use(rehypePrism).use(rehypeSanitize, {
|
|
295
|
+
attributes: {
|
|
296
|
+
"*": ["className", "class", "id"],
|
|
297
|
+
a: ["href", "target", "rel", "id"],
|
|
298
|
+
img: ["src", "alt"]
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
if (articleSlug) {
|
|
302
|
+
processor = processor.use(rehypeProcessImages, { articleSlug });
|
|
303
|
+
}
|
|
304
|
+
const result = yield processor.use(rehypeStringify).process(markdown);
|
|
305
|
+
return result.toString();
|
|
306
|
+
} catch (error) {
|
|
307
|
+
reportArticlesError({
|
|
308
|
+
code: "markdown-conversion-failed",
|
|
309
|
+
message: "Unable to convert markdown to HTML.",
|
|
310
|
+
error,
|
|
311
|
+
context: { articleSlug }
|
|
312
|
+
});
|
|
313
|
+
return markdown;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function nodeTextValue(c) {
|
|
318
|
+
return c.type === "text" ? c.value : "";
|
|
319
|
+
}
|
|
320
|
+
function extractHeadingItem(node) {
|
|
321
|
+
var _a;
|
|
322
|
+
const match = /^h([1-6])$/.exec(node.tagName);
|
|
323
|
+
if (!match) return null;
|
|
324
|
+
const id = typeof ((_a = node.properties) == null ? void 0 : _a.id) === "string" ? node.properties.id : "";
|
|
325
|
+
const text = node.children.map(nodeTextValue).join("");
|
|
326
|
+
if (!id || !text) return null;
|
|
327
|
+
return { id, depth: Number.parseInt(match[1], 10), text };
|
|
328
|
+
}
|
|
329
|
+
function extractToc(markdown) {
|
|
330
|
+
return __async(this, null, function* () {
|
|
331
|
+
const headings = [];
|
|
332
|
+
const collectHeadings = () => (tree) => {
|
|
333
|
+
visit(tree, "element", (node) => {
|
|
334
|
+
const item = extractHeadingItem(node);
|
|
335
|
+
if (item) headings.push(item);
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
yield remark().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSlug).use(collectHeadings).use(rehypeStringify).process(markdown);
|
|
339
|
+
return headings;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/server-articles.ts
|
|
344
|
+
var articlesDirectory = path.join(
|
|
345
|
+
/* turbopackIgnore: true */
|
|
346
|
+
process.cwd(),
|
|
347
|
+
"public/articles"
|
|
348
|
+
);
|
|
349
|
+
function getReadingTime(content) {
|
|
350
|
+
return readingTime(content).text;
|
|
351
|
+
}
|
|
352
|
+
function findArticleImage(slug) {
|
|
353
|
+
try {
|
|
354
|
+
const articleDir = path.join(articlesDirectory, slug);
|
|
355
|
+
const entries = fs.readdirSync(articleDir, { withFileTypes: true });
|
|
356
|
+
const names = entries.map((entry) => {
|
|
357
|
+
if (typeof entry === "string") return entry;
|
|
358
|
+
if (entry && typeof entry.name === "string") return entry.name;
|
|
359
|
+
return null;
|
|
360
|
+
}).filter((name) => Boolean(name));
|
|
361
|
+
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
362
|
+
const imageFileName = names.find(
|
|
363
|
+
(name) => imageExtensions.some((ext) => name.toLowerCase().endsWith(ext))
|
|
364
|
+
);
|
|
365
|
+
return imageFileName ? sanitizeImagePath2(imageFileName, slug) : null;
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function sanitizeImagePath2(rawPath, articleSlug) {
|
|
371
|
+
if (!rawPath || typeof rawPath !== "string") return null;
|
|
372
|
+
const cleanPath = rawPath.replaceAll(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
373
|
+
if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) return cleanPath;
|
|
374
|
+
if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
|
|
375
|
+
reportArticlesError({
|
|
376
|
+
code: "unsafe-image-path",
|
|
377
|
+
message: "Rejected unsafe article image path.",
|
|
378
|
+
context: { articleSlug, path: cleanPath }
|
|
379
|
+
});
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {
|
|
383
|
+
reportArticlesError({
|
|
384
|
+
code: "unsafe-image-path",
|
|
385
|
+
message: "Rejected article image path with invalid characters.",
|
|
386
|
+
context: { articleSlug, path: cleanPath }
|
|
387
|
+
});
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
if (cleanPath.includes("/")) {
|
|
391
|
+
const normalizedPath = path.normalize(cleanPath);
|
|
392
|
+
if (normalizedPath.startsWith("..") || normalizedPath.includes("../")) {
|
|
393
|
+
reportArticlesError({
|
|
394
|
+
code: "unsafe-image-path",
|
|
395
|
+
message: "Rejected article image path traversal attempt.",
|
|
396
|
+
context: { articleSlug, path: cleanPath }
|
|
397
|
+
});
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
return `/articles/${articleSlug}/${cleanPath}`;
|
|
401
|
+
}
|
|
402
|
+
return `/articles/${articleSlug}/${cleanPath}`;
|
|
403
|
+
}
|
|
404
|
+
function findArticleFile(slug) {
|
|
405
|
+
const mdPath = path.join(articlesDirectory, slug, "article.md");
|
|
406
|
+
const mdxPath = path.join(articlesDirectory, slug, "article.mdx");
|
|
407
|
+
if (fs.existsSync(mdPath)) return { filePath: mdPath, contentType: "md" };
|
|
408
|
+
if (fs.existsSync(mdxPath)) return { filePath: mdxPath, contentType: "mdx" };
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
function getAvailableArticleSlugs() {
|
|
412
|
+
try {
|
|
413
|
+
if (!fs.existsSync(articlesDirectory)) return [];
|
|
414
|
+
return walkArticleDir(articlesDirectory, "");
|
|
415
|
+
} catch (error) {
|
|
416
|
+
reportArticlesError({
|
|
417
|
+
code: "article-directory-read-failed",
|
|
418
|
+
message: "Unable to read articles directory.",
|
|
419
|
+
error,
|
|
420
|
+
context: { directory: articlesDirectory }
|
|
421
|
+
});
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function walkArticleDir(dir, baseSlug) {
|
|
426
|
+
const slugs = [];
|
|
427
|
+
try {
|
|
428
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
429
|
+
for (const item of items) {
|
|
430
|
+
if (!item.isDirectory()) continue;
|
|
431
|
+
const slug = baseSlug ? `${baseSlug}/${item.name}` : item.name;
|
|
432
|
+
if (findArticleFile(slug) !== null) {
|
|
433
|
+
slugs.push(slug);
|
|
434
|
+
}
|
|
435
|
+
slugs.push(...walkArticleDir(path.join(dir, item.name), slug));
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
}
|
|
439
|
+
return slugs;
|
|
440
|
+
}
|
|
441
|
+
function parseDateField(rawDate) {
|
|
442
|
+
if (!rawDate) return void 0;
|
|
443
|
+
try {
|
|
444
|
+
const parsed = new Date(rawDate);
|
|
445
|
+
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString().split("T")[0];
|
|
446
|
+
} catch (e) {
|
|
447
|
+
}
|
|
448
|
+
return void 0;
|
|
449
|
+
}
|
|
450
|
+
function resolveFeaturedImage(rawImage, slug) {
|
|
451
|
+
const img = typeof rawImage === "string" ? rawImage : "";
|
|
452
|
+
if (img && !img.startsWith("http")) return sanitizeImagePath2(img, slug) || "/placeholder-logo.png";
|
|
453
|
+
if (!img) return findArticleImage(slug) || "/placeholder-logo.png";
|
|
454
|
+
return img;
|
|
455
|
+
}
|
|
456
|
+
function parseFaqItems(raw) {
|
|
457
|
+
if (!Array.isArray(raw)) return void 0;
|
|
458
|
+
const items = raw.filter(
|
|
459
|
+
(item) => typeof item === "object" && item !== null && typeof item.question === "string" && typeof item.answer === "string"
|
|
460
|
+
);
|
|
461
|
+
return items.length ? items : void 0;
|
|
462
|
+
}
|
|
463
|
+
function parseHowToSteps(raw) {
|
|
464
|
+
if (!Array.isArray(raw)) return void 0;
|
|
465
|
+
const steps = raw.filter(
|
|
466
|
+
(item) => typeof item === "object" && item !== null && typeof item.name === "string" && typeof item.text === "string"
|
|
467
|
+
);
|
|
468
|
+
return steps.length ? steps : void 0;
|
|
469
|
+
}
|
|
470
|
+
function getArticleSummary(slug) {
|
|
471
|
+
return __async(this, null, function* () {
|
|
472
|
+
try {
|
|
473
|
+
const found = findArticleFile(slug);
|
|
474
|
+
if (!found) return null;
|
|
475
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
476
|
+
const { data, content: markdownContent } = matter(fileContent);
|
|
477
|
+
const readTime = getReadingTime(markdownContent);
|
|
478
|
+
const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
|
|
479
|
+
const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
|
|
480
|
+
return {
|
|
481
|
+
slug,
|
|
482
|
+
title: data.title || slug.replaceAll("-", " "),
|
|
483
|
+
excerpt: data.excerpt || "",
|
|
484
|
+
date: parseDateField(data.date),
|
|
485
|
+
lastmod: parseDateField(data.lastmod),
|
|
486
|
+
author: data.author || "Andrew Blase",
|
|
487
|
+
category: categories[0],
|
|
488
|
+
categories,
|
|
489
|
+
readTime,
|
|
490
|
+
featuredImage: resolveFeaturedImage(data.featuredImage, slug),
|
|
491
|
+
tags: data.tags || [],
|
|
492
|
+
contentType: found.contentType,
|
|
493
|
+
draft: data.draft === true,
|
|
494
|
+
faq: parseFaqItems(data.faq),
|
|
495
|
+
howTo: parseHowToSteps(data.howTo),
|
|
496
|
+
canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
|
|
497
|
+
articleType: typeof data.articleType === "string" ? data.articleType : void 0,
|
|
498
|
+
series: typeof data.series === "string" ? data.series : void 0,
|
|
499
|
+
aiCrawl: data.aiCrawl === true
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
reportArticlesError({
|
|
503
|
+
code: "article-load-failed",
|
|
504
|
+
message: "Unable to load article summary.",
|
|
505
|
+
error,
|
|
506
|
+
context: { slug }
|
|
507
|
+
});
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
var getArticleMetadata = cache((slug) => __async(void 0, null, function* () {
|
|
513
|
+
try {
|
|
514
|
+
const summary = yield getArticleSummary(slug);
|
|
515
|
+
if (!summary) return null;
|
|
516
|
+
const found = findArticleFile(slug);
|
|
517
|
+
if (!found) return null;
|
|
518
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
519
|
+
const { content: markdownContent } = matter(fileContent);
|
|
520
|
+
const toc = yield extractToc(markdownContent);
|
|
521
|
+
let htmlContent;
|
|
522
|
+
let mdxSource;
|
|
523
|
+
if (found.contentType === "mdx") {
|
|
524
|
+
mdxSource = markdownContent;
|
|
525
|
+
} else {
|
|
526
|
+
htmlContent = yield markdownToHtml(markdownContent, slug);
|
|
527
|
+
}
|
|
528
|
+
return __spreadProps(__spreadValues({}, summary), { content: markdownContent, htmlContent, mdxSource, toc });
|
|
529
|
+
} catch (error) {
|
|
530
|
+
reportArticlesError({
|
|
531
|
+
code: "article-load-failed",
|
|
532
|
+
message: "Unable to load article metadata.",
|
|
533
|
+
error,
|
|
534
|
+
context: { slug }
|
|
535
|
+
});
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}));
|
|
539
|
+
var getAllArticles = cache(() => __async(void 0, null, function* () {
|
|
540
|
+
const slugs = getAvailableArticleSlugs();
|
|
541
|
+
const articles = yield Promise.all(slugs.map((slug) => getArticleSummary(slug)));
|
|
542
|
+
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
543
|
+
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) => {
|
|
544
|
+
if (!a.date && !b.date) return 0;
|
|
545
|
+
if (!a.date) return 1;
|
|
546
|
+
if (!b.date) return -1;
|
|
547
|
+
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
|
548
|
+
});
|
|
549
|
+
}));
|
|
550
|
+
function getArticleMarkdown(slug) {
|
|
551
|
+
return __async(this, null, function* () {
|
|
552
|
+
try {
|
|
553
|
+
const summary = yield getArticleSummary(slug);
|
|
554
|
+
if (!(summary == null ? void 0 : summary.aiCrawl)) return null;
|
|
555
|
+
const found = findArticleFile(slug);
|
|
556
|
+
if (!found) return null;
|
|
557
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
558
|
+
const { content: markdownContent } = matter(fileContent);
|
|
559
|
+
return markdownContent;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
reportArticlesError({
|
|
562
|
+
code: "article-markdown-load-failed",
|
|
563
|
+
message: "Unable to load article markdown.",
|
|
564
|
+
error,
|
|
565
|
+
context: { slug }
|
|
566
|
+
});
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function getArticleMarkdownResponse(slug, config) {
|
|
572
|
+
return __async(this, null, function* () {
|
|
573
|
+
const markdown = yield getArticleMarkdown(slug);
|
|
574
|
+
if (markdown === null) return new Response("Not Found", { status: 404 });
|
|
575
|
+
const article = yield getArticleMetadata(slug);
|
|
576
|
+
return new Response(markdown, {
|
|
577
|
+
headers: __spreadValues({
|
|
578
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
579
|
+
"Cache-Control": "public, max-age=3600, s-maxage=3600"
|
|
580
|
+
}, article ? getArticleAiHeaders(article, config) : {})
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
function getArticleMarkdownUrl(article, config) {
|
|
585
|
+
if (article.aiCrawl !== true) return void 0;
|
|
586
|
+
const pathname = `/articles/${article.slug}.md`;
|
|
587
|
+
if (!config) return pathname;
|
|
588
|
+
return `${config.siteUrl.replace(/\/$/, "")}${pathname}`;
|
|
589
|
+
}
|
|
590
|
+
function getArticleAiHeaders(article, config) {
|
|
591
|
+
const markdownUrl = getArticleMarkdownUrl(article, config);
|
|
592
|
+
if (markdownUrl) {
|
|
593
|
+
return {
|
|
594
|
+
Link: `<${markdownUrl}>; rel="alternate"; type="text/markdown"`
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
"X-Robots-Tag": "noai, noimageai"
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/nextjs.ts
|
|
603
|
+
function createArticleMarkdownHandler(config) {
|
|
604
|
+
function GET(_request, context) {
|
|
605
|
+
return __async(this, null, function* () {
|
|
606
|
+
const params = yield context.params;
|
|
607
|
+
const slugParts = params["slug"];
|
|
608
|
+
const slug = Array.isArray(slugParts) ? slugParts.join("/") : slugParts != null ? slugParts : "";
|
|
609
|
+
const cleanSlug = slug.endsWith(".md") ? slug.slice(0, -3) : slug;
|
|
610
|
+
return getArticleMarkdownResponse(cleanSlug, config);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return { GET };
|
|
614
|
+
}
|
|
615
|
+
export {
|
|
616
|
+
createArticleMarkdownHandler
|
|
617
|
+
};
|
|
618
|
+
//# sourceMappingURL=nextjs.js.map
|