@fullstackdatasolutions/articles 1.2.2 → 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 (48) 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 +324 -13
  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 +324 -13
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +677 -51
  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 +662 -51
  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__/linkClassification.test.ts +55 -0
  30. package/src/__tests__/markdown.test.ts +77 -1
  31. package/src/__tests__/nextjs.test.ts +31 -15
  32. package/src/__tests__/renderMdx.test.tsx +162 -3
  33. package/src/__tests__/seoUtils.test.ts +279 -0
  34. package/src/__tests__/server-articles.test.ts +413 -1
  35. package/src/__tests__/validateArticles.test.ts +167 -6
  36. package/src/articleTypes.ts +57 -0
  37. package/src/articlesConfig.ts +176 -1
  38. package/src/authorUtils.ts +19 -1
  39. package/src/errorReporting.ts +1 -0
  40. package/src/index.ts +17 -1
  41. package/src/linkClassification.ts +30 -0
  42. package/src/markdown.ts +103 -25
  43. package/src/nextjs.ts +7 -4
  44. package/src/renderMdx.tsx +43 -6
  45. package/src/seoUtils.ts +247 -26
  46. package/src/server-articles.ts +375 -24
  47. package/src/server.ts +35 -4
  48. 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,
@@ -188,8 +203,7 @@ function reportArticlesError(report) {
188
203
  articlesErrorHandler(report);
189
204
  }
190
205
 
191
- // src/markdown.ts
192
- var DEFAULT_LINK_TARGET_STRATEGY = "external-new-tab";
206
+ // src/linkClassification.ts
193
207
  function isNonBrowserNavigationLink(href) {
194
208
  return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) && !href.startsWith("http://") && !href.startsWith("https://");
195
209
  }
@@ -207,6 +221,9 @@ function isExternalHttpLink(href, siteUrl) {
207
221
  if (!siteOrigin) return true;
208
222
  return getOrigin(href) !== siteOrigin;
209
223
  }
224
+
225
+ // src/markdown.ts
226
+ var DEFAULT_LINK_TARGET_STRATEGY = "external-new-tab";
210
227
  function shouldOpenInNewTab(href, options = {}) {
211
228
  var _a;
212
229
  if (!href || href.startsWith("#") || isNonBrowserNavigationLink(href)) return false;
@@ -522,6 +539,64 @@ function extractToc(markdown) {
522
539
  return headings;
523
540
  });
524
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
+ }
525
600
 
526
601
  // src/server-articles.ts
