@fullstackdatasolutions/articles 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +313 -1
  3. package/dist/index.cjs +308 -79
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +267 -16
  6. package/dist/index.d.ts +267 -16
  7. package/dist/index.js +300 -79
  8. package/dist/index.js.map +1 -1
  9. package/dist/nextjs.cjs +325 -31
  10. package/dist/nextjs.cjs.map +1 -1
  11. package/dist/nextjs.d.cts +179 -2
  12. package/dist/nextjs.d.ts +179 -2
  13. package/dist/nextjs.js +325 -31
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +660 -50
  16. package/dist/server.cjs.map +1 -1
  17. package/dist/server.d.cts +333 -12
  18. package/dist/server.d.ts +333 -12
  19. package/dist/server.js +645 -50
  20. package/dist/server.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/ArticleAnswer.tsx +35 -0
  23. package/src/ArticleSchemas.tsx +263 -23
  24. package/src/AuthorArticlesPage.tsx +38 -8
  25. package/src/__tests__/ArticleAnswer.test.tsx +25 -0
  26. package/src/__tests__/ArticleSchemas.test.tsx +516 -0
  27. package/src/__tests__/AuthorArticlesPage.test.tsx +76 -0
  28. package/src/__tests__/authorUtils.test.ts +50 -0
  29. package/src/__tests__/markdown.test.ts +77 -1
  30. package/src/__tests__/nextjs.test.ts +31 -15
  31. package/src/__tests__/seoUtils.test.ts +279 -0
  32. package/src/__tests__/server-articles.test.ts +434 -1
  33. package/src/__tests__/validateArticles.test.ts +167 -6
  34. package/src/articleTypes.ts +57 -0
  35. package/src/articlesConfig.ts +176 -1
  36. package/src/authorUtils.ts +19 -1
  37. package/src/errorReporting.ts +1 -0
  38. package/src/index.ts +17 -1
  39. package/src/markdown.ts +100 -1
  40. package/src/nextjs.ts +7 -4
  41. package/src/seoUtils.ts +247 -26
  42. package/src/server-articles.ts +385 -25
  43. package/src/server.ts +35 -4
  44. 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: parseDateField(data.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 === true,
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,16 +987,186 @@ function getArticleMarkdown(slug) {
858
987
  }
859
988
  });
860
989
  }
861
- function getArticleMarkdownResponse(slug, config) {
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
- return new Response(markdown, {
867
- headers: __spreadValues({
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
+ const canonicalUrl = `${config.siteUrl.replace(/\/$/, "")}/articles/${slug}`;
1164
+ return new Response(body, {
1165
+ headers: {
868
1166
  "Content-Type": "text/markdown; charset=utf-8",
869
- "Cache-Control": "public, max-age=3600, s-maxage=3600"
870
- }, article ? getArticleAiHeaders(article, config) : {})
1167
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1168
+ Link: `<${canonicalUrl}>; rel="canonical"`
1169
+ }
871
1170
  });
872
1171
  });
873
1172
  }
@@ -888,22 +1187,15 @@ function getArticleAiHeaders(article, config) {
888
1187
  "X-Robots-Tag": "noai, noimageai"
889
1188
  };
890
1189
  }
