@fullstackdatasolutions/articles 1.2.3 → 1.3.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/CHANGELOG.md +40 -0
- package/README.md +313 -1
- package/dist/index.cjs +308 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +267 -16
- package/dist/index.d.ts +267 -16
- package/dist/index.js +300 -79
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +320 -11
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +179 -2
- package/dist/nextjs.d.ts +179 -2
- package/dist/nextjs.js +320 -11
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +655 -47
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +333 -12
- package/dist/server.d.ts +333 -12
- package/dist/server.js +640 -47
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticleAnswer.tsx +35 -0
- package/src/ArticleSchemas.tsx +263 -23
- package/src/AuthorArticlesPage.tsx +38 -8
- package/src/__tests__/ArticleAnswer.test.tsx +25 -0
- package/src/__tests__/ArticleSchemas.test.tsx +516 -0
- package/src/__tests__/AuthorArticlesPage.test.tsx +76 -0
- package/src/__tests__/authorUtils.test.ts +50 -0
- package/src/__tests__/markdown.test.ts +77 -1
- package/src/__tests__/nextjs.test.ts +31 -15
- package/src/__tests__/seoUtils.test.ts +279 -0
- package/src/__tests__/server-articles.test.ts +413 -1
- package/src/__tests__/validateArticles.test.ts +167 -6
- package/src/articleTypes.ts +57 -0
- package/src/articlesConfig.ts +176 -1
- package/src/authorUtils.ts +19 -1
- package/src/errorReporting.ts +1 -0
- package/src/index.ts +17 -1
- package/src/markdown.ts +100 -1
- package/src/nextjs.ts +7 -4
- package/src/seoUtils.ts +247 -26
- package/src/server-articles.ts +375 -24
- package/src/server.ts +35 -4
- package/src/validateArticles.ts +157 -12
package/dist/server.cjs
CHANGED
|
@@ -79,16 +79,21 @@ var __async = (__this, __arguments, generator) => {
|
|
|
79
79
|
// src/server.ts
|
|
80
80
|
var server_exports = {};
|
|
81
81
|
__export(server_exports, {
|
|
82
|
+
AI_CRAWLERS: () => AI_CRAWLERS,
|
|
83
|
+
ArticleAnswer: () => ArticleAnswer,
|
|
82
84
|
ArticleContent: () => ArticleContent,
|
|
83
85
|
ArticleTOC: () => ArticleTOC,
|
|
84
86
|
buildArticleBreadcrumbs: () => buildArticleBreadcrumbs,
|
|
85
87
|
buildAuthorBreadcrumbs: () => buildAuthorBreadcrumbs,
|
|
86
88
|
buildCategoryBreadcrumbs: () => buildCategoryBreadcrumbs,
|
|
89
|
+
buildMarkdownTwinHeader: () => buildMarkdownTwinHeader,
|
|
87
90
|
buildPageUrl: () => buildPageUrl,
|
|
88
91
|
buildPaginationLinks: () => buildPaginationLinks,
|
|
89
92
|
categoryToSlug: () => categoryToSlug,
|
|
93
|
+
deriveFaqFromHeadings: () => deriveFaqFromHeadings,
|
|
90
94
|
emitArticleEvent: () => emitArticleEvent,
|
|
91
95
|
extractToc: () => extractToc,
|
|
96
|
+
formatPageTitle: () => formatPageTitle,
|
|
92
97
|
generateArticleMetadata: () => generateArticleMetadata,
|
|
93
98
|
generateArticleStaticParams: () => generateArticleStaticParams,
|
|
94
99
|
generateArticlesIndexMetadata: () => generateArticlesIndexMetadata,
|
|
@@ -100,6 +105,8 @@ __export(server_exports, {
|
|
|
100
105
|
generateCategoryPageMetadata: () => generateCategoryPageMetadata,
|
|
101
106
|
generateCategoryStaticParams: () => generateCategoryStaticParams,
|
|
102
107
|
generateListingPageStaticParams: () => generateListingPageStaticParams,
|
|
108
|
+
generateLlmsFullTxt: () => generateLlmsFullTxt,
|
|
109
|
+
generateLlmsTxt: () => generateLlmsTxt,
|
|
103
110
|
generateRssFeed: () => generateRssFeed,
|
|
104
111
|
generateSeriesMetadata: () => generateSeriesMetadata,
|
|
105
112
|
generateSeriesStaticParams: () => generateSeriesStaticParams,
|
|
@@ -120,16 +127,24 @@ __export(server_exports, {
|
|
|
120
127
|
getArticlesByCategory: () => getArticlesByCategory,
|
|
121
128
|
getArticlesBySeries: () => getArticlesBySeries,
|
|
122
129
|
getAuthorBySlug: () => getAuthorBySlug,
|
|
130
|
+
getAuthorMarkdown: () => getAuthorMarkdown,
|
|
123
131
|
getAvailableArticleSlugs: () => getAvailableArticleSlugs,
|
|
124
132
|
getBreadcrumbsConfig: () => getBreadcrumbsConfig,
|
|
133
|
+
getCategoryMarkdown: () => getCategoryMarkdown,
|
|
125
134
|
getContentSlotBoundaries: () => getContentSlotBoundaries,
|
|
135
|
+
getMarkdownTwinResponse: () => getMarkdownTwinResponse,
|
|
136
|
+
getOrganizationId: () => getOrganizationId,
|
|
126
137
|
getPath: () => getPath,
|
|
127
138
|
getPathArticles: () => getPathArticles,
|
|
139
|
+
getPersonId: () => getPersonId,
|
|
128
140
|
getRelatedArticlesByCategory: () => getRelatedArticlesByCategory,
|
|
129
141
|
getRelatedContent: () => getRelatedContent,
|
|
142
|
+
getSeriesMarkdown: () => getSeriesMarkdown,
|
|
130
143
|
getTotalPages: () => getTotalPages,
|
|
144
|
+
getWebSiteId: () => getWebSiteId,
|
|
131
145
|
isPageOutOfRange: () => isPageOutOfRange,
|
|
132
146
|
markdownToHtml: () => markdownToHtml,
|
|
147
|
+
matchAiCrawler: () => matchAiCrawler,
|
|
133
148
|
paginateArticles: () => paginateArticles,
|
|
134
149
|
parsePageParam: () => parsePageParam,
|
|
135
150
|
resolveAuthorAvatar: () => resolveAuthorAvatar,
|
|
@@ -524,6 +539,64 @@ function extractToc(markdown) {
|
|
|
524
539
|
return headings;
|
|
525
540
|
});
|
|
526
541
|
}
|
|
542
|
+
var QUESTION_OPENERS = /^(what|how|why|when|where|who|which|can|should|does|do|is|are|will|would|must)\b/i;
|
|
543
|
+
function deriveFaqFromHeadings(markdown) {
|
|
544
|
+
const items = [];
|
|
545
|
+
const collector = createFaqCollector(items);
|
|
546
|
+
let inFence = false;
|
|
547
|
+
for (const line of markdown.split("\n")) {
|
|
548
|
+
if (FENCE.test(line)) {
|
|
549
|
+
inFence = !inFence;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (inFence) continue;
|
|
553
|
+
collector.consume(line);
|
|
554
|
+
}
|
|
555
|
+
collector.flush();
|
|
556
|
+
return items;
|
|
557
|
+
}
|
|
558
|
+
var FENCE = /^\s*(```|~~~)/;
|
|
559
|
+
var ANY_HEADING = /^#{1,6}\s/;
|
|
560
|
+
function isSpace(char) {
|
|
561
|
+
return char === " " || char === " ";
|
|
562
|
+
}
|
|
563
|
+
function trimTrailingHashes(value) {
|
|
564
|
+
let end = value.length;
|
|
565
|
+
while (end > 0 && value[end - 1] === "#") end--;
|
|
566
|
+
return value.slice(0, end).trimEnd();
|
|
567
|
+
}
|
|
568
|
+
function readQuestionHeading(line) {
|
|
569
|
+
if (!line.startsWith("##") || line.startsWith("###")) return null;
|
|
570
|
+
if (!isSpace(line[2])) return null;
|
|
571
|
+
const text = trimTrailingHashes(line.slice(3).trim());
|
|
572
|
+
if (!text.endsWith("?") || !QUESTION_OPENERS.test(text)) return null;
|
|
573
|
+
return text;
|
|
574
|
+
}
|
|
575
|
+
function createFaqCollector(items) {
|
|
576
|
+
let pending = null;
|
|
577
|
+
let buffer = [];
|
|
578
|
+
const flush = () => {
|
|
579
|
+
if (pending && buffer.length > 0) {
|
|
580
|
+
items.push({ question: pending, answer: buffer.join(" ").trim() });
|
|
581
|
+
}
|
|
582
|
+
pending = null;
|
|
583
|
+
buffer = [];
|
|
584
|
+
};
|
|
585
|
+
const consume = (line) => {
|
|
586
|
+
if (ANY_HEADING.test(line)) {
|
|
587
|
+
flush();
|
|
588
|
+
pending = readQuestionHeading(line);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (!pending) return;
|
|
592
|
+
if (line.trim() === "") {
|
|
593
|
+
if (buffer.length > 0) flush();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
buffer.push(line.trim());
|
|
597
|
+
};
|
|
598
|
+
return { consume, flush };
|
|
599
|
+
}
|
|
527
600
|
|
|
528
601
|
// src/server-articles.ts
|
|
529
602
|
var articlesDirectory = import_node_path.default.join(
|
|
@@ -656,6 +729,58 @@ function parseHowToSteps(raw) {
|
|
|
656
729
|
function parseOptionalString(raw) {
|
|
657
730
|
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
|
|
658
731
|
}
|
|
732
|
+
function deriveFaq(markdownContent, config) {
|
|
733
|
+
if ((config == null ? void 0 : config.deriveFaqFromHeadings) !== true) return void 0;
|
|
734
|
+
const derived = deriveFaqFromHeadings(markdownContent);
|
|
735
|
+
return derived.length ? derived : void 0;
|
|
736
|
+
}
|
|
737
|
+
function resolveLastmod(rawLastmod, rawDate, filePath, config) {
|
|
738
|
+
const declared = parseDateField(rawLastmod);
|
|
739
|
+
if (declared) return declared;
|
|
740
|
+
if ((config == null ? void 0 : config.lastmodFallback) !== "fileMtime") return void 0;
|
|
741
|
+
try {
|
|
742
|
+
return parseDateField(import_node_fs.default.statSync(filePath).mtime);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
return parseDateField(rawDate);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function resolveAiCrawl(raw, config) {
|
|
748
|
+
if (typeof raw === "boolean") return raw;
|
|
749
|
+
return (config == null ? void 0 : config.aiCrawlDefault) === true;
|
|
750
|
+
}
|
|
751
|
+
function parseEntityReferences(raw, config) {
|
|
752
|
+
if (!Array.isArray(raw)) return void 0;
|
|
753
|
+
const items = raw.map((item) => {
|
|
754
|
+
var _a, _b;
|
|
755
|
+
if (typeof item === "string") {
|
|
756
|
+
const key = item.trim();
|
|
757
|
+
if (!key) return null;
|
|
758
|
+
return (_b = (_a = config == null ? void 0 : config.entities) == null ? void 0 : _a[key]) != null ? _b : { name: key };
|
|
759
|
+
}
|
|
760
|
+
if (typeof item === "object" && item !== null && typeof item.name === "string") {
|
|
761
|
+
const entity = item;
|
|
762
|
+
const name = entity.name.trim();
|
|
763
|
+
if (!name) return null;
|
|
764
|
+
return entity.sameAs ? { name, sameAs: entity.sameAs } : { name };
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}).filter((item) => item !== null);
|
|
768
|
+
return items.length ? items : void 0;
|
|
769
|
+
}
|
|
770
|
+
function parseCitations(raw) {
|
|
771
|
+
if (!Array.isArray(raw)) return void 0;
|
|
772
|
+
const items = raw.map((item) => {
|
|
773
|
+
if (typeof item === "string") return item.trim() ? { name: item.trim() } : null;
|
|
774
|
+
if (typeof item === "object" && item !== null && typeof item.name === "string") {
|
|
775
|
+
const citation = item;
|
|
776
|
+
const name = citation.name.trim();
|
|
777
|
+
if (!name) return null;
|
|
778
|
+
return citation.url ? { name, url: citation.url } : { name };
|
|
779
|
+
}
|
|
780
|
+
return null;
|
|
781
|
+
}).filter((item) => item !== null);
|
|
782
|
+
return items.length ? items : void 0;
|
|
783
|
+
}
|
|
659
784
|
function parseSeriesOrder(raw) {
|
|
660
785
|
return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
|
|
661
786
|
}
|
|
@@ -732,6 +857,7 @@ function getAllAuthors(config) {
|
|
|
732
857
|
}
|
|
733
858
|
function getArticleSummary(slug, config) {
|
|
734
859
|
return __async(this, null, function* () {
|
|
860
|
+
var _a;
|
|
735
861
|
try {
|
|
736
862
|
const found = findArticleFile(slug);
|
|
737
863
|
if (!found) return null;
|
|
@@ -748,7 +874,7 @@ function getArticleSummary(slug, config) {
|
|
|
748
874
|
title: data.title || slug.replaceAll("-", " "),
|
|
749
875
|
excerpt: data.excerpt || "",
|
|
750
876
|
date: parseDateField(data.date),
|
|
751
|
-
lastmod:
|
|
877
|
+
lastmod: resolveLastmod(data.lastmod, data.date, found.filePath, config),
|
|
752
878
|
author,
|
|
753
879
|
authors,
|
|
754
880
|
authorSlug: primaryAuthorProfile == null ? void 0 : primaryAuthorProfile.slug,
|
|
@@ -761,14 +887,17 @@ function getArticleSummary(slug, config) {
|
|
|
761
887
|
tags: data.tags || [],
|
|
762
888
|
contentType: found.contentType,
|
|
763
889
|
draft: data.draft === true,
|
|
764
|
-
faq: parseFaqItems(data.faq),
|
|
890
|
+
faq: (_a = parseFaqItems(data.faq)) != null ? _a : deriveFaq(markdownContent, config),
|
|
765
891
|
howTo: parseHowToSteps(data.howTo),
|
|
892
|
+
answer: parseOptionalString(data.answer),
|
|
893
|
+
about: parseEntityReferences(data.about, config),
|
|
894
|
+
citation: parseCitations(data.citation),
|
|
766
895
|
canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
|
|
767
896
|
articleType: typeof data.articleType === "string" ? data.articleType : void 0,
|
|
768
897
|
series: typeof data.series === "string" ? data.series : void 0,
|
|
769
898
|
seriesSlug: parseOptionalString(data.seriesSlug),
|
|
770
899
|
seriesOrder: parseSeriesOrder(data.seriesOrder),
|
|
771
|
-
aiCrawl: data.aiCrawl
|
|
900
|
+
aiCrawl: resolveAiCrawl(data.aiCrawl, config),
|
|
772
901
|
searchTitle: parseOptionalString(data.searchTitle),
|
|
773
902
|
searchDescription: parseOptionalString(data.searchDescription),
|
|
774
903
|
socialTitle: parseOptionalString(data.socialTitle),
|
|
@@ -837,10 +966,10 @@ function getAdjacentArticles(currentSlug) {
|
|
|
837
966
|
return { previous, next };
|
|
838
967
|
});
|
|
839
968
|
}
|
|
840
|
-
function getArticleMarkdown(slug) {
|
|
969
|
+
function getArticleMarkdown(slug, config) {
|
|
841
970
|
return __async(this, null, function* () {
|
|
842
971
|
try {
|
|
843
|
-
const summary = yield getArticleSummary(slug);
|
|
972
|
+
const summary = yield getArticleSummary(slug, config);
|
|
844
973
|
if (!(summary == null ? void 0 : summary.aiCrawl)) return null;
|
|
845
974
|
const found = findArticleFile(slug);
|
|
846
975
|
if (!found) return null;
|
|
@@ -858,12 +987,180 @@ function getArticleMarkdown(slug) {
|
|
|
858
987
|
}
|
|
859
988
|
});
|
|
860
989
|
}
|
|
861
|
-
|
|
990
|
+
var AI_CRAWLERS = [
|
|
991
|
+
"GPTBot",
|
|
992
|
+
"ChatGPT-User",
|
|
993
|
+
"OAI-SearchBot",
|
|
994
|
+
"CCBot",
|
|
995
|
+
"ClaudeBot",
|
|
996
|
+
"Claude-User",
|
|
997
|
+
"Claude-SearchBot",
|
|
998
|
+
"anthropic-ai",
|
|
999
|
+
"PerplexityBot",
|
|
1000
|
+
"Perplexity-User",
|
|
1001
|
+
"Google-Extended",
|
|
1002
|
+
"Applebot-Extended",
|
|
1003
|
+
"Bytespider",
|
|
1004
|
+
"Amazonbot",
|
|
1005
|
+
"meta-externalagent",
|
|
1006
|
+
"cohere-ai",
|
|
1007
|
+
"DuckAssistBot",
|
|
1008
|
+
"MistralAI-User"
|
|
1009
|
+
];
|
|
1010
|
+
function matchAiCrawler(userAgent) {
|
|
1011
|
+
var _a;
|
|
1012
|
+
if (!userAgent) return null;
|
|
1013
|
+
const normalized = userAgent.toLowerCase();
|
|
1014
|
+
return (_a = AI_CRAWLERS.find((crawler) => normalized.includes(crawler.toLowerCase()))) != null ? _a : null;
|
|
1015
|
+
}
|
|
1016
|
+
function buildMarkdownTwinHeader(article, config, body) {
|
|
1017
|
+
var _a, _b;
|
|
1018
|
+
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1019
|
+
const firstLine = (_b = (_a = body.trimStart().split("\n", 1)[0]) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
1020
|
+
const bodyRepeatsTitle = firstLine.toLowerCase() === `# ${article.title}`.toLowerCase();
|
|
1021
|
+
const facts = [
|
|
1022
|
+
`Source: ${siteUrl}/articles/${article.slug}`,
|
|
1023
|
+
article.date ? `Published: ${article.date}` : "",
|
|
1024
|
+
article.lastmod ? `Updated: ${article.lastmod}` : "",
|
|
1025
|
+
config.showAuthor !== false && article.author ? `Author: ${article.author}` : "",
|
|
1026
|
+
`Site: ${config.siteName}`
|
|
1027
|
+
].filter(Boolean);
|
|
1028
|
+
const blocks = [
|
|
1029
|
+
bodyRepeatsTitle ? "" : `# ${article.title}`,
|
|
1030
|
+
article.excerpt ? `> ${article.excerpt}` : "",
|
|
1031
|
+
facts.join("\n"),
|
|
1032
|
+
article.answer ? `**Short answer:** ${article.answer}` : "",
|
|
1033
|
+
"---"
|
|
1034
|
+
].filter((block) => block !== "");
|
|
1035
|
+
return `${blocks.join("\n\n")}
|
|
1036
|
+
|
|
1037
|
+
`;
|
|
1038
|
+
}
|
|
1039
|
+
function reportAiCrawl(slug, config, headers) {
|
|
1040
|
+
var _a, _b;
|
|
1041
|
+
if (!config.onAiCrawl) return;
|
|
1042
|
+
const userAgent = (_a = headers == null ? void 0 : headers.get("user-agent")) != null ? _a : "";
|
|
1043
|
+
try {
|
|
1044
|
+
config.onAiCrawl({ slug, crawler: (_b = matchAiCrawler(userAgent)) != null ? _b : "unknown", userAgent });
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
reportArticlesError({
|
|
1047
|
+
code: "ai-crawl-handler-failed",
|
|
1048
|
+
message: "onAiCrawl handler threw.",
|
|
1049
|
+
error,
|
|
1050
|
+
context: { slug }
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function buildListingMarkdown(heading, intro, articles, config) {
|
|
1055
|
+
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1056
|
+
const crawlable = articles.filter((article) => article.aiCrawl === true);
|
|
1057
|
+
const entries = crawlable.map((article) => {
|
|
1058
|
+
var _a;
|
|
1059
|
+
const summary = (_a = article.answer) != null ? _a : article.excerpt;
|
|
1060
|
+
const line = `- [${article.title}](${siteUrl}/articles/${article.slug}.md)`;
|
|
1061
|
+
return summary ? `${line}: ${summary}` : line;
|
|
1062
|
+
});
|
|
1063
|
+
return [
|
|
1064
|
+
`# ${heading}`,
|
|
1065
|
+
"",
|
|
1066
|
+
...intro.flatMap((line) => [line, ""]),
|
|
1067
|
+
`Source: ${siteUrl}`,
|
|
1068
|
+
`Site: ${config.siteName}`,
|
|
1069
|
+
"",
|
|
1070
|
+
"---",
|
|
1071
|
+
"",
|
|
1072
|
+
...entries.length > 0 ? entries : ["_No articles available._"],
|
|
1073
|
+
""
|
|
1074
|
+
].join("\n");
|
|
1075
|
+
}
|
|
1076
|
+
function getCategoryMarkdown(categorySlug, config) {
|
|
1077
|
+
return __async(this, null, function* () {
|
|
1078
|
+
var _a;
|
|
1079
|
+
const articles = yield getArticlesByCategory(categorySlug, config);
|
|
1080
|
+
if (articles.length === 0) return null;
|
|
1081
|
+
const name = (_a = articles[0].categories.find((c) => categoryToSlug(c) === categorySlug)) != null ? _a : categorySlug;
|
|
1082
|
+
const description = resolveCategoryDescription(categorySlug, config);
|
|
1083
|
+
return buildListingMarkdown(name, description ? [description] : [], articles, config);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
function resolveCategoryDescription(categorySlug, config) {
|
|
1087
|
+
var _a, _b;
|
|
1088
|
+
const entry = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
|
|
1089
|
+
if (!entry) return void 0;
|
|
1090
|
+
return typeof entry === "string" ? entry : (_b = entry.long) != null ? _b : entry.short;
|
|
1091
|
+
}
|
|
1092
|
+
function getAuthorMarkdown(authorSlug, config) {
|
|
1093
|
+
return __async(this, null, function* () {
|
|
1094
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1095
|
+
const author = getAuthorBySlug(authorSlug, config);
|
|
1096
|
+
if (!author) return null;
|
|
1097
|
+
const articles = yield getArticlesByAuthor(authorSlug, config);
|
|
1098
|
+
const intro = [
|
|
1099
|
+
(_a = author.promise) != null ? _a : "",
|
|
1100
|
+
(_b = author.bio) != null ? _b : "",
|
|
1101
|
+
...((_c = author.servesWho) == null ? void 0 : _c.length) ? [`Writes for: ${author.servesWho.join(", ")}`] : [],
|
|
1102
|
+
...((_d = author.knowsAbout) == null ? void 0 : _d.length) ? [`Writes about: ${author.knowsAbout.join(", ")}`] : [],
|
|
1103
|
+
...((_e = author.credentials) == null ? void 0 : _e.length) ? ["## Stated experience", ...author.credentials.map((item) => `- ${item}`)] : [],
|
|
1104
|
+
...((_f = author.proof) == null ? void 0 : _f.length) ? [
|
|
1105
|
+
"## Proof points",
|
|
1106
|
+
...author.proof.map(
|
|
1107
|
+
(item) => item.url ? `- [${item.claim}](${item.url})` : `- ${item.claim}`
|
|
1108
|
+
)
|
|
1109
|
+
] : [],
|
|
1110
|
+
...((_g = author.originStory) == null ? void 0 : _g.length) ? [
|
|
1111
|
+
"## Background",
|
|
1112
|
+
...author.originStory.flatMap((section) => [
|
|
1113
|
+
...section.heading ? [`### ${section.heading}`] : [],
|
|
1114
|
+
...section.paragraphs
|
|
1115
|
+
])
|
|
1116
|
+
] : []
|
|
1117
|
+
].filter((line) => line.trim() !== "");
|
|
1118
|
+
return buildListingMarkdown(author.name, intro, articles, config);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
function getSeriesMarkdown(seriesSlug, config) {
|
|
1122
|
+
return __async(this, null, function* () {
|
|
1123
|
+
var _a;
|
|
1124
|
+
const articles = yield getArticlesBySeries(seriesSlug, config);
|
|
1125
|
+
if (articles.length === 0) return null;
|
|
1126
|
+
const name = (_a = articles[0].series) != null ? _a : seriesSlug;
|
|
1127
|
+
return buildListingMarkdown(name, [], articles, config);
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function getMarkdownTwinResponse(slug, config, options) {
|
|
1131
|
+
return __async(this, null, function* () {
|
|
1132
|
+
const listing = yield resolveListingMarkdown(slug, config);
|
|
1133
|
+
if (listing !== void 0) {
|
|
1134
|
+
if (listing === null) return new Response("Not Found", { status: 404 });
|
|
1135
|
+
reportAiCrawl(slug, config, options == null ? void 0 : options.headers);
|
|
1136
|
+
return new Response(listing, { headers: LISTING_MARKDOWN_HEADERS });
|
|
1137
|
+
}
|
|
1138
|
+
return getArticleMarkdownResponse(slug, config, options);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
var LISTING_MARKDOWN_HEADERS = {
|
|
1142
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
1143
|
+
"Cache-Control": "public, max-age=3600, s-maxage=3600"
|
|
1144
|
+
};
|
|
1145
|
+
function resolveListingMarkdown(slug, config) {
|
|
1146
|
+
return __async(this, null, function* () {
|
|
1147
|
+
const [prefix, ...rest] = slug.split("/");
|
|
1148
|
+
const key = rest.join("/");
|
|
1149
|
+
if (!key) return void 0;
|
|
1150
|
+
if (prefix === "category") return getCategoryMarkdown(key, config);
|
|
1151
|
+
if (prefix === "authors") return getAuthorMarkdown(key, config);
|
|
1152
|
+
if (prefix === "series") return getSeriesMarkdown(key, config);
|
|
1153
|
+
return void 0;
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function getArticleMarkdownResponse(slug, config, options) {
|
|
862
1157
|
return __async(this, null, function* () {
|
|
863
|
-
const markdown = yield getArticleMarkdown(slug);
|
|
1158
|
+
const markdown = yield getArticleMarkdown(slug, config);
|
|
864
1159
|
if (markdown === null) return new Response("Not Found", { status: 404 });
|
|
865
|
-
const article = yield getArticleMetadata(slug);
|
|
866
|
-
|
|
1160
|
+
const article = yield getArticleMetadata(slug, config);
|
|
1161
|
+
reportAiCrawl(slug, config, options == null ? void 0 : options.headers);
|
|
1162
|
+
const body = article && config.markdownTwinHeader !== false ? `${buildMarkdownTwinHeader(article, config, markdown)}${markdown.trimStart()}` : markdown;
|
|
1163
|
+
return new Response(body, {
|
|
867
1164
|
headers: __spreadValues({
|
|
868
1165
|
"Content-Type": "text/markdown; charset=utf-8",
|
|
869
1166
|
"Cache-Control": "public, max-age=3600, s-maxage=3600"
|
|
@@ -888,22 +1185,15 @@ function getArticleAiHeaders(article, config) {
|
|
|
888
1185
|
"X-Robots-Tag": "noai, noimageai"
|
|
889
1186
|
};
|
|
890
1187
|
}
|
|
891
|
-
function getAiRobotsTxtRules() {
|
|
1188
|
+
function getAiRobotsTxtRules(config) {
|
|
892
1189
|
return __async(this, null, function* () {
|
|
893
|
-
const articles = yield getAllArticles();
|
|
1190
|
+
const articles = yield getAllArticles(config);
|
|
894
1191
|
const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
|
|
895
1192
|
if (blockedArticles.length === 0) return "";
|
|
896
|
-
const aiCrawlers = [
|
|
897
|
-
"GPTBot",
|
|
898
|
-
"ChatGPT-User",
|
|
899
|
-
"CCBot",
|
|
900
|
-
"ClaudeBot",
|
|
901
|
-
"Claude-User",
|
|
902
|
-
"PerplexityBot",
|
|
903
|
-
"Google-Extended"
|
|
904
|
-
];
|
|
905
1193
|
const disallowRules = blockedArticles.map((article) => `Disallow: /articles/${article.slug}`).join("\n");
|
|
906
|
-
return
|
|
1194
|
+
return AI_CRAWLERS.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join(
|
|
1195
|
+
"\n\n"
|
|
1196
|
+
);
|
|
907
1197
|
});
|
|
908
1198
|
}
|
|
909
1199
|
function searchArticles(query, config) {
|
|
@@ -1043,6 +1333,7 @@ function getRelatedContent(article, config, limit = 3) {
|
|
|
1043
1333
|
}
|
|
1044
1334
|
|
|
1045
1335
|
// src/articlesConfig.ts
|
|
1336
|
+
var DEFAULT_PAGE_SIZE = 6;
|
|
1046
1337
|
function breadcrumbsAreEnabled(config) {
|
|
1047
1338
|
var _a;
|
|
1048
1339
|
return config.breadcrumbs !== false && ((_a = config.breadcrumbs) == null ? void 0 : _a.show) !== false;
|
|
@@ -1052,6 +1343,20 @@ function getBreadcrumbsConfig(config) {
|
|
|
1052
1343
|
if (config.breadcrumbs === false) return {};
|
|
1053
1344
|
return (_a = config.breadcrumbs) != null ? _a : {};
|
|
1054
1345
|
}
|
|
1346
|
+
function getOrganizationId(config) {
|
|
1347
|
+
return `${config.siteUrl.replace(/\/$/, "")}/#organization`;
|
|
1348
|
+
}
|
|
1349
|
+
function getWebSiteId(config) {
|
|
1350
|
+
return `${config.siteUrl.replace(/\/$/, "")}/#website`;
|
|
1351
|
+
}
|
|
1352
|
+
function getPersonId(authorUrl) {
|
|
1353
|
+
return `${authorUrl.replace(/\/$/, "")}#person`;
|
|
1354
|
+
}
|
|
1355
|
+
var DEFAULT_TITLE_TEMPLATE = "{title} | {siteName}";
|
|
1356
|
+
function formatPageTitle(title, config) {
|
|
1357
|
+
var _a;
|
|
1358
|
+
return ((_a = config.titleTemplate) != null ? _a : DEFAULT_TITLE_TEMPLATE).replaceAll("{title}", title).replaceAll("{siteName}", config.siteName);
|
|
1359
|
+
}
|
|
1055
1360
|
|
|
1056
1361
|
// src/pagination.ts
|
|
1057
1362
|
function getTotalPages(totalCount, pageSize) {
|
|
@@ -1099,10 +1404,11 @@ function isPageOutOfRange(page, totalPages) {
|
|
|
1099
1404
|
function escapeXml(str) {
|
|
1100
1405
|
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1101
1406
|
}
|
|
1102
|
-
function generateRssFeed(articles, config) {
|
|
1103
|
-
var _a;
|
|
1407
|
+
function generateRssFeed(articles, config, options) {
|
|
1408
|
+
var _a, _b;
|
|
1104
1409
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1105
1410
|
const showAuthor = config.showAuthor !== false;
|
|
1411
|
+
const fullContent = (options == null ? void 0 : options.fullContent) === true;
|
|
1106
1412
|
const items = articles.map((article) => {
|
|
1107
1413
|
const url = `${siteUrl}/articles/${article.slug}`;
|
|
1108
1414
|
const pubDate = article.date ? new Date(article.date).toUTCString() : "";
|
|
@@ -1114,6 +1420,7 @@ function generateRssFeed(articles, config) {
|
|
|
1114
1420
|
` <guid isPermaLink="true">${url}</guid>`,
|
|
1115
1421
|
pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
|
|
1116
1422
|
article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : "",
|
|
1423
|
+
fullContent && article.htmlContent ? ` <content:encoded><![CDATA[${article.htmlContent}]]></content:encoded>` : "",
|
|
1117
1424
|
showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : "",
|
|
1118
1425
|
article.category ? ` <category><![CDATA[${article.category}]]></category>` : "",
|
|
1119
1426
|
imageUrl ? ` <media:content url="${imageUrl}" medium="image" width="1200" height="630"/>` : "",
|
|
@@ -1122,17 +1429,82 @@ function generateRssFeed(articles, config) {
|
|
|
1122
1429
|
}).join("\n");
|
|
1123
1430
|
const description = (_a = config.description) != null ? _a : `${config.siteName} articles`;
|
|
1124
1431
|
return `<?xml version="1.0" encoding="UTF-8" ?>
|
|
1125
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
|
1432
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
1126
1433
|
<channel>
|
|
1127
1434
|
<title><![CDATA[${config.siteName}]]></title>
|
|
1128
1435
|
<link>${siteUrl}/articles</link>
|
|
1129
1436
|
<description><![CDATA[${description}]]></description>
|
|
1130
|
-
<language
|
|
1437
|
+
<language>${(_b = config.language) != null ? _b : "en"}</language>
|
|
1131
1438
|
<atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
|
|
1132
1439
|
${items}
|
|
1133
1440
|
</channel>
|
|
1134
1441
|
</rss>`;
|
|
1135
1442
|
}
|
|
1443
|
+
function buildLlmsHeader(config) {
|
|
1444
|
+
var _a;
|
|
1445
|
+
const summary = (_a = config.description) != null ? _a : `${config.siteName} articles`;
|
|
1446
|
+
return [`# ${config.siteName}`, "", `> ${summary}`, ""];
|
|
1447
|
+
}
|
|
1448
|
+
function generateLlmsTxt(articles, config) {
|
|
1449
|
+
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1450
|
+
const crawlable = articles.filter((article) => article.aiCrawl === true);
|
|
1451
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1452
|
+
for (const article of crawlable) {
|
|
1453
|
+
const category = article.category || "Articles";
|
|
1454
|
+
const existing = byCategory.get(category);
|
|
1455
|
+
if (existing) existing.push(article);
|
|
1456
|
+
else byCategory.set(category, [article]);
|
|
1457
|
+
}
|
|
1458
|
+
const sections = [...byCategory.entries()].map(([category, categoryArticles]) => {
|
|
1459
|
+
const lines = categoryArticles.map((article) => {
|
|
1460
|
+
const url = `${siteUrl}/articles/${article.slug}.md`;
|
|
1461
|
+
const summary = article.excerpt ? `: ${article.excerpt}` : "";
|
|
1462
|
+
return `- [${article.title}](${url})${summary}`;
|
|
1463
|
+
});
|
|
1464
|
+
return [`## ${category}`, "", ...lines].join("\n");
|
|
1465
|
+
});
|
|
1466
|
+
return [
|
|
1467
|
+
...buildLlmsHeader(config),
|
|
1468
|
+
...sections.length > 0 ? sections : ["## Articles", "", "_No articles available._"],
|
|
1469
|
+
...buildLlmsListingSection(crawlable, config),
|
|
1470
|
+
""
|
|
1471
|
+
].join("\n");
|
|
1472
|
+
}
|
|
1473
|
+
function buildLlmsListingSection(articles, config) {
|
|
1474
|
+
var _a;
|
|
1475
|
+
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1476
|
+
const categories = [...new Set(articles.flatMap((a) => {
|
|
1477
|
+
var _a2;
|
|
1478
|
+
return (_a2 = a.categories) != null ? _a2 : [];
|
|
1479
|
+
}))].filter(Boolean);
|
|
1480
|
+
const series = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
|
|
1481
|
+
const authors = Object.values((_a = config.authors) != null ? _a : {});
|
|
1482
|
+
const lines = [
|
|
1483
|
+
...categories.map(
|
|
1484
|
+
(name) => `- [${name}](${siteUrl}/articles/category/${categoryToSlug(name)}.md)`
|
|
1485
|
+
),
|
|
1486
|
+
...config.showAuthorPage === false ? [] : authors.map((a) => `- [${a.name}](${siteUrl}/articles/authors/${a.slug}.md)`),
|
|
1487
|
+
...series.map((slug) => `- [${slug}](${siteUrl}/articles/series/${slug}.md)`)
|
|
1488
|
+
];
|
|
1489
|
+
if (lines.length === 0) return [];
|
|
1490
|
+
return ["## Collections", "", ...lines];
|
|
1491
|
+
}
|
|
1492
|
+
function generateLlmsFullTxt(articles, config) {
|
|
1493
|
+
return __async(this, null, function* () {
|
|
1494
|
+
const crawlable = articles.filter((article) => article.aiCrawl === true);
|
|
1495
|
+
const documents = yield Promise.all(
|
|
1496
|
+
crawlable.map((article) => __async(this, null, function* () {
|
|
1497
|
+
const body = yield getArticleMarkdown(article.slug, config);
|
|
1498
|
+
if (body === null) return null;
|
|
1499
|
+
return `${buildMarkdownTwinHeader(article, config, body)}${body.trimStart()}`;
|
|
1500
|
+
}))
|
|
1501
|
+
);
|
|
1502
|
+
return [
|
|
1503
|
+
...buildLlmsHeader(config),
|
|
1504
|
+
...documents.filter((doc) => doc !== null)
|
|
1505
|
+
].join("\n");
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1136
1508
|
function generateArticleStaticParams() {
|
|
1137
1509
|
return getAvailableArticleSlugs().map((slug) => ({ slug }));
|
|
1138
1510
|
}
|
|
@@ -1198,7 +1570,7 @@ function generateArticleMetadata(slug, config) {
|
|
|
1198
1570
|
const markdownUrl = getArticleMarkdownUrl(article, config);
|
|
1199
1571
|
const authorNames = getArticleAuthors(article, config).map((author) => author.name);
|
|
1200
1572
|
return {
|
|
1201
|
-
title:
|
|
1573
|
+
title: formatPageTitle(search.title, config),
|
|
1202
1574
|
description,
|
|
1203
1575
|
keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
|
|
1204
1576
|
openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
|
|
@@ -1252,7 +1624,7 @@ function generateArticlesIndexMetadata(config) {
|
|
|
1252
1624
|
var _a, _b;
|
|
1253
1625
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1254
1626
|
const indexUrl = `${siteUrl}/articles`;
|
|
1255
|
-
const title =
|
|
1627
|
+
const title = formatPageTitle("Articles", config);
|
|
1256
1628
|
const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
|
|
1257
1629
|
return {
|
|
1258
1630
|
title,
|
|
@@ -1300,7 +1672,7 @@ function generateCategoryMetadata(categorySlug, config) {
|
|
|
1300
1672
|
const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
|
|
1301
1673
|
const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
|
|
1302
1674
|
const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
|
|
1303
|
-
const title = `${categoryName} Articles
|
|
1675
|
+
const title = formatPageTitle(`${categoryName} Articles`, config);
|
|
1304
1676
|
return {
|
|
1305
1677
|
title,
|
|
1306
1678
|
description,
|
|
@@ -1344,7 +1716,7 @@ function generateSeriesMetadata(seriesSlug, config) {
|
|
|
1344
1716
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1345
1717
|
const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
|
|
1346
1718
|
const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
|
|
1347
|
-
const title = `${seriesName} Series
|
|
1719
|
+
const title = formatPageTitle(`${seriesName} Series`, config);
|
|
1348
1720
|
return {
|
|
1349
1721
|
title,
|
|
1350
1722
|
description,
|
|
@@ -1386,7 +1758,7 @@ function generateAuthorMetadata(authorSlug, config) {
|
|
|
1386
1758
|
if (!author || config.showAuthorPage === false) return { title: "Author Not Found" };
|
|
1387
1759
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1388
1760
|
const authorUrl = (_a = author.url) != null ? _a : `${siteUrl}/articles/authors/${author.slug}`;
|
|
1389
|
-
const title = `${author.name} Articles
|
|
1761
|
+
const title = formatPageTitle(`${author.name} Articles`, config);
|
|
1390
1762
|
return {
|
|
1391
1763
|
title,
|
|
1392
1764
|
description: author.bio,
|
|
@@ -1568,17 +1940,41 @@ function resolveAuthorAvatar(author, config) {
|
|
|
1568
1940
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
1569
1941
|
return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
|
|
1570
1942
|
}
|
|
1943
|
+
function newestArticleDate(articles) {
|
|
1944
|
+
var _a;
|
|
1945
|
+
let newest;
|
|
1946
|
+
for (const article of articles) {
|
|
1947
|
+
const stamp = (_a = article.lastmod) != null ? _a : article.date;
|
|
1948
|
+
if (!stamp) continue;
|
|
1949
|
+
const parsed = new Date(stamp);
|
|
1950
|
+
if (Number.isNaN(parsed.getTime())) continue;
|
|
1951
|
+
if (!newest || parsed > newest) newest = parsed;
|
|
1952
|
+
}
|
|
1953
|
+
return newest;
|
|
1954
|
+
}
|
|
1955
|
+
function paginationEntries(basePath, itemCount, pageSize, lastModified, priority) {
|
|
1956
|
+
const totalPages = getTotalPages(itemCount, pageSize);
|
|
1957
|
+
const entries = [];
|
|
1958
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
1959
|
+
entries.push({
|
|
1960
|
+
url: buildPageUrl(basePath, page),
|
|
1961
|
+
lastModified,
|
|
1962
|
+
changeFrequency: "weekly",
|
|
1963
|
+
priority
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
return entries;
|
|
1967
|
+
}
|
|
1571
1968
|
function getArticleSitemapEntries(baseUrlOrConfig) {
|
|
1572
1969
|
return __async(this, null, function* () {
|
|
1573
|
-
|
|
1970
|
+
var _a, _b;
|
|
1971
|
+
const config = typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig;
|
|
1972
|
+
const baseUrl = ((_a = config == null ? void 0 : config.siteUrl) != null ? _a : baseUrlOrConfig).replace(/\/$/, "");
|
|
1574
1973
|
try {
|
|
1575
|
-
const [articles, categories] = yield Promise.all([
|
|
1576
|
-
getAllArticles(typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig),
|
|
1577
|
-
getAllCategories()
|
|
1578
|
-
]);
|
|
1974
|
+
const [articles, categories] = yield Promise.all([getAllArticles(config), getAllCategories()]);
|
|
1579
1975
|
const articleEntries = articles.map((article) => {
|
|
1580
|
-
var
|
|
1581
|
-
const dateStr = (
|
|
1976
|
+
var _a2;
|
|
1977
|
+
const dateStr = (_a2 = article.lastmod) != null ? _a2 : article.date;
|
|
1582
1978
|
const lastModified = dateStr ? new Date(dateStr) : void 0;
|
|
1583
1979
|
return {
|
|
1584
1980
|
url: `${baseUrl}/articles/${article.slug}`,
|
|
@@ -1589,16 +1985,86 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
|
|
|
1589
1985
|
});
|
|
1590
1986
|
const categoryEntries = categories.map((cat) => ({
|
|
1591
1987
|
url: `${baseUrl}/articles/category/${cat.slug}`,
|
|
1592
|
-
lastModified:
|
|
1988
|
+
lastModified: newestArticleDate(
|
|
1989
|
+
articles.filter(
|
|
1990
|
+
(article) => {
|
|
1991
|
+
var _a2;
|
|
1992
|
+
return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
|
|
1993
|
+
}
|
|
1994
|
+
)
|
|
1995
|
+
),
|
|
1593
1996
|
changeFrequency: "weekly",
|
|
1594
1997
|
priority: 0.7
|
|
1595
1998
|
}));
|
|
1596
|
-
const
|
|
1999
|
+
const authors = config && config.showAuthorPage !== false ? getAllAuthors(config) : [];
|
|
2000
|
+
const authorEntries = authors.map((author) => ({
|
|
1597
2001
|
url: `${baseUrl}/articles/authors/${author.slug}`,
|
|
2002
|
+
lastModified: newestArticleDate(
|
|
2003
|
+
articles.filter(
|
|
2004
|
+
(article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
|
|
2005
|
+
)
|
|
2006
|
+
),
|
|
1598
2007
|
changeFrequency: "monthly",
|
|
1599
2008
|
priority: 0.6
|
|
1600
2009
|
}));
|
|
1601
|
-
|
|
2010
|
+
const seriesSlugs = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
|
|
2011
|
+
const seriesEntries = seriesSlugs.map((seriesSlug) => ({
|
|
2012
|
+
url: `${baseUrl}/articles/series/${seriesSlug}`,
|
|
2013
|
+
lastModified: newestArticleDate(articles.filter((a) => a.seriesSlug === seriesSlug)),
|
|
2014
|
+
changeFrequency: "weekly",
|
|
2015
|
+
priority: 0.6
|
|
2016
|
+
}));
|
|
2017
|
+
const pageEntries = [];
|
|
2018
|
+
if ((config == null ? void 0 : config.listingPagination) === "pages") {
|
|
2019
|
+
const pageSize = (_b = config.pageSize) != null ? _b : DEFAULT_PAGE_SIZE;
|
|
2020
|
+
pageEntries.push(
|
|
2021
|
+
...paginationEntries(
|
|
2022
|
+
`${baseUrl}/articles`,
|
|
2023
|
+
articles.length,
|
|
2024
|
+
pageSize,
|
|
2025
|
+
newestArticleDate(articles),
|
|
2026
|
+
0.5
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
2029
|
+
for (const cat of categories) {
|
|
2030
|
+
const inCategory = articles.filter(
|
|
2031
|
+
(article) => {
|
|
2032
|
+
var _a2;
|
|
2033
|
+
return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
|
|
2034
|
+
}
|
|
2035
|
+
);
|
|
2036
|
+
pageEntries.push(
|
|
2037
|
+
...paginationEntries(
|
|
2038
|
+
`${baseUrl}/articles/category/${cat.slug}`,
|
|
2039
|
+
inCategory.length,
|
|
2040
|
+
pageSize,
|
|
2041
|
+
newestArticleDate(inCategory),
|
|
2042
|
+
0.4
|
|
2043
|
+
)
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
for (const author of authors) {
|
|
2047
|
+
const byAuthor = articles.filter(
|
|
2048
|
+
(article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
|
|
2049
|
+
);
|
|
2050
|
+
pageEntries.push(
|
|
2051
|
+
...paginationEntries(
|
|
2052
|
+
`${baseUrl}/articles/authors/${author.slug}`,
|
|
2053
|
+
byAuthor.length,
|
|
2054
|
+
pageSize,
|
|
2055
|
+
newestArticleDate(byAuthor),
|
|
2056
|
+
0.4
|
|
2057
|
+
)
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return [
|
|
2062
|
+
...articleEntries,
|
|
2063
|
+
...categoryEntries,
|
|
2064
|
+
...authorEntries,
|
|
2065
|
+
...seriesEntries,
|
|
2066
|
+
...pageEntries
|
|
2067
|
+
];
|
|
1602
2068
|
} catch (e) {
|
|
1603
2069
|
return [];
|
|
1604
2070
|
}
|
|
@@ -1772,8 +2238,32 @@ function ArticleTOC({ toc, className }) {
|
|
|
1772
2238
|
);
|
|
1773
2239
|
}
|
|
1774
2240
|
|
|
2241
|
+
// src/ArticleAnswer.tsx
|
|
2242
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
2243
|
+
function ArticleAnswer({
|
|
2244
|
+
article,
|
|
2245
|
+
label = "The short answer",
|
|
2246
|
+
className
|
|
2247
|
+
}) {
|
|
2248
|
+
var _a;
|
|
2249
|
+
if (!((_a = article.answer) == null ? void 0 : _a.trim())) return null;
|
|
2250
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
2251
|
+
"aside",
|
|
2252
|
+
{
|
|
2253
|
+
className: `mb-8 rounded-lg border-l-4 border-primary bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
|
|
2254
|
+
style: { borderLeftWidth: "4px" },
|
|
2255
|
+
children: [
|
|
2256
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "mb-2 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
|
|
2257
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-base leading-relaxed", children: article.answer })
|
|
2258
|
+
]
|
|
2259
|
+
}
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
1775
2263
|
// src/validateArticles.ts
|
|
1776
2264
|
var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
|
|
2265
|
+
var THIN_CONTENT_WORDS = 300;
|
|
2266
|
+
var STALE_CONTENT_MONTHS = 18;
|
|
1777
2267
|
var SEARCH_TITLE_MAX = 60;
|
|
1778
2268
|
var SEARCH_DESCRIPTION_MAX = 160;
|
|
1779
2269
|
var SOCIAL_TITLE_MAX = 95;
|
|
@@ -1917,12 +2407,15 @@ function checkRequiredFrontmatter(articles) {
|
|
|
1917
2407
|
}
|
|
1918
2408
|
return issues;
|
|
1919
2409
|
}
|
|
1920
|
-
function checkDiscoveryFieldLengths(articles) {
|
|
2410
|
+
function checkDiscoveryFieldLengths(articles, config) {
|
|
2411
|
+
var _a, _b;
|
|
1921
2412
|
const issues = [];
|
|
1922
2413
|
for (const article of articles) {
|
|
2414
|
+
const effectiveTitle = formatPageTitle((_a = article.searchTitle) != null ? _a : article.title, config);
|
|
2415
|
+
const effectiveDescription = (_b = article.searchDescription) != null ? _b : article.excerpt;
|
|
1923
2416
|
const checks = [
|
|
1924
|
-
[
|
|
1925
|
-
[
|
|
2417
|
+
[effectiveTitle, "effective-title-too-long", SEARCH_TITLE_MAX],
|
|
2418
|
+
[effectiveDescription, "effective-description-too-long", SEARCH_DESCRIPTION_MAX],
|
|
1926
2419
|
[article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
|
|
1927
2420
|
[article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
|
|
1928
2421
|
];
|
|
@@ -1939,6 +2432,101 @@ function checkDiscoveryFieldLengths(articles) {
|
|
|
1939
2432
|
}
|
|
1940
2433
|
return issues;
|
|
1941
2434
|
}
|
|
2435
|
+
function checkAnswerability(articles) {
|
|
2436
|
+
var _a, _b, _c;
|
|
2437
|
+
const issues = [];
|
|
2438
|
+
for (const article of articles) {
|
|
2439
|
+
const hasQuestionHeading = ((_a = article.toc) != null ? _a : []).some(
|
|
2440
|
+
(item) => item.depth === 2 && item.text.trim().endsWith("?")
|
|
2441
|
+
);
|
|
2442
|
+
if (!article.answer && !((_b = article.faq) == null ? void 0 : _b.length) && !hasQuestionHeading) {
|
|
2443
|
+
issues.push({
|
|
2444
|
+
severity: "warning",
|
|
2445
|
+
code: "no-answer",
|
|
2446
|
+
message: "Article has no `answer`, no `faq`, and no question-shaped heading - nothing for an answer engine to lift.",
|
|
2447
|
+
articleSlug: article.slug
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
if (article.wordCount !== void 0 && article.wordCount < THIN_CONTENT_WORDS) {
|
|
2451
|
+
issues.push({
|
|
2452
|
+
severity: "warning",
|
|
2453
|
+
code: "thin-content",
|
|
2454
|
+
message: `Article is ${article.wordCount} words (under ${THIN_CONTENT_WORDS}).`,
|
|
2455
|
+
articleSlug: article.slug
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
if (!((_c = article.about) == null ? void 0 : _c.length)) {
|
|
2459
|
+
issues.push({
|
|
2460
|
+
severity: "warning",
|
|
2461
|
+
code: "missing-about",
|
|
2462
|
+
message: "Article has no `about` entity references.",
|
|
2463
|
+
articleSlug: article.slug
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return issues;
|
|
2468
|
+
}
|
|
2469
|
+
function checkEntityReferences(articles, config) {
|
|
2470
|
+
var _a;
|
|
2471
|
+
const registry = config.entities;
|
|
2472
|
+
if (!registry) return [];
|
|
2473
|
+
const known = new Set(Object.values(registry).map((entity) => entity.name));
|
|
2474
|
+
const issues = [];
|
|
2475
|
+
for (const article of articles) {
|
|
2476
|
+
for (const entity of (_a = article.about) != null ? _a : []) {
|
|
2477
|
+
if (!known.has(entity.name) && !entity.sameAs) {
|
|
2478
|
+
issues.push({
|
|
2479
|
+
severity: "warning",
|
|
2480
|
+
code: "unknown-entity",
|
|
2481
|
+
message: `about entry "${entity.name}" is not in config.entities and has no sameAs.`,
|
|
2482
|
+
articleSlug: article.slug
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return issues;
|
|
2488
|
+
}
|
|
2489
|
+
function checkStaleContent(articles, now) {
|
|
2490
|
+
var _a;
|
|
2491
|
+
const cutoff = new Date(now);
|
|
2492
|
+
cutoff.setMonth(cutoff.getMonth() - STALE_CONTENT_MONTHS);
|
|
2493
|
+
const issues = [];
|
|
2494
|
+
for (const article of articles) {
|
|
2495
|
+
const stamp = (_a = article.lastmod) != null ? _a : article.date;
|
|
2496
|
+
if (!stamp) continue;
|
|
2497
|
+
const parsed = new Date(stamp);
|
|
2498
|
+
if (Number.isNaN(parsed.getTime())) continue;
|
|
2499
|
+
if (parsed < cutoff) {
|
|
2500
|
+
issues.push({
|
|
2501
|
+
severity: "warning",
|
|
2502
|
+
code: "stale-content",
|
|
2503
|
+
message: `Last updated ${stamp}, over ${STALE_CONTENT_MONTHS} months ago.`,
|
|
2504
|
+
articleSlug: article.slug
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
return issues;
|
|
2509
|
+
}
|
|
2510
|
+
function checkOrphanArticles(articles) {
|
|
2511
|
+
const bodies = articles.filter((article) => typeof article.content === "string");
|
|
2512
|
+
if (bodies.length === 0) return [];
|
|
2513
|
+
const issues = [];
|
|
2514
|
+
for (const article of articles) {
|
|
2515
|
+
const needle = `/articles/${article.slug}`;
|
|
2516
|
+
const linked = bodies.some(
|
|
2517
|
+
(other) => other.slug !== article.slug && other.content.includes(needle)
|
|
2518
|
+
);
|
|
2519
|
+
if (!linked) {
|
|
2520
|
+
issues.push({
|
|
2521
|
+
severity: "warning",
|
|
2522
|
+
code: "orphan-article",
|
|
2523
|
+
message: "No other article links to this one.",
|
|
2524
|
+
articleSlug: article.slug
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
return issues;
|
|
2529
|
+
}
|
|
1942
2530
|
function checkCategorySlugs(articles) {
|
|
1943
2531
|
var _a;
|
|
1944
2532
|
const issues = [];
|
|
@@ -1962,7 +2550,8 @@ function checkCategorySlugs(articles) {
|
|
|
1962
2550
|
}
|
|
1963
2551
|
return issues;
|
|
1964
2552
|
}
|
|
1965
|
-
function validateArticles(articles, config) {
|
|
2553
|
+
function validateArticles(articles, config, options) {
|
|
2554
|
+
var _a;
|
|
1966
2555
|
const errors = [
|
|
1967
2556
|
...checkDuplicateCanonicalUrls(articles),
|
|
1968
2557
|
...checkAuthorReferences(articles, config),
|
|
@@ -1972,15 +2561,19 @@ function validateArticles(articles, config) {
|
|
|
1972
2561
|
];
|
|
1973
2562
|
const warnings = [
|
|
1974
2563
|
...checkRequiredFrontmatter(articles),
|
|
1975
|
-
...checkDiscoveryFieldLengths(articles),
|
|
2564
|
+
...checkDiscoveryFieldLengths(articles, config),
|
|
2565
|
+
...checkAnswerability(articles),
|
|
2566
|
+
...checkEntityReferences(articles, config),
|
|
2567
|
+
...checkStaleContent(articles, (_a = options == null ? void 0 : options.now) != null ? _a : /* @__PURE__ */ new Date()),
|
|
2568
|
+
...checkOrphanArticles(articles),
|
|
1976
2569
|
...checkCategorySlugs(articles)
|
|
1977
2570
|
];
|
|
1978
2571
|
return { ok: errors.length === 0, errors, warnings };
|
|
1979
2572
|
}
|
|
1980
|
-
function validateAllArticles(config) {
|
|
2573
|
+
function validateAllArticles(config, options) {
|
|
1981
2574
|
return __async(this, null, function* () {
|
|
1982
2575
|
const articles = yield getAllArticles(config);
|
|
1983
|
-
return validateArticles(articles, config);
|
|
2576
|
+
return validateArticles(articles, config, options);
|
|
1984
2577
|
});
|
|
1985
2578
|
}
|
|
1986
2579
|
|
|
@@ -1994,16 +2587,21 @@ function emitArticleEvent(handler, event) {
|
|
|
1994
2587
|
}
|
|
1995
2588
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1996
2589
|
0 && (module.exports = {
|
|
2590
|
+
AI_CRAWLERS,
|
|
2591
|
+
ArticleAnswer,
|
|
1997
2592
|
ArticleContent,
|
|
1998
2593
|
ArticleTOC,
|
|
1999
2594
|
buildArticleBreadcrumbs,
|
|
2000
2595
|
buildAuthorBreadcrumbs,
|
|
2001
2596
|
buildCategoryBreadcrumbs,
|
|
2597
|
+
buildMarkdownTwinHeader,
|
|
2002
2598
|
buildPageUrl,
|
|
2003
2599
|
buildPaginationLinks,
|
|
2004
2600
|
categoryToSlug,
|
|
2601
|
+
deriveFaqFromHeadings,
|
|
2005
2602
|
emitArticleEvent,
|
|
2006
2603
|
extractToc,
|
|
2604
|
+
formatPageTitle,
|
|
2007
2605
|
generateArticleMetadata,
|
|
2008
2606
|
generateArticleStaticParams,
|
|
2009
2607
|
generateArticlesIndexMetadata,
|
|
@@ -2015,6 +2613,8 @@ function emitArticleEvent(handler, event) {
|
|
|
2015
2613
|
generateCategoryPageMetadata,
|
|
2016
2614
|
generateCategoryStaticParams,
|
|
2017
2615
|
generateListingPageStaticParams,
|
|
2616
|
+
generateLlmsFullTxt,
|
|
2617
|
+
generateLlmsTxt,
|
|
2018
2618
|
generateRssFeed,
|
|
2019
2619
|
generateSeriesMetadata,
|
|
2020
2620
|
generateSeriesStaticParams,
|
|
@@ -2035,16 +2635,24 @@ function emitArticleEvent(handler, event) {
|
|
|
2035
2635
|
getArticlesByCategory,
|
|
2036
2636
|
getArticlesBySeries,
|
|
2037
2637
|
getAuthorBySlug,
|
|
2638
|
+
getAuthorMarkdown,
|
|
2038
2639
|
getAvailableArticleSlugs,
|
|
2039
2640
|
getBreadcrumbsConfig,
|
|
2641
|
+
getCategoryMarkdown,
|
|
2040
2642
|
getContentSlotBoundaries,
|
|
2643
|
+
getMarkdownTwinResponse,
|
|
2644
|
+
getOrganizationId,
|
|
2041
2645
|
getPath,
|
|
2042
2646
|
getPathArticles,
|
|
2647
|
+
getPersonId,
|
|
2043
2648
|
getRelatedArticlesByCategory,
|
|
2044
2649
|
getRelatedContent,
|
|
2650
|
+
getSeriesMarkdown,
|
|
2045
2651
|
getTotalPages,
|
|
2652
|
+
getWebSiteId,
|
|
2046
2653
|
isPageOutOfRange,
|
|
2047
2654
|
markdownToHtml,
|
|
2655
|
+
matchAiCrawler,
|
|
2048
2656
|
paginateArticles,
|
|
2049
2657
|
parsePageParam,
|
|
2050
2658
|
resolveAuthorAvatar,
|