@fullstackdatasolutions/articles 0.7.1 → 0.8.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 +139 -76
- package/dist/index.cjs +30 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +30 -13
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +180 -25
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +33 -14
- package/dist/server.d.ts +33 -14
- package/dist/server.js +173 -24
- package/dist/server.js.map +1 -1
- package/package.json +7 -1
- package/scripts/postinstall.cjs +9 -0
- package/src/ArticleDetailHero.tsx +8 -4
- package/src/ArticleSchemas.tsx +17 -4
- package/src/ArticlesPage.tsx +5 -1
- package/src/FeaturedArticle.tsx +8 -5
- package/src/__tests__/ArticleDetailHero.test.tsx +12 -0
- package/src/__tests__/ArticleSchemas.test.tsx +28 -0
- package/src/__tests__/FeaturedArticle.test.tsx +5 -0
- package/src/__tests__/errorReporting.test.ts +89 -0
- package/src/__tests__/seoUtils.test.ts +38 -0
- package/src/__tests__/server-articles.test.ts +194 -0
- package/src/articleTypes.ts +1 -0
- package/src/articlesConfig.ts +2 -0
- package/src/errorReporting.ts +34 -0
- package/src/markdown.ts +27 -5
- package/src/seoUtils.ts +10 -2
- package/src/server-articles.ts +124 -9
- package/src/server.ts +12 -0
package/dist/server.d.cts
CHANGED
|
@@ -37,6 +37,7 @@ interface Article {
|
|
|
37
37
|
canonicalUrl?: string;
|
|
38
38
|
articleType?: string;
|
|
39
39
|
series?: string;
|
|
40
|
+
aiCrawl?: boolean;
|
|
40
41
|
}
|
|
41
42
|
interface CategoryInfo {
|
|
42
43
|
name: string;
|
|
@@ -45,19 +46,6 @@ interface CategoryInfo {
|
|
|
45
46
|
featuredImage: string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
declare function sanitizeImagePath(rawPath: string, articleSlug: string): string | null;
|
|
49
|
-
declare function getAvailableArticleSlugs(): string[];
|
|
50
|
-
declare const getArticleMetadata: (slug: string) => Promise<Article | null>;
|
|
51
|
-
declare const getAllArticles: () => Promise<Article[]>;
|
|
52
|
-
declare function getAdjacentArticles(currentSlug: string): Promise<{
|
|
53
|
-
previous: Article | null;
|
|
54
|
-
next: Article | null;
|
|
55
|
-
}>;
|
|
56
|
-
declare function searchArticles(query: string): Promise<Article[]>;
|
|
57
|
-
declare function categoryToSlug(category: string): string;
|
|
58
|
-
declare function getAllCategories(): Promise<CategoryInfo[]>;
|
|
59
|
-
declare function getArticlesByCategory(categorySlug: string): Promise<Article[]>;
|
|
60
|
-
|
|
61
49
|
/** Keys for each renderable section of the articles listing page. */
|
|
62
50
|
type ArticlesSection = 'hero' | 'search' | 'featured' | 'latest' | 'categories' | 'newsletter';
|
|
63
51
|
/**
|
|
@@ -149,8 +137,28 @@ interface ArticlesConfig {
|
|
|
149
137
|
showToc?: boolean;
|
|
150
138
|
/** Set to false to hide the "Back to Articles" navigation link on article detail pages. Default: true. */
|
|
151
139
|
showBackToArticles?: boolean;
|
|
140
|
+
/** Set to false to hide author names from UI and metadata. Default: true. */
|
|
141
|
+
showAuthor?: boolean;
|
|
152
142
|
}
|
|
153
143
|
|
|
144
|
+
declare function sanitizeImagePath(rawPath: string, articleSlug: string): string | null;
|
|
145
|
+
declare function getAvailableArticleSlugs(): string[];
|
|
146
|
+
declare const getArticleMetadata: (slug: string) => Promise<Article | null>;
|
|
147
|
+
declare const getAllArticles: () => Promise<Article[]>;
|
|
148
|
+
declare function getAdjacentArticles(currentSlug: string): Promise<{
|
|
149
|
+
previous: Article | null;
|
|
150
|
+
next: Article | null;
|
|
151
|
+
}>;
|
|
152
|
+
declare function getArticleMarkdown(slug: string): Promise<string | null>;
|
|
153
|
+
declare function getArticleMarkdownResponse(slug: string, config: ArticlesConfig): Promise<Response>;
|
|
154
|
+
declare function getArticleMarkdownUrl(article: Pick<Article, 'slug' | 'aiCrawl'>, config?: Pick<ArticlesConfig, 'siteUrl'>): string | undefined;
|
|
155
|
+
declare function getArticleAiHeaders(article: Pick<Article, 'slug' | 'aiCrawl'>, config?: Pick<ArticlesConfig, 'siteUrl'>): Record<string, string>;
|
|
156
|
+
declare function getAiRobotsTxtRules(): Promise<string>;
|
|
157
|
+
declare function searchArticles(query: string, config?: ArticlesConfig): Promise<Article[]>;
|
|
158
|
+
declare function categoryToSlug(category: string): string;
|
|
159
|
+
declare function getAllCategories(): Promise<CategoryInfo[]>;
|
|
160
|
+
declare function getArticlesByCategory(categorySlug: string): Promise<Article[]>;
|
|
161
|
+
|
|
154
162
|
declare function generateArticleStaticParams(): {
|
|
155
163
|
slug: string;
|
|
156
164
|
}[];
|
|
@@ -165,6 +173,17 @@ declare function getArticleSitemapEntries(baseUrlOrConfig: string | ArticlesConf
|
|
|
165
173
|
declare function markdownToHtml(markdown: string, articleSlug?: string): Promise<string>;
|
|
166
174
|
declare function extractToc(markdown: string): Promise<TocItem[]>;
|
|
167
175
|
|
|
176
|
+
type ArticlesErrorCode = 'article-directory-read-failed' | 'article-load-failed' | 'article-markdown-load-failed' | 'markdown-conversion-failed' | 'unsafe-image-path';
|
|
177
|
+
type ArticlesErrorContext = Readonly<Record<string, string | number | boolean | undefined>>;
|
|
178
|
+
type ArticlesErrorReport = Readonly<{
|
|
179
|
+
code: ArticlesErrorCode;
|
|
180
|
+
message: string;
|
|
181
|
+
error?: unknown;
|
|
182
|
+
context?: ArticlesErrorContext;
|
|
183
|
+
}>;
|
|
184
|
+
type ArticlesErrorHandler = (report: ArticlesErrorReport) => void;
|
|
185
|
+
declare function setArticlesErrorHandler(handler?: ArticlesErrorHandler): void;
|
|
186
|
+
|
|
168
187
|
type ArticleContentProps = Readonly<{
|
|
169
188
|
article: Article;
|
|
170
189
|
className?: string;
|
|
@@ -177,4 +196,4 @@ type ArticleTOCProps = Readonly<{
|
|
|
177
196
|
}>;
|
|
178
197
|
declare function ArticleTOC({ toc, className }: ArticleTOCProps): react_jsx_runtime.JSX.Element | null;
|
|
179
198
|
|
|
180
|
-
export { type Article, ArticleContent, ArticleTOC, type ArticlesConfig, type CategoryInfo, type TocItem, categoryToSlug, extractToc, generateArticleMetadata, generateArticleStaticParams, generateArticlesIndexMetadata, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
|
|
199
|
+
export { type Article, ArticleContent, ArticleTOC, type ArticlesConfig, type ArticlesErrorCode, type ArticlesErrorContext, type ArticlesErrorHandler, type ArticlesErrorReport, type CategoryInfo, type TocItem, categoryToSlug, extractToc, generateArticleMetadata, generateArticleStaticParams, generateArticlesIndexMetadata, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAiRobotsTxtRules, getAllArticles, getAllCategories, getArticleAiHeaders, getArticleMarkdown, getArticleMarkdownResponse, getArticleMarkdownUrl, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles, setArticlesErrorHandler };
|
package/dist/server.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ interface Article {
|
|
|
37
37
|
canonicalUrl?: string;
|
|
38
38
|
articleType?: string;
|
|
39
39
|
series?: string;
|
|
40
|
+
aiCrawl?: boolean;
|
|
40
41
|
}
|
|
41
42
|
interface CategoryInfo {
|
|
42
43
|
name: string;
|
|
@@ -45,19 +46,6 @@ interface CategoryInfo {
|
|
|
45
46
|
featuredImage: string;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
declare function sanitizeImagePath(rawPath: string, articleSlug: string): string | null;
|
|
49
|
-
declare function getAvailableArticleSlugs(): string[];
|
|
50
|
-
declare const getArticleMetadata: (slug: string) => Promise<Article | null>;
|
|
51
|
-
declare const getAllArticles: () => Promise<Article[]>;
|
|
52
|
-
declare function getAdjacentArticles(currentSlug: string): Promise<{
|
|
53
|
-
previous: Article | null;
|
|
54
|
-
next: Article | null;
|
|
55
|
-
}>;
|
|
56
|
-
declare function searchArticles(query: string): Promise<Article[]>;
|
|
57
|
-
declare function categoryToSlug(category: string): string;
|
|
58
|
-
declare function getAllCategories(): Promise<CategoryInfo[]>;
|
|
59
|
-
declare function getArticlesByCategory(categorySlug: string): Promise<Article[]>;
|
|
60
|
-
|
|
61
49
|
/** Keys for each renderable section of the articles listing page. */
|
|
62
50
|
type ArticlesSection = 'hero' | 'search' | 'featured' | 'latest' | 'categories' | 'newsletter';
|
|
63
51
|
/**
|
|
@@ -149,8 +137,28 @@ interface ArticlesConfig {
|
|
|
149
137
|
showToc?: boolean;
|
|
150
138
|
/** Set to false to hide the "Back to Articles" navigation link on article detail pages. Default: true. */
|
|
151
139
|
showBackToArticles?: boolean;
|
|
140
|
+
/** Set to false to hide author names from UI and metadata. Default: true. */
|
|
141
|
+
showAuthor?: boolean;
|
|
152
142
|
}
|
|
153
143
|
|
|
144
|
+
declare function sanitizeImagePath(rawPath: string, articleSlug: string): string | null;
|
|
145
|
+
declare function getAvailableArticleSlugs(): string[];
|
|
146
|
+
declare const getArticleMetadata: (slug: string) => Promise<Article | null>;
|
|
147
|
+
declare const getAllArticles: () => Promise<Article[]>;
|
|
148
|
+
declare function getAdjacentArticles(currentSlug: string): Promise<{
|
|
149
|
+
previous: Article | null;
|
|
150
|
+
next: Article | null;
|
|
151
|
+
}>;
|
|
152
|
+
declare function getArticleMarkdown(slug: string): Promise<string | null>;
|
|
153
|
+
declare function getArticleMarkdownResponse(slug: string, config: ArticlesConfig): Promise<Response>;
|
|
154
|
+
declare function getArticleMarkdownUrl(article: Pick<Article, 'slug' | 'aiCrawl'>, config?: Pick<ArticlesConfig, 'siteUrl'>): string | undefined;
|
|
155
|
+
declare function getArticleAiHeaders(article: Pick<Article, 'slug' | 'aiCrawl'>, config?: Pick<ArticlesConfig, 'siteUrl'>): Record<string, string>;
|
|
156
|
+
declare function getAiRobotsTxtRules(): Promise<string>;
|
|
157
|
+
declare function searchArticles(query: string, config?: ArticlesConfig): Promise<Article[]>;
|
|
158
|
+
declare function categoryToSlug(category: string): string;
|
|
159
|
+
declare function getAllCategories(): Promise<CategoryInfo[]>;
|
|
160
|
+
declare function getArticlesByCategory(categorySlug: string): Promise<Article[]>;
|
|
161
|
+
|
|
154
162
|
declare function generateArticleStaticParams(): {
|
|
155
163
|
slug: string;
|
|
156
164
|
}[];
|
|
@@ -165,6 +173,17 @@ declare function getArticleSitemapEntries(baseUrlOrConfig: string | ArticlesConf
|
|
|
165
173
|
declare function markdownToHtml(markdown: string, articleSlug?: string): Promise<string>;
|
|
166
174
|
declare function extractToc(markdown: string): Promise<TocItem[]>;
|
|
167
175
|
|
|
176
|
+
type ArticlesErrorCode = 'article-directory-read-failed' | 'article-load-failed' | 'article-markdown-load-failed' | 'markdown-conversion-failed' | 'unsafe-image-path';
|
|
177
|
+
type ArticlesErrorContext = Readonly<Record<string, string | number | boolean | undefined>>;
|
|
178
|
+
type ArticlesErrorReport = Readonly<{
|
|
179
|
+
code: ArticlesErrorCode;
|
|
180
|
+
message: string;
|
|
181
|
+
error?: unknown;
|
|
182
|
+
context?: ArticlesErrorContext;
|
|
183
|
+
}>;
|
|
184
|
+
type ArticlesErrorHandler = (report: ArticlesErrorReport) => void;
|
|
185
|
+
declare function setArticlesErrorHandler(handler?: ArticlesErrorHandler): void;
|
|
186
|
+
|
|
168
187
|
type ArticleContentProps = Readonly<{
|
|
169
188
|
article: Article;
|
|
170
189
|
className?: string;
|
|
@@ -177,4 +196,4 @@ type ArticleTOCProps = Readonly<{
|
|
|
177
196
|
}>;
|
|
178
197
|
declare function ArticleTOC({ toc, className }: ArticleTOCProps): react_jsx_runtime.JSX.Element | null;
|
|
179
198
|
|
|
180
|
-
export { type Article, ArticleContent, ArticleTOC, type ArticlesConfig, type CategoryInfo, type TocItem, categoryToSlug, extractToc, generateArticleMetadata, generateArticleStaticParams, generateArticlesIndexMetadata, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
|
|
199
|
+
export { type Article, ArticleContent, ArticleTOC, type ArticlesConfig, type ArticlesErrorCode, type ArticlesErrorContext, type ArticlesErrorHandler, type ArticlesErrorReport, type CategoryInfo, type TocItem, categoryToSlug, extractToc, generateArticleMetadata, generateArticleStaticParams, generateArticlesIndexMetadata, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAiRobotsTxtRules, getAllArticles, getAllCategories, getArticleAiHeaders, getArticleMarkdown, getArticleMarkdownResponse, getArticleMarkdownUrl, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles, setArticlesErrorHandler };
|
package/dist/server.js
CHANGED
|
@@ -68,6 +68,23 @@ import remarkGithubBlockquoteAlert from "remark-github-blockquote-alert";
|
|
|
68
68
|
import remarkParse from "remark-parse";
|
|
69
69
|
import remarkRehype from "remark-rehype";
|
|
70
70
|
import { visit } from "unist-util-visit";
|
|
71
|
+
|
|
72
|
+
// src/errorReporting.ts
|
|
73
|
+
function defaultArticlesErrorHandler(report) {
|
|
74
|
+
if (process.env.NODE_ENV === "production") return;
|
|
75
|
+
const context = report.context ? ` ${JSON.stringify(report.context)}` : "";
|
|
76
|
+
const detail = report.error instanceof Error ? `: ${report.error.message}` : "";
|
|
77
|
+
console.warn(`[articles:${report.code}] ${report.message}${context}${detail}`);
|
|
78
|
+
}
|
|
79
|
+
var articlesErrorHandler = defaultArticlesErrorHandler;
|
|
80
|
+
function setArticlesErrorHandler(handler) {
|
|
81
|
+
articlesErrorHandler = handler != null ? handler : defaultArticlesErrorHandler;
|
|
82
|
+
}
|
|
83
|
+
function reportArticlesError(report) {
|
|
84
|
+
articlesErrorHandler(report);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/markdown.ts
|
|
71
88
|
function sanitizeImagePath(rawPath, articleSlug) {
|
|
72
89
|
if (!rawPath || typeof rawPath !== "string") {
|
|
73
90
|
return null;
|
|
@@ -77,17 +94,29 @@ function sanitizeImagePath(rawPath, articleSlug) {
|
|
|
77
94
|
return cleanPath;
|
|
78
95
|
}
|
|
79
96
|
if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
|
|
80
|
-
|
|
97
|
+
reportArticlesError({
|
|
98
|
+
code: "unsafe-image-path",
|
|
99
|
+
message: "Rejected unsafe markdown image path.",
|
|
100
|
+
context: { articleSlug, path: cleanPath }
|
|
101
|
+
});
|
|
81
102
|
return null;
|
|
82
103
|
}
|
|
83
104
|
if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {
|
|
84
|
-
|
|
105
|
+
reportArticlesError({
|
|
106
|
+
code: "unsafe-image-path",
|
|
107
|
+
message: "Rejected markdown image path with invalid characters.",
|
|
108
|
+
context: { articleSlug, path: cleanPath }
|
|
109
|
+
});
|
|
85
110
|
return null;
|
|
86
111
|
}
|
|
87
112
|
if (cleanPath.includes("/")) {
|
|
88
113
|
const normalizedPath = cleanPath.replaceAll("\\", "/");
|
|
89
114
|
if (normalizedPath.startsWith("..") || normalizedPath.includes("../")) {
|
|
90
|
-
|
|
115
|
+
reportArticlesError({
|
|
116
|
+
code: "unsafe-image-path",
|
|
117
|
+
message: "Rejected markdown image path traversal attempt.",
|
|
118
|
+
context: { articleSlug, path: cleanPath }
|
|
119
|
+
});
|
|
91
120
|
return null;
|
|
92
121
|
}
|
|
93
122
|
return `/articles/${articleSlug}/${normalizedPath}`;
|
|
@@ -262,7 +291,11 @@ var rehypeProcessImages = (options = {}) => {
|
|
|
262
291
|
if (sanitizedSrc) {
|
|
263
292
|
node.properties.src = sanitizedSrc;
|
|
264
293
|
} else {
|
|
265
|
-
|
|
294
|
+
reportArticlesError({
|
|
295
|
+
code: "unsafe-image-path",
|
|
296
|
+
message: "Using placeholder for unsafe markdown image path.",
|
|
297
|
+
context: { articleSlug: options.articleSlug, path: src }
|
|
298
|
+
});
|
|
266
299
|
node.properties.src = "/placeholder-logo.png";
|
|
267
300
|
}
|
|
268
301
|
}
|
|
@@ -286,7 +319,12 @@ function markdownToHtml(markdown, articleSlug) {
|
|
|
286
319
|
const result = yield processor.use(rehypeStringify).process(markdown);
|
|
287
320
|
return result.toString();
|
|
288
321
|
} catch (error) {
|
|
289
|
-
|
|
322
|
+
reportArticlesError({
|
|
323
|
+
code: "markdown-conversion-failed",
|
|
324
|
+
message: "Unable to convert markdown to HTML.",
|
|
325
|
+
error,
|
|
326
|
+
context: { articleSlug }
|
|
327
|
+
});
|
|
290
328
|
return markdown;
|
|
291
329
|
}
|
|
292
330
|
});
|
|
@@ -318,7 +356,11 @@ function extractToc(markdown) {
|
|
|
318
356
|
}
|
|
319
357
|
|
|
320
358
|
// src/server-articles.ts
|
|
321
|
-
var articlesDirectory = path.join(
|
|
359
|
+
var articlesDirectory = path.join(
|
|
360
|
+
/* turbopackIgnore: true */
|
|
361
|
+
process.cwd(),
|
|
362
|
+
"public/articles"
|
|
363
|
+
);
|
|
322
364
|
function getReadingTime(content) {
|
|
323
365
|
return readingTime(content).text;
|
|
324
366
|
}
|
|
@@ -345,17 +387,29 @@ function sanitizeImagePath2(rawPath, articleSlug) {
|
|
|
345
387
|
const cleanPath = rawPath.replaceAll(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
346
388
|
if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) return cleanPath;
|
|
347
389
|
if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
|
|
348
|
-
|
|
390
|
+
reportArticlesError({
|
|
391
|
+
code: "unsafe-image-path",
|
|
392
|
+
message: "Rejected unsafe article image path.",
|
|
393
|
+
context: { articleSlug, path: cleanPath }
|
|
394
|
+
});
|
|
349
395
|
return null;
|
|
350
396
|
}
|
|
351
397
|
if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {
|
|
352
|
-
|
|
398
|
+
reportArticlesError({
|
|
399
|
+
code: "unsafe-image-path",
|
|
400
|
+
message: "Rejected article image path with invalid characters.",
|
|
401
|
+
context: { articleSlug, path: cleanPath }
|
|
402
|
+
});
|
|
353
403
|
return null;
|
|
354
404
|
}
|
|
355
405
|
if (cleanPath.includes("/")) {
|
|
356
406
|
const normalizedPath = path.normalize(cleanPath);
|
|
357
407
|
if (normalizedPath.startsWith("..") || normalizedPath.includes("../")) {
|
|
358
|
-
|
|
408
|
+
reportArticlesError({
|
|
409
|
+
code: "unsafe-image-path",
|
|
410
|
+
message: "Rejected article image path traversal attempt.",
|
|
411
|
+
context: { articleSlug, path: cleanPath }
|
|
412
|
+
});
|
|
359
413
|
return null;
|
|
360
414
|
}
|
|
361
415
|
return `/articles/${articleSlug}/${cleanPath}`;
|
|
@@ -374,7 +428,12 @@ function getAvailableArticleSlugs() {
|
|
|
374
428
|
if (!fs.existsSync(articlesDirectory)) return [];
|
|
375
429
|
return walkArticleDir(articlesDirectory, "");
|
|
376
430
|
} catch (error) {
|
|
377
|
-
|
|
431
|
+
reportArticlesError({
|
|
432
|
+
code: "article-directory-read-failed",
|
|
433
|
+
message: "Unable to read articles directory.",
|
|
434
|
+
error,
|
|
435
|
+
context: { directory: articlesDirectory }
|
|
436
|
+
});
|
|
378
437
|
return [];
|
|
379
438
|
}
|
|
380
439
|
}
|
|
@@ -451,10 +510,16 @@ function getArticleSummary(slug) {
|
|
|
451
510
|
howTo: parseHowToSteps(data.howTo),
|
|
452
511
|
canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
|
|
453
512
|
articleType: typeof data.articleType === "string" ? data.articleType : void 0,
|
|
454
|
-
series: typeof data.series === "string" ? data.series : void 0
|
|
513
|
+
series: typeof data.series === "string" ? data.series : void 0,
|
|
514
|
+
aiCrawl: data.aiCrawl === true
|
|
455
515
|
};
|
|
456
516
|
} catch (error) {
|
|
457
|
-
|
|
517
|
+
reportArticlesError({
|
|
518
|
+
code: "article-load-failed",
|
|
519
|
+
message: "Unable to load article summary.",
|
|
520
|
+
error,
|
|
521
|
+
context: { slug }
|
|
522
|
+
});
|
|
458
523
|
return null;
|
|
459
524
|
}
|
|
460
525
|
});
|
|
@@ -477,7 +542,12 @@ var getArticleMetadata = cache((slug) => __async(void 0, null, function* () {
|
|
|
477
542
|
}
|
|
478
543
|
return __spreadProps(__spreadValues({}, summary), { content: markdownContent, htmlContent, mdxSource, toc });
|
|
479
544
|
} catch (error) {
|
|
480
|
-
|
|
545
|
+
reportArticlesError({
|
|
546
|
+
code: "article-load-failed",
|
|
547
|
+
message: "Unable to load article metadata.",
|
|
548
|
+
error,
|
|
549
|
+
context: { slug }
|
|
550
|
+
});
|
|
481
551
|
return null;
|
|
482
552
|
}
|
|
483
553
|
}));
|
|
@@ -502,16 +572,86 @@ function getAdjacentArticles(currentSlug) {
|
|
|
502
572
|
return { previous, next };
|
|
503
573
|
});
|
|
504
574
|
}
|
|
505
|
-
function
|
|
575
|
+
function getArticleMarkdown(slug) {
|
|
576
|
+
return __async(this, null, function* () {
|
|
577
|
+
try {
|
|
578
|
+
const summary = yield getArticleSummary(slug);
|
|
579
|
+
if (!(summary == null ? void 0 : summary.aiCrawl)) return null;
|
|
580
|
+
const found = findArticleFile(slug);
|
|
581
|
+
if (!found) return null;
|
|
582
|
+
const fileContent = fs.readFileSync(found.filePath, "utf8");
|
|
583
|
+
const { content: markdownContent } = matter(fileContent);
|
|
584
|
+
return markdownContent;
|
|
585
|
+
} catch (error) {
|
|
586
|
+
reportArticlesError({
|
|
587
|
+
code: "article-markdown-load-failed",
|
|
588
|
+
message: "Unable to load article markdown.",
|
|
589
|
+
error,
|
|
590
|
+
context: { slug }
|
|
591
|
+
});
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function getArticleMarkdownResponse(slug, config) {
|
|
597
|
+
return __async(this, null, function* () {
|
|
598
|
+
const markdown = yield getArticleMarkdown(slug);
|
|
599
|
+
if (markdown === null) return new Response("Not Found", { status: 404 });
|
|
600
|
+
const article = yield getArticleMetadata(slug);
|
|
601
|
+
return new Response(markdown, {
|
|
602
|
+
headers: __spreadValues({
|
|
603
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
604
|
+
"Cache-Control": "public, max-age=3600, s-maxage=3600"
|
|
605
|
+
}, article ? getArticleAiHeaders(article, config) : {})
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function getArticleMarkdownUrl(article, config) {
|
|
610
|
+
if (article.aiCrawl !== true) return void 0;
|
|
611
|
+
const pathname = `/articles/${article.slug}.md`;
|
|
612
|
+
if (!config) return pathname;
|
|
613
|
+
return `${config.siteUrl.replace(/\/$/, "")}${pathname}`;
|
|
614
|
+
}
|
|
615
|
+
function getArticleAiHeaders(article, config) {
|
|
616
|
+
const markdownUrl = getArticleMarkdownUrl(article, config);
|
|
617
|
+
if (markdownUrl) {
|
|
618
|
+
return {
|
|
619
|
+
Link: `<${markdownUrl}>; rel="alternate"; type="text/markdown"`
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
"X-Robots-Tag": "noai, noimageai"
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function getAiRobotsTxtRules() {
|
|
627
|
+
return __async(this, null, function* () {
|
|
628
|
+
const articles = yield getAllArticles();
|
|
629
|
+
const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
|
|
630
|
+
if (blockedArticles.length === 0) return "";
|
|
631
|
+
const aiCrawlers = [
|
|
632
|
+
"GPTBot",
|
|
633
|
+
"ChatGPT-User",
|
|
634
|
+
"CCBot",
|
|
635
|
+
"ClaudeBot",
|
|
636
|
+
"Claude-User",
|
|
637
|
+
"PerplexityBot",
|
|
638
|
+
"Google-Extended"
|
|
639
|
+
];
|
|
640
|
+
const disallowRules = blockedArticles.map((article) => `Disallow: /articles/${article.slug}`).join("\n");
|
|
641
|
+
return aiCrawlers.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join("\n\n");
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function searchArticles(query, config) {
|
|
506
645
|
return __async(this, null, function* () {
|
|
507
646
|
if (!(query == null ? void 0 : query.trim())) return getAllArticles();
|
|
508
647
|
const articles = yield getAllArticles();
|
|
509
648
|
const searchTerm = query.toLowerCase().trim();
|
|
649
|
+
const includeAuthor = (config == null ? void 0 : config.showAuthor) !== false;
|
|
510
650
|
return articles.filter((article) => {
|
|
511
651
|
var _a;
|
|
512
652
|
const matchesTitle = article.title.toLowerCase().includes(searchTerm);
|
|
513
653
|
const matchesExcerpt = article.excerpt.toLowerCase().includes(searchTerm);
|
|
514
|
-
const matchesAuthor = article.author.toLowerCase().includes(searchTerm);
|
|
654
|
+
const matchesAuthor = includeAuthor && article.author.toLowerCase().includes(searchTerm);
|
|
515
655
|
const matchesCategory = article.categories.some((cat) => cat.toLowerCase().includes(searchTerm));
|
|
516
656
|
const matchesTags = (_a = article.tags) == null ? void 0 : _a.some((tag) => tag.toLowerCase().includes(searchTerm));
|
|
517
657
|
return matchesTitle || matchesExcerpt || matchesAuthor || matchesCategory || Boolean(matchesTags);
|
|
@@ -582,11 +722,13 @@ function generateArticleMetadata(slug, config) {
|
|
|
582
722
|
const canonicalUrl = (_a = article.canonicalUrl) != null ? _a : articleUrl;
|
|
583
723
|
const imageUrl = article.featuredImage ? resolveImageUrl(article.featuredImage, siteUrl) : `${siteUrl}/placeholder-logo.png`;
|
|
584
724
|
const description = (_b = article.excerpt) != null ? _b : `Read ${article.title} on ${config.siteName}.`;
|
|
725
|
+
const showAuthor = config.showAuthor !== false;
|
|
726
|
+
const markdownUrl = getArticleMarkdownUrl(article, config);
|
|
585
727
|
return {
|
|
586
728
|
title: `${article.title} | ${config.siteName}`,
|
|
587
729
|
description,
|
|
588
730
|
keywords: [...((_c = article.tags) != null ? _c : []).map((tag) => tag.toLowerCase())].join(", "),
|
|
589
|
-
openGraph: __spreadProps(__spreadValues(__spreadValues({
|
|
731
|
+
openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
|
|
590
732
|
title: article.title,
|
|
591
733
|
description,
|
|
592
734
|
url: articleUrl,
|
|
@@ -594,8 +736,7 @@ function generateArticleMetadata(slug, config) {
|
|
|
594
736
|
images: [{ url: imageUrl, width: 1200, height: 630, alt: article.title }],
|
|
595
737
|
locale: "en_US",
|
|
596
738
|
type: "article"
|
|
597
|
-
}, article.date && { publishedTime: article.date }), article.lastmod && { modifiedTime: new Date(article.lastmod).toISOString() }), {
|
|
598
|
-
authors: [article.author],
|
|
739
|
+
}, article.date && { publishedTime: article.date }), article.lastmod && { modifiedTime: new Date(article.lastmod).toISOString() }), showAuthor && { authors: [article.author] }), {
|
|
599
740
|
tags: (_d = article.tags) != null ? _d : []
|
|
600
741
|
}),
|
|
601
742
|
twitter: {
|
|
@@ -604,9 +745,13 @@ function generateArticleMetadata(slug, config) {
|
|
|
604
745
|
description,
|
|
605
746
|
images: [imageUrl]
|
|
606
747
|
},
|
|
607
|
-
alternates: {
|
|
748
|
+
alternates: __spreadValues({
|
|
608
749
|
canonical: canonicalUrl
|
|
609
|
-
},
|
|
750
|
+
}, markdownUrl && {
|
|
751
|
+
types: {
|
|
752
|
+
"text/markdown": markdownUrl
|
|
753
|
+
}
|
|
754
|
+
}),
|
|
610
755
|
robots: {
|
|
611
756
|
index: true,
|
|
612
757
|
follow: true,
|
|
@@ -618,9 +763,7 @@ function generateArticleMetadata(slug, config) {
|
|
|
618
763
|
"max-snippet": -1
|
|
619
764
|
}
|
|
620
765
|
},
|
|
621
|
-
other: __spreadProps(__spreadValues(__spreadValues({
|
|
622
|
-
"article:author": article.author
|
|
623
|
-
}, article.date && {
|
|
766
|
+
other: __spreadProps(__spreadValues(__spreadValues(__spreadValues({}, showAuthor && { "article:author": article.author }), article.date && {
|
|
624
767
|
"article:published_time": new Date(article.date).toISOString()
|
|
625
768
|
}), article.lastmod && {
|
|
626
769
|
"article:modified_time": new Date(article.lastmod).toISOString()
|
|
@@ -830,14 +973,20 @@ export {
|
|
|
830
973
|
generateCategoryMetadata,
|
|
831
974
|
generateCategoryStaticParams,
|
|
832
975
|
getAdjacentArticles,
|
|
976
|
+
getAiRobotsTxtRules,
|
|
833
977
|
getAllArticles,
|
|
834
978
|
getAllCategories,
|
|
979
|
+
getArticleAiHeaders,
|
|
980
|
+
getArticleMarkdown,
|
|
981
|
+
getArticleMarkdownResponse,
|
|
982
|
+
getArticleMarkdownUrl,
|
|
835
983
|
getArticleMetadata,
|
|
836
984
|
getArticleSitemapEntries,
|
|
837
985
|
getArticlesByCategory,
|
|
838
986
|
getAvailableArticleSlugs,
|
|
839
987
|
markdownToHtml,
|
|
840
988
|
sanitizeImagePath2 as sanitizeImagePath,
|
|
841
|
-
searchArticles
|
|
989
|
+
searchArticles,
|
|
990
|
+
setArticlesErrorHandler
|
|
842
991
|
};
|
|
843
992
|
//# sourceMappingURL=server.js.map
|