891
- function getAiRobotsTxtRules() {
1190
+ function getAiRobotsTxtRules(config) {
892
1191
  return __async(this, null, function* () {
893
- const articles = yield getAllArticles();
1192
+ const articles = yield getAllArticles(config);
894
1193
  const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
895
1194
  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
1195
  const disallowRules = blockedArticles.map((article) => `Disallow: /articles/${article.slug}`).join("\n");
906
- return aiCrawlers.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join("\n\n");
1196
+ return AI_CRAWLERS.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join(
1197
+ "\n\n"
1198
+ );
907
1199
  });
908
1200
  }
909
1201
  function searchArticles(query, config) {
@@ -1043,6 +1335,7 @@ function getRelatedContent(article, config, limit = 3) {
1043
1335
  }
1044
1336
 
1045
1337
  // src/articlesConfig.ts
1338
+ var DEFAULT_PAGE_SIZE = 6;
1046
1339
  function breadcrumbsAreEnabled(config) {
1047
1340
  var _a;
1048
1341
  return config.breadcrumbs !== false && ((_a = config.breadcrumbs) == null ? void 0 : _a.show) !== false;
@@ -1052,6 +1345,20 @@ function getBreadcrumbsConfig(config) {
1052
1345
  if (config.breadcrumbs === false) return {};
1053
1346
  return (_a = config.breadcrumbs) != null ? _a : {};
1054
1347
  }
1348
+ function getOrganizationId(config) {
1349
+ return `${config.siteUrl.replace(/\/$/, "")}/#organization`;
1350
+ }
1351
+ function getWebSiteId(config) {
1352
+ return `${config.siteUrl.replace(/\/$/, "")}/#website`;
1353
+ }
1354
+ function getPersonId(authorUrl) {
1355
+ return `${authorUrl.replace(/\/$/, "")}#person`;
1356
+ }
1357
+ var DEFAULT_TITLE_TEMPLATE = "{title} | {siteName}";
1358
+ function formatPageTitle(title, config) {
1359
+ var _a;
1360
+ return ((_a = config.titleTemplate) != null ? _a : DEFAULT_TITLE_TEMPLATE).replaceAll("{title}", title).replaceAll("{siteName}", config.siteName);
1361
+ }
1055
1362
 
1056
1363
  // src/pagination.ts
1057
1364
  function getTotalPages(totalCount, pageSize) {
@@ -1099,10 +1406,11 @@ function isPageOutOfRange(page, totalPages) {
1099
1406
  function escapeXml(str) {
1100
1407
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1101
1408
  }
1102
- function generateRssFeed(articles, config) {
1103
- var _a;
1409
+ function generateRssFeed(articles, config, options) {
1410
+ var _a, _b;
1104
1411
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1105
1412
  const showAuthor = config.showAuthor !== false;
1413
+ const fullContent = (options == null ? void 0 : options.fullContent) === true;
1106
1414
  const items = articles.map((article) => {
1107
1415
  const url = `${siteUrl}/articles/${article.slug}`;
1108
1416
  const pubDate = article.date ? new Date(article.date).toUTCString() : "";
@@ -1114,6 +1422,7 @@ function generateRssFeed(articles, config) {
1114
1422
  ` <guid isPermaLink="true">${url}</guid>`,
1115
1423
  pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
1116
1424
  article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : "",
1425
+ fullContent && article.htmlContent ? ` <content:encoded><![CDATA[${article.htmlContent}]]></content:encoded>` : "",
1117
1426
  showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : "",
1118
1427
  article.category ? ` <category><![CDATA[${article.category}]]></category>` : "",
1119
1428
  imageUrl ? ` <media:content url="${imageUrl}" medium="image" width="1200" height="630"/>` : "",
@@ -1122,17 +1431,82 @@ function generateRssFeed(articles, config) {
1122
1431
  }).join("\n");
1123
1432
  const description = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1124
1433
  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/">
1434
+ <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
1435
  <channel>
1127
1436
  <title><![CDATA[${config.siteName}]]></title>
1128
1437
  <link>${siteUrl}/articles</link>
1129
1438
  <description><![CDATA[${description}]]></description>
1130
- <language>en</language>
1439
+ <language>${(_b = config.language) != null ? _b : "en"}</language>
1131
1440
  <atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
1132
1441
  ${items}
1133
1442
  </channel>
1134
1443
  </rss>`;
1135
1444
  }
1445
+ function buildLlmsHeader(config) {
1446
+ var _a;
1447
+ const summary = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1448
+ return [`# ${config.siteName}`, "", `> ${summary}`, ""];
1449
+ }
1450
+ function generateLlmsTxt(articles, config) {
1451
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1452
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
1453
+ const byCategory = /* @__PURE__ */ new Map();
1454
+ for (const article of crawlable) {
1455
+ const category = article.category || "Articles";
1456
+ const existing = byCategory.get(category);
1457
+ if (existing) existing.push(article);
1458
+ else byCategory.set(category, [article]);
1459
+ }
1460
+ const sections = [...byCategory.entries()].map(([category, categoryArticles]) => {
1461
+ const lines = categoryArticles.map((article) => {
1462
+ const url = `${siteUrl}/articles/${article.slug}.md`;
1463
+ const summary = article.excerpt ? `: ${article.excerpt}` : "";
1464
+ return `- [${article.title}](${url})${summary}`;
1465
+ });
1466
+ return [`## ${category}`, "", ...lines].join("\n");
1467
+ });
1468
+ return [
1469
+ ...buildLlmsHeader(config),
1470
+ ...sections.length > 0 ? sections : ["## Articles", "", "_No articles available._"],
1471
+ ...buildLlmsListingSection(crawlable, config),
1472
+ ""
1473
+ ].join("\n");
1474
+ }
1475
+ function buildLlmsListingSection(articles, config) {
1476
+ var _a;
1477
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1478
+ const categories = [...new Set(articles.flatMap((a) => {
1479
+ var _a2;
1480
+ return (_a2 = a.categories) != null ? _a2 : [];
1481
+ }))].filter(Boolean);
1482
+ const series = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
1483
+ const authors = Object.values((_a = config.authors) != null ? _a : {});
1484
+ const lines = [
1485
+ ...categories.map(
1486
+ (name) => `- [${name}](${siteUrl}/articles/category/${categoryToSlug(name)}.md)`
1487
+ ),
1488
+ ...config.showAuthorPage === false ? [] : authors.map((a) => `- [${a.name}](${siteUrl}/articles/authors/${a.slug}.md)`),
1489
+ ...series.map((slug) => `- [${slug}](${siteUrl}/articles/series/${slug}.md)`)
1490
+ ];
1491
+ if (lines.length === 0) return [];
1492
+ return ["## Collections", "", ...lines];
1493
+ }
1494
+ function generateLlmsFullTxt(articles, config) {
1495
+ return __async(this, null, function* () {
1496
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
1497
+ const documents = yield Promise.all(
1498
+ crawlable.map((article) => __async(this, null, function* () {
1499
+ const body = yield getArticleMarkdown(article.slug, config);
1500
+ if (body === null) return null;
1501
+ return `${buildMarkdownTwinHeader(article, config, body)}${body.trimStart()}`;
1502
+ }))
1503
+ );
1504
+ return [
1505
+ ...buildLlmsHeader(config),
1506
+ ...documents.filter((doc) => doc !== null)
1507
+ ].join("\n");
1508
+ });
1509
+ }
1136
1510
  function generateArticleStaticParams() {
1137
1511
  return getAvailableArticleSlugs().map((slug) => ({ slug }));
1138
1512
  }
@@ -1198,7 +1572,7 @@ function generateArticleMetadata(slug, config) {
1198
1572
  const markdownUrl = getArticleMarkdownUrl(article, config);
1199
1573
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
1200
1574
  return {
1201
- title: `${search.title} | ${config.siteName}`,
1575
+ title: formatPageTitle(search.title, config),
1202
1576
  description,
1203
1577
  keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
1204
1578
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
@@ -1252,7 +1626,7 @@ function generateArticlesIndexMetadata(config) {
1252
1626
  var _a, _b;
1253
1627
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1254
1628
  const indexUrl = `${siteUrl}/articles`;
1255
- const title = `Articles | ${config.siteName}`;
1629
+ const title = formatPageTitle("Articles", config);
1256
1630
  const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
1257
1631
  return {
1258
1632
  title,
@@ -1300,7 +1674,7 @@ function generateCategoryMetadata(categorySlug, config) {
1300
1674
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
1301
1675
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
1302
1676
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
1303
- const title = `${categoryName} Articles | ${config.siteName}`;
1677
+ const title = formatPageTitle(`${categoryName} Articles`, config);
1304
1678
  return {
1305
1679
  title,
1306
1680
  description,
@@ -1344,7 +1718,7 @@ function generateSeriesMetadata(seriesSlug, config) {
1344
1718
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1345
1719
  const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1346
1720
  const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1347
- const title = `${seriesName} Series | ${config.siteName}`;
1721
+ const title = formatPageTitle(`${seriesName} Series`, config);
1348
1722
  return {
1349
1723
  title,
1350
1724
  description,
@@ -1386,7 +1760,7 @@ function generateAuthorMetadata(authorSlug, config) {
1386
1760
  if (!author || config.showAuthorPage === false) return { title: "Author Not Found" };
1387
1761
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1388
1762
  const authorUrl = (_a = author.url) != null ? _a : `${siteUrl}/articles/authors/${author.slug}`;
1389
- const title = `${author.name} Articles | ${config.siteName}`;
1763
+ const title = formatPageTitle(`${author.name} Articles`, config);
1390
1764
  return {
1391
1765
  title,
1392
1766
  description: author.bio,
@@ -1568,17 +1942,41 @@ function resolveAuthorAvatar(author, config) {
1568
1942
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1569
1943
  return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
1570
1944
  }
1945
+ function newestArticleDate(articles) {
1946
+ var _a;
1947
+ let newest;
1948
+ for (const article of articles) {
1949
+ const stamp = (_a = article.lastmod) != null ? _a : article.date;
1950
+ if (!stamp) continue;
1951
+ const parsed = new Date(stamp);
1952
+ if (Number.isNaN(parsed.getTime())) continue;
1953
+ if (!newest || parsed > newest) newest = parsed;
1954
+ }
1955
+ return newest;
1956
+ }
1957
+ function paginationEntries(basePath, itemCount, pageSize, lastModified, priority) {
1958
+ const totalPages = getTotalPages(itemCount, pageSize);
1959
+ const entries = [];
1960
+ for (let page = 2; page <= totalPages; page++) {
1961
+ entries.push({
1962
+ url: buildPageUrl(basePath, page),
1963
+ lastModified,
1964
+ changeFrequency: "weekly",
1965
+ priority
1966
+ });
1967
+ }
1968
+ return entries;
1969
+ }
1571
1970
  function getArticleSitemapEntries(baseUrlOrConfig) {
1572
1971
  return __async(this, null, function* () {
1573
- const baseUrl = (typeof baseUrlOrConfig === "string" ? baseUrlOrConfig : baseUrlOrConfig.siteUrl).replace(/\/$/, "");
1972
+ var _a, _b;
1973
+ const config = typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig;
1974
+ const baseUrl = ((_a = config == null ? void 0 : config.siteUrl) != null ? _a : baseUrlOrConfig).replace(/\/$/, "");
1574
1975
  try {
1575
- const [articles, categories] = yield Promise.all([
1576
- getAllArticles(typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig),
1577
- getAllCategories()
1578
- ]);
1976
+ const [articles, categories] = yield Promise.all([getAllArticles(config), getAllCategories()]);
1579
1977
  const articleEntries = articles.map((article) => {
1580
- var _a;
1581
- const dateStr = (_a = article.lastmod) != null ? _a : article.date;
1978
+ var _a2;
1979
+ const dateStr = (_a2 = article.lastmod) != null ? _a2 : article.date;
1582
1980
  const lastModified = dateStr ? new Date(dateStr) : void 0;
1583
1981
  return {
1584
1982
  url: `${baseUrl}/articles/${article.slug}`,
@@ -1589,16 +1987,86 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1589
1987
  });
1590
1988
  const categoryEntries = categories.map((cat) => ({
1591
1989
  url: `${baseUrl}/articles/category/${cat.slug}`,
1592
- lastModified: /* @__PURE__ */ new Date(),
1990
+ lastModified: newestArticleDate(
1991
+ articles.filter(
1992
+ (article) => {
1993
+ var _a2;
1994
+ return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
1995
+ }
1996
+ )
1997
+ ),
1593
1998
  changeFrequency: "weekly",
1594
1999
  priority: 0.7
1595
2000
  }));
1596
- const authorEntries = typeof baseUrlOrConfig === "string" || baseUrlOrConfig.showAuthorPage === false ? [] : getAllAuthors(baseUrlOrConfig).map((author) => ({
2001
+ const authors = config && config.showAuthorPage !== false ? getAllAuthors(config) : [];
2002
+ const authorEntries = authors.map((author) => ({
1597
2003
  url: `${baseUrl}/articles/authors/${author.slug}`,
2004
+ lastModified: newestArticleDate(
2005
+ articles.filter(
2006
+ (article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
2007
+ )
2008
+ ),
1598
2009
  changeFrequency: "monthly",
1599
2010
  priority: 0.6
1600
2011
  }));
1601
- return [...articleEntries, ...categoryEntries, ...authorEntries];
2012
+ const seriesSlugs = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
2013
+ const seriesEntries = seriesSlugs.map((seriesSlug) => ({
2014
+ url: `${baseUrl}/articles/series/${seriesSlug}`,
2015
+ lastModified: newestArticleDate(articles.filter((a) => a.seriesSlug === seriesSlug)),
2016
+ changeFrequency: "weekly",
2017
+ priority: 0.6
2018
+ }));
2019
+ const pageEntries = [];
2020
+ if ((config == null ? void 0 : config.listingPagination) === "pages") {
2021
+ const pageSize = (_b = config.pageSize) != null ? _b : DEFAULT_PAGE_SIZE;
2022
+ pageEntries.push(
2023
+ ...paginationEntries(
2024
+ `${baseUrl}/articles`,
2025
+ articles.length,
2026
+ pageSize,
2027
+ newestArticleDate(articles),
2028
+ 0.5
2029
+ )
2030
+ );
2031
+ for (const cat of categories) {
2032
+ const inCategory = articles.filter(
2033
+ (article) => {
2034
+ var _a2;
2035
+ return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
2036
+ }
2037
+ );
2038
+ pageEntries.push(
2039
+ ...paginationEntries(
2040
+ `${baseUrl}/articles/category/${cat.slug}`,
2041
+ inCategory.length,
2042
+ pageSize,
2043
+ newestArticleDate(inCategory),
2044
+ 0.4
2045
+ )
2046
+ );
2047
+ }
2048
+ for (const author of authors) {
2049
+ const byAuthor = articles.filter(
2050
+ (article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
2051
+ );
2052
+ pageEntries.push(
2053
+ ...paginationEntries(
2054
+ `${baseUrl}/articles/authors/${author.slug}`,
2055
+ byAuthor.length,
2056
+ pageSize,
2057
+ newestArticleDate(byAuthor),
2058
+ 0.4
2059
+ )
2060
+ );
2061
+ }
2062
+ }
2063
+ return [
2064
+ ...articleEntries,
2065
+ ...categoryEntries,
2066
+ ...authorEntries,
2067
+ ...seriesEntries,
2068
+ ...pageEntries
2069
+ ];
1602
2070
  } catch (e) {
1603
2071
  return [];
1604
2072
  }
@@ -1772,8 +2240,32 @@ function ArticleTOC({ toc, className }) {
1772
2240
  );
1773
2241
  }
1774
2242
 
2243
+ // src/ArticleAnswer.tsx
2244
+ var import_jsx_runtime4 = require("react/jsx-runtime");
2245
+ function ArticleAnswer({
2246
+ article,
2247
+ label = "The short answer",
2248
+ className
2249
+ }) {
2250
+ var _a;
2251
+ if (!((_a = article.answer) == null ? void 0 : _a.trim())) return null;
2252
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2253
+ "aside",
2254
+ {
2255
+ className: `mb-8 rounded-lg border-l-4 border-primary bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
2256
+ style: { borderLeftWidth: "4px" },
2257
+ children: [
2258
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "mb-2 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
2259
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-base leading-relaxed", children: article.answer })
2260
+ ]
2261
+ }
2262
+ );
2263
+ }
2264
+
1775
2265
  // src/validateArticles.ts
1776
2266
  var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
2267
+ var THIN_CONTENT_WORDS = 300;
2268
+ var STALE_CONTENT_MONTHS = 18;
1777
2269
  var SEARCH_TITLE_MAX = 60;
1778
2270
  var SEARCH_DESCRIPTION_MAX = 160;
1779
2271
  var SOCIAL_TITLE_MAX = 95;
@@ -1917,12 +2409,15 @@ function checkRequiredFrontmatter(articles) {
1917
2409
  }
1918
2410
  return issues;
1919
2411
  }
1920
- function checkDiscoveryFieldLengths(articles) {
2412
+ function checkDiscoveryFieldLengths(articles, config) {
2413
+ var _a, _b;
1921
2414
  const issues = [];
1922
2415
  for (const article of articles) {
2416
+ const effectiveTitle = formatPageTitle((_a = article.searchTitle) != null ? _a : article.title, config);
2417
+ const effectiveDescription = (_b = article.searchDescription) != null ? _b : article.excerpt;
1923
2418
  const checks = [
1924
- [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1925
- [article.searchDescription, "search-description-too-long", SEARCH_DESCRIPTION_MAX],
2419
+ [effectiveTitle, "effective-title-too-long", SEARCH_TITLE_MAX],
2420
+ [effectiveDescription, "effective-description-too-long", SEARCH_DESCRIPTION_MAX],
1926
2421
  [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1927
2422
  [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1928
2423
  ];
@@ -1939,6 +2434,101 @@ function checkDiscoveryFieldLengths(articles) {
1939
2434
  }
1940
2435
  return issues;
1941
2436
  }
2437
+ function checkAnswerability(articles) {
2438
+ var _a, _b, _c;
2439
+ const issues = [];
2440
+ for (const article of articles) {
2441
+ const hasQuestionHeading = ((_a = article.toc) != null ? _a : []).some(
2442
+ (item) => item.depth === 2 && item.text.trim().endsWith("?")
2443
+ );
2444
+ if (!article.answer && !((_b = article.faq) == null ? void 0 : _b.length) && !hasQuestionHeading) {
2445
+ issues.push({
2446
+ severity: "warning",
2447
+ code: "no-answer",
2448
+ message: "Article has no `answer`, no `faq`, and no question-shaped heading - nothing for an answer engine to lift.",
2449
+ articleSlug: article.slug
2450
+ });
2451
+ }
2452
+ if (article.wordCount !== void 0 && article.wordCount < THIN_CONTENT_WORDS) {
2453
+ issues.push({
2454
+ severity: "warning",
2455
+ code: "thin-content",
2456
+ message: `Article is ${article.wordCount} words (under ${THIN_CONTENT_WORDS}).`,
2457
+ articleSlug: article.slug
2458
+ });
2459
+ }
2460
+ if (!((_c = article.about) == null ? void 0 : _c.length)) {
2461
+ issues.push({
2462
+ severity: "warning",
2463
+ code: "missing-about",
2464
+ message: "Article has no `about` entity references.",
2465
+ articleSlug: article.slug
2466
+ });
2467
+ }
2468
+ }
2469
+ return issues;
2470
+ }
2471
+ function checkEntityReferences(articles, config) {
2472
+ var _a;
2473
+ const registry = config.entities;
2474
+ if (!registry) return [];
2475
+ const known = new Set(Object.values(registry).map((entity) => entity.name));
2476
+ const issues = [];
2477
+ for (const article of articles) {
2478
+ for (const entity of (_a = article.about) != null ? _a : []) {
2479
+ if (!known.has(entity.name) && !entity.sameAs) {
2480
+ issues.push({
2481
+ severity: "warning",
2482
+ code: "unknown-entity",
2483
+ message: `about entry "${entity.name}" is not in config.entities and has no sameAs.`,
2484
+ articleSlug: article.slug
2485
+ });
2486
+ }
2487
+ }
2488
+ }
2489
+ return issues;
2490
+ }
2491
+ function checkStaleContent(articles, now) {
2492
+ var _a;
2493
+ const cutoff = new Date(now);
2494
+ cutoff.setMonth(cutoff.getMonth() - STALE_CONTENT_MONTHS);
2495
+ const issues = [];
2496
+ for (const article of articles) {
2497
+ const stamp = (_a = article.lastmod) != null ? _a : article.date;
2498
+ if (!stamp) continue;
2499
+ const parsed = new Date(stamp);
2500
+ if (Number.isNaN(parsed.getTime())) continue;
2501
+ if (parsed < cutoff) {
2502
+ issues.push({
2503
+ severity: "warning",
2504
+ code: "stale-content",
2505
+ message: `Last updated ${stamp}, over ${STALE_CONTENT_MONTHS} months ago.`,
2506
+ articleSlug: article.slug
2507
+ });
2508
+ }
2509
+ }
2510
+ return issues;
2511
+ }
2512
+ function checkOrphanArticles(articles) {
2513
+ const bodies = articles.filter((article) => typeof article.content === "string");
2514
+ if (bodies.length === 0) return [];
2515
+ const issues = [];
2516
+ for (const article of articles) {
2517
+ const needle = `/articles/${article.slug}`;
2518
+ const linked = bodies.some(
2519
+ (other) => other.slug !== article.slug && other.content.includes(needle)
2520
+ );
2521
+ if (!linked) {
2522
+ issues.push({
2523
+ severity: "warning",
2524
+ code: "orphan-article",
2525
+ message: "No other article links to this one.",
2526
+ articleSlug: article.slug
2527
+ });
2528
+ }
2529
+ }
2530
+ return issues;
2531
+ }
1942
2532
  function checkCategorySlugs(articles) {
1943
2533
  var _a;
1944
2534
  const issues = [];
@@ -1962,7 +2552,8 @@ function checkCategorySlugs(articles) {
1962
2552
  }
1963
2553
  return issues;
1964
2554
  }
1965
- function validateArticles(articles, config) {
2555
+ function validateArticles(articles, config, options) {
2556
+ var _a;
1966
2557
  const errors = [
1967
2558
  ...checkDuplicateCanonicalUrls(articles),
1968
2559
  ...checkAuthorReferences(articles, config),
@@ -1972,15 +2563,19 @@ function validateArticles(articles, config) {
1972
2563
  ];
1973
2564
  const warnings = [
1974
2565
  ...checkRequiredFrontmatter(articles),
1975
- ...checkDiscoveryFieldLengths(articles),
2566
+ ...checkDiscoveryFieldLengths(articles, config),
2567
+ ...checkAnswerability(articles),
2568
+ ...checkEntityReferences(articles, config),
2569
+ ...checkStaleContent(articles, (_a = options == null ? void 0 : options.now) != null ? _a : /* @__PURE__ */ new Date()),
2570
+ ...checkOrphanArticles(articles),
1976
2571
  ...checkCategorySlugs(articles)
1977
2572
  ];
1978
2573
  return { ok: errors.length === 0, errors, warnings };
1979
2574
  }
1980
- function validateAllArticles(config) {
2575
+ function validateAllArticles(config, options) {
1981
2576
  return __async(this, null, function* () {
1982
2577
  const articles = yield getAllArticles(config);
1983
- return validateArticles(articles, config);
2578
+ return validateArticles(articles, config, options);
1984
2579
  });
1985
2580
  }
1986
2581
 
@@ -1994,16 +2589,21 @@ function emitArticleEvent(handler, event) {
1994
2589
  }
1995
2590
  // Annotate the CommonJS export names for ESM import in node:
1996
2591
  0 && (module.exports = {
2592
+ AI_CRAWLERS,
2593
+ ArticleAnswer,
1997
2594
  ArticleContent,
1998
2595
  ArticleTOC,
1999
2596
  buildArticleBreadcrumbs,
2000
2597
  buildAuthorBreadcrumbs,
2001
2598
  buildCategoryBreadcrumbs,
2599
+ buildMarkdownTwinHeader,
2002
2600
  buildPageUrl,
2003
2601
  buildPaginationLinks,
2004
2602
  categoryToSlug,
2603
+ deriveFaqFromHeadings,
2005
2604
  emitArticleEvent,
2006
2605
  extractToc,
2606
+ formatPageTitle,
2007
2607
  generateArticleMetadata,
2008
2608
  generateArticleStaticParams,
2009
2609
  generateArticlesIndexMetadata,
@@ -2015,6 +2615,8 @@ function emitArticleEvent(handler, event) {
2015
2615
  generateCategoryPageMetadata,
2016
2616
  generateCategoryStaticParams,
2017
2617
  generateListingPageStaticParams,
2618
+ generateLlmsFullTxt,
2619
+ generateLlmsTxt,
2018
2620
  generateRssFeed,
2019
2621
  generateSeriesMetadata,
2020
2622
  generateSeriesStaticParams,
@@ -2035,16 +2637,24 @@ function emitArticleEvent(handler, event) {
2035
2637
  getArticlesByCategory,
2036
2638
  getArticlesBySeries,
2037
2639
  getAuthorBySlug,
2640
+ getAuthorMarkdown,
2038
2641
  getAvailableArticleSlugs,
2039
2642
  getBreadcrumbsConfig,
2643
+ getCategoryMarkdown,
2040
2644
  getContentSlotBoundaries,
2645
+ getMarkdownTwinResponse,
2646
+ getOrganizationId,
2041
2647
  getPath,
2042
2648
  getPathArticles,
2649
+ getPersonId,
2043
2650
  getRelatedArticlesByCategory,
2044
2651
  getRelatedContent,
2652
+ getSeriesMarkdown,
2045
2653
  getTotalPages,
2654
+ getWebSiteId,
2046
2655
  isPageOutOfRange,
2047
2656
  markdownToHtml,
2657
+ matchAiCrawler,
2048
2658
  paginateArticles,
2049
2659
  parsePageParam,
2050
2660
  resolveAuthorAvatar,