@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +40 -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 +320 -11
  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 +320 -11
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +655 -47
  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 +640 -47
  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 +413 -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 +375 -24
  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,12 +987,180 @@ 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, {
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 aiCrawlers.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join("\n\n");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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>en</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: `${search.title} | ${config.siteName}`,
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 = `Articles | ${config.siteName}`;
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 | ${config.siteName}`;
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 | ${config.siteName}`;
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 | ${config.siteName}`;
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
- const baseUrl = (typeof baseUrlOrConfig === "string" ? baseUrlOrConfig : baseUrlOrConfig.siteUrl).replace(/\/$/, "");
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 _a;
1581
- const dateStr = (_a = article.lastmod) != null ? _a : article.date;
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: /* @__PURE__ */ new Date(),
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 authorEntries = typeof baseUrlOrConfig === "string" || baseUrlOrConfig.showAuthorPage === false ? [] : getAllAuthors(baseUrlOrConfig).map((author) => ({
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
- return [...articleEntries, ...categoryEntries, ...authorEntries];
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
- [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1925
- [article.searchDescription, "search-description-too-long", SEARCH_DESCRIPTION_MAX],
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,