527
602
  var articlesDirectory = import_node_path.default.join(
@@ -654,6 +729,58 @@ function parseHowToSteps(raw) {
654
729
  function parseOptionalString(raw) {
655
730
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
656
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
+ }
657
784
  function parseSeriesOrder(raw) {
658
785
  return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
659
786
  }
@@ -730,6 +857,7 @@ function getAllAuthors(config) {
730
857
  }
731
858
  function getArticleSummary(slug, config) {
732
859
  return __async(this, null, function* () {
860
+ var _a;
733
861
  try {
734
862
  const found = findArticleFile(slug);
735
863
  if (!found) return null;
@@ -746,7 +874,7 @@ function getArticleSummary(slug, config) {
746
874
  title: data.title || slug.replaceAll("-", " "),
747
875
  excerpt: data.excerpt || "",
748
876
  date: parseDateField(data.date),
749
- lastmod: parseDateField(data.lastmod),
877
+ lastmod: resolveLastmod(data.lastmod, data.date, found.filePath, config),
750
878
  author,
751
879
  authors,
752
880
  authorSlug: primaryAuthorProfile == null ? void 0 : primaryAuthorProfile.slug,
@@ -759,14 +887,17 @@ function getArticleSummary(slug, config) {
759
887
  tags: data.tags || [],
760
888
  contentType: found.contentType,
761
889
  draft: data.draft === true,
762
- faq: parseFaqItems(data.faq),
890
+ faq: (_a = parseFaqItems(data.faq)) != null ? _a : deriveFaq(markdownContent, config),
763
891
  howTo: parseHowToSteps(data.howTo),
892
+ answer: parseOptionalString(data.answer),
893
+ about: parseEntityReferences(data.about, config),
894
+ citation: parseCitations(data.citation),
764
895
  canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
765
896
  articleType: typeof data.articleType === "string" ? data.articleType : void 0,
766
897
  series: typeof data.series === "string" ? data.series : void 0,
767
898
  seriesSlug: parseOptionalString(data.seriesSlug),
768
899
  seriesOrder: parseSeriesOrder(data.seriesOrder),
769
- aiCrawl: data.aiCrawl === true,
900
+ aiCrawl: resolveAiCrawl(data.aiCrawl, config),
770
901
  searchTitle: parseOptionalString(data.searchTitle),
771
902
  searchDescription: parseOptionalString(data.searchDescription),
772
903
  socialTitle: parseOptionalString(data.socialTitle),
@@ -835,10 +966,10 @@ function getAdjacentArticles(currentSlug) {
835
966
  return { previous, next };
836
967
  });
837
968
  }
838
- function getArticleMarkdown(slug) {
969
+ function getArticleMarkdown(slug, config) {
839
970
  return __async(this, null, function* () {
840
971
  try {
841
- const summary = yield getArticleSummary(slug);
972
+ const summary = yield getArticleSummary(slug, config);
842
973
  if (!(summary == null ? void 0 : summary.aiCrawl)) return null;
843
974
  const found = findArticleFile(slug);
844
975
  if (!found) return null;
@@ -856,12 +987,180 @@ function getArticleMarkdown(slug) {
856
987
  }
857
988
  });
858
989
  }
859
- 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) {
860
1146
  return __async(this, null, function* () {
861
- const markdown = yield getArticleMarkdown(slug);
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) {
1157
+ return __async(this, null, function* () {
1158
+ const markdown = yield getArticleMarkdown(slug, config);
862
1159
  if (markdown === null) return new Response("Not Found", { status: 404 });
863
- const article = yield getArticleMetadata(slug);
864
- 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, {
865
1164
  headers: __spreadValues({
866
1165
  "Content-Type": "text/markdown; charset=utf-8",
867
1166
  "Cache-Control": "public, max-age=3600, s-maxage=3600"
@@ -886,22 +1185,15 @@ function getArticleAiHeaders(article, config) {
886
1185
  "X-Robots-Tag": "noai, noimageai"
887
1186
  };
888
1187
  }
889
- function getAiRobotsTxtRules() {
1188
+ function getAiRobotsTxtRules(config) {
890
1189
  return __async(this, null, function* () {
891
- const articles = yield getAllArticles();
1190
+ const articles = yield getAllArticles(config);
892
1191
  const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
893
1192
  if (blockedArticles.length === 0) return "";
894
- const aiCrawlers = [
895
- "GPTBot",
896
- "ChatGPT-User",
897
- "CCBot",
898
- "ClaudeBot",
899
- "Claude-User",
900
- "PerplexityBot",
901
- "Google-Extended"
902
- ];
903
1193
  const disallowRules = blockedArticles.map((article) => `Disallow: /articles/${article.slug}`).join("\n");
904
- 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
+ );
905
1197
  });
906
1198
  }
907
1199
  function searchArticles(query, config) {
@@ -1041,6 +1333,7 @@ function getRelatedContent(article, config, limit = 3) {
1041
1333
  }
1042
1334
 
1043
1335
  // src/articlesConfig.ts
1336
+ var DEFAULT_PAGE_SIZE = 6;
1044
1337
  function breadcrumbsAreEnabled(config) {
1045
1338
  var _a;
1046
1339
  return config.breadcrumbs !== false && ((_a = config.breadcrumbs) == null ? void 0 : _a.show) !== false;
@@ -1050,6 +1343,20 @@ function getBreadcrumbsConfig(config) {
1050
1343
  if (config.breadcrumbs === false) return {};
1051
1344
  return (_a = config.breadcrumbs) != null ? _a : {};
1052
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
+ }
1053
1360
 
1054
1361
  // src/pagination.ts
1055
1362
  function getTotalPages(totalCount, pageSize) {
@@ -1097,10 +1404,11 @@ function isPageOutOfRange(page, totalPages) {
1097
1404
  function escapeXml(str) {
1098
1405
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1099
1406
  }
1100
- function generateRssFeed(articles, config) {
1101
- var _a;
1407
+ function generateRssFeed(articles, config, options) {
1408
+ var _a, _b;
1102
1409
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1103
1410
  const showAuthor = config.showAuthor !== false;
1411
+ const fullContent = (options == null ? void 0 : options.fullContent) === true;
1104
1412
  const items = articles.map((article) => {
1105
1413
  const url = `${siteUrl}/articles/${article.slug}`;
1106
1414
  const pubDate = article.date ? new Date(article.date).toUTCString() : "";
@@ -1112,6 +1420,7 @@ function generateRssFeed(articles, config) {
1112
1420
  ` <guid isPermaLink="true">${url}</guid>`,
1113
1421
  pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
1114
1422
  article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : "",
1423
+ fullContent && article.htmlContent ? ` <content:encoded><![CDATA[${article.htmlContent}]]></content:encoded>` : "",
1115
1424
  showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : "",
1116
1425
  article.category ? ` <category><![CDATA[${article.category}]]></category>` : "",
1117
1426
  imageUrl ? ` <media:content url="${imageUrl}" medium="image" width="1200" height="630"/>` : "",
@@ -1120,17 +1429,82 @@ function generateRssFeed(articles, config) {
1120
1429
  }).join("\n");
1121
1430
  const description = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1122
1431
  return `<?xml version="1.0" encoding="UTF-8" ?>
1123
- <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/">
1124
1433
  <channel>
1125
1434
  <title><![CDATA[${config.siteName}]]></title>
1126
1435
  <link>${siteUrl}/articles</link>
1127
1436
  <description><![CDATA[${description}]]></description>
1128
- <language>en</language>
1437
+ <language>${(_b = config.language) != null ? _b : "en"}</language>
1129
1438
  <atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
1130
1439
  ${items}
1131
1440
  </channel>
1132
1441
  </rss>`;
1133
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
+ }
1134
1508
  function generateArticleStaticParams() {
1135
1509
  return getAvailableArticleSlugs().map((slug) => ({ slug }));
1136
1510
  }
@@ -1196,7 +1570,7 @@ function generateArticleMetadata(slug, config) {
1196
1570
  const markdownUrl = getArticleMarkdownUrl(article, config);
1197
1571
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
1198
1572
  return {
1199
- title: `${search.title} | ${config.siteName}`,
1573
+ title: formatPageTitle(search.title, config),
1200
1574
  description,
1201
1575
  keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
1202
1576
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
@@ -1250,7 +1624,7 @@ function generateArticlesIndexMetadata(config) {
1250
1624
  var _a, _b;
1251
1625
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1252
1626
  const indexUrl = `${siteUrl}/articles`;
1253
- const title = `Articles | ${config.siteName}`;
1627
+ const title = formatPageTitle("Articles", config);
1254
1628
  const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
1255
1629
  return {
1256
1630
  title,
@@ -1298,7 +1672,7 @@ function generateCategoryMetadata(categorySlug, config) {
1298
1672
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
1299
1673
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
1300
1674
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
1301
- const title = `${categoryName} Articles | ${config.siteName}`;
1675
+ const title = formatPageTitle(`${categoryName} Articles`, config);
1302
1676
  return {
1303
1677
  title,
1304
1678
  description,
@@ -1342,7 +1716,7 @@ function generateSeriesMetadata(seriesSlug, config) {
1342
1716
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1343
1717
  const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1344
1718
  const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1345
- const title = `${seriesName} Series | ${config.siteName}`;
1719
+ const title = formatPageTitle(`${seriesName} Series`, config);
1346
1720
  return {
1347
1721
  title,
1348
1722
  description,
@@ -1384,7 +1758,7 @@ function generateAuthorMetadata(authorSlug, config) {
1384
1758
  if (!author || config.showAuthorPage === false) return { title: "Author Not Found" };
1385
1759
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1386
1760
  const authorUrl = (_a = author.url) != null ? _a : `${siteUrl}/articles/authors/${author.slug}`;
1387
- const title = `${author.name} Articles | ${config.siteName}`;
1761
+ const title = formatPageTitle(`${author.name} Articles`, config);
1388
1762
  return {
1389
1763
  title,
1390
1764
  description: author.bio,
@@ -1566,17 +1940,41 @@ function resolveAuthorAvatar(author, config) {
1566
1940
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1567
1941
  return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
1568
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
+ }
1569
1968
  function getArticleSitemapEntries(baseUrlOrConfig) {
1570
1969
  return __async(this, null, function* () {
1571
- 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(/\/$/, "");
1572
1973
  try {
1573
- const [articles, categories] = yield Promise.all([
1574
- getAllArticles(typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig),
1575
- getAllCategories()
1576
- ]);
1974
+ const [articles, categories] = yield Promise.all([getAllArticles(config), getAllCategories()]);
1577
1975
  const articleEntries = articles.map((article) => {
1578
- var _a;
1579
- const dateStr = (_a = article.lastmod) != null ? _a : article.date;
1976
+ var _a2;
1977
+ const dateStr = (_a2 = article.lastmod) != null ? _a2 : article.date;
1580
1978
  const lastModified = dateStr ? new Date(dateStr) : void 0;
1581
1979
  return {
1582
1980
  url: `${baseUrl}/articles/${article.slug}`,
@@ -1587,16 +1985,86 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1587
1985
  });
1588
1986
  const categoryEntries = categories.map((cat) => ({
1589
1987
  url: `${baseUrl}/articles/category/${cat.slug}`,
1590
- 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
+ ),
1591
1996
  changeFrequency: "weekly",
1592
1997
  priority: 0.7
1593
1998
  }));
1594
- 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) => ({
1595
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
+ ),
1596
2007
  changeFrequency: "monthly",
1597
2008
  priority: 0.6
1598
2009
  }));
1599
- 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
+ ];
1600
2068
  } catch (e) {
1601
2069
  return [];
1602
2070
  }
@@ -1605,6 +2073,7 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1605
2073
 
1606
2074
  // src/renderMdx.tsx
1607
2075
  var import_react2 = __toESM(require("react"), 1);
2076
+ var import_link = __toESM(require("next/link"), 1);
1608
2077
  var devRuntime = __toESM(require("react/jsx-dev-runtime"), 1);
1609
2078
  var runtime = __toESM(require("react/jsx-runtime"), 1);
1610
2079
  var import_mdx = require("@mdx-js/mdx");
@@ -1620,6 +2089,19 @@ function makeImgComponent(basePath) {
1620
2089
  return import_react2.default.createElement("img", __spreadValues({ src: resolvedSrc, alt }, props));
1621
2090
  };
1622
2091
  }
2092
+ function isInternalNavigableHref(href, siteUrl) {
2093
+ if (!href || href.startsWith("#") || isNonBrowserNavigationLink(href)) return false;
2094
+ return !isExternalHttpLink(href, siteUrl);
2095
+ }
2096
+ function makeLinkComponent(siteUrl) {
2097
+ return function MdxLink(_a) {
2098
+ var _b = _a, { href, children } = _b, props = __objRest(_b, ["href", "children"]);
2099
+ if (typeof href === "string" && isInternalNavigableHref(href, siteUrl)) {
2100
+ return import_react2.default.createElement(import_link.default, __spreadValues({ href }, props), children);
2101
+ }
2102
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", __spreadProps(__spreadValues({ href }, props), { children }));
2103
+ };
2104
+ }
1623
2105
  function renderMdxSource(source, basePath, config) {
1624
2106
  return __async(this, null, function* () {
1625
2107
  const isDevelopment = process.env.NODE_ENV === "development";
@@ -1634,8 +2116,10 @@ function renderMdxSource(source, basePath, config) {
1634
2116
  ]
1635
2117
  }));
1636
2118
  const Content = mdxModule.default;
1637
- const internalComponents = basePath ? { img: makeImgComponent(basePath) } : void 0;
1638
- const components = internalComponents || (config == null ? void 0 : config.mdxComponents) ? __spreadValues(__spreadValues({}, internalComponents), config == null ? void 0 : config.mdxComponents) : void 0;
2119
+ const internalComponents = __spreadValues({
2120
+ a: makeLinkComponent(config == null ? void 0 : config.siteUrl)
2121
+ }, basePath ? { img: makeImgComponent(basePath) } : {});
2122
+ const components = __spreadValues(__spreadValues({}, internalComponents), config == null ? void 0 : config.mdxComponents);
1639
2123
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Content, { components });
1640
2124
  });
1641
2125
  }
@@ -1754,8 +2238,32 @@ function ArticleTOC({ toc, className }) {
1754
2238
  );
1755
2239
  }
1756
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
+
1757
2263
  // src/validateArticles.ts
1758
2264
  var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
2265
+ var THIN_CONTENT_WORDS = 300;
2266
+ var STALE_CONTENT_MONTHS = 18;
1759
2267
  var SEARCH_TITLE_MAX = 60;
1760
2268
  var SEARCH_DESCRIPTION_MAX = 160;
1761
2269
  var SOCIAL_TITLE_MAX = 95;
@@ -1899,12 +2407,15 @@ function checkRequiredFrontmatter(articles) {
1899
2407
  }
1900
2408
  return issues;
1901
2409
  }
1902
- function checkDiscoveryFieldLengths(articles) {
2410
+ function checkDiscoveryFieldLengths(articles, config) {
2411
+ var _a, _b;
1903
2412
  const issues = [];
1904
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;
1905
2416
  const checks = [
1906
- [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1907
- [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],
1908
2419
  [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1909
2420
  [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1910
2421
  ];
@@ -1921,6 +2432,101 @@ function checkDiscoveryFieldLengths(articles) {
1921
2432
  }
1922
2433
  return issues;
1923
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
+ }
1924
2530
  function checkCategorySlugs(articles) {
1925
2531
  var _a;
1926
2532
  const issues = [];
@@ -1944,7 +2550,8 @@ function checkCategorySlugs(articles) {
1944
2550
  }
1945
2551
  return issues;
1946
2552
  }
1947
- function validateArticles(articles, config) {
2553
+ function validateArticles(articles, config, options) {
2554
+ var _a;
1948
2555
  const errors = [
1949
2556
  ...checkDuplicateCanonicalUrls(articles),
1950
2557
  ...checkAuthorReferences(articles, config),
@@ -1954,15 +2561,19 @@ function validateArticles(articles, config) {
1954
2561
  ];
1955
2562
  const warnings = [
1956
2563
  ...checkRequiredFrontmatter(articles),
1957
- ...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),
1958
2569
  ...checkCategorySlugs(articles)
1959
2570
  ];
1960
2571
  return { ok: errors.length === 0, errors, warnings };
1961
2572
  }
1962
- function validateAllArticles(config) {
2573
+ function validateAllArticles(config, options) {
1963
2574
  return __async(this, null, function* () {
1964
2575
  const articles = yield getAllArticles(config);
1965
- return validateArticles(articles, config);
2576
+ return validateArticles(articles, config, options);
1966
2577
  });
1967
2578
  }
1968
2579
 
@@ -1976,16 +2587,21 @@ function emitArticleEvent(handler, event) {
1976
2587
  }
1977
2588
  // Annotate the CommonJS export names for ESM import in node:
1978
2589
  0 && (module.exports = {
2590
+ AI_CRAWLERS,
2591
+ ArticleAnswer,
1979
2592
  ArticleContent,
1980
2593
  ArticleTOC,
1981
2594
  buildArticleBreadcrumbs,
1982
2595
  buildAuthorBreadcrumbs,
1983
2596
  buildCategoryBreadcrumbs,
2597
+ buildMarkdownTwinHeader,
1984
2598
  buildPageUrl,
1985
2599
  buildPaginationLinks,
1986
2600
  categoryToSlug,
2601
+ deriveFaqFromHeadings,
1987
2602
  emitArticleEvent,
1988
2603
  extractToc,
2604
+ formatPageTitle,
1989
2605
  generateArticleMetadata,
1990
2606
  generateArticleStaticParams,
1991
2607
  generateArticlesIndexMetadata,
@@ -1997,6 +2613,8 @@ function emitArticleEvent(handler, event) {
1997
2613
  generateCategoryPageMetadata,
1998
2614
  generateCategoryStaticParams,
1999
2615
  generateListingPageStaticParams,
2616
+ generateLlmsFullTxt,
2617
+ generateLlmsTxt,
2000
2618
  generateRssFeed,
2001
2619
  generateSeriesMetadata,
2002
2620
  generateSeriesStaticParams,
@@ -2017,16 +2635,24 @@ function emitArticleEvent(handler, event) {
2017
2635
  getArticlesByCategory,
2018
2636
  getArticlesBySeries,
2019
2637
  getAuthorBySlug,
2638
+ getAuthorMarkdown,
2020
2639
  getAvailableArticleSlugs,
2021
2640
  getBreadcrumbsConfig,
2641
+ getCategoryMarkdown,
2022
2642
  getContentSlotBoundaries,
2643
+ getMarkdownTwinResponse,
2644
+ getOrganizationId,
2023
2645
  getPath,
2024
2646
  getPathArticles,
2647
+ getPersonId,
2025
2648
  getRelatedArticlesByCategory,
2026
2649
  getRelatedContent,
2650
+ getSeriesMarkdown,
2027
2651
  getTotalPages,
2652
+ getWebSiteId,
2028
2653
  isPageOutOfRange,
2029
2654
  markdownToHtml,
2655
+ matchAiCrawler,
2030
2656
  paginateArticles,
2031
2657
  parsePageParam,
2032
2658
  resolveAuthorAvatar,