@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.js CHANGED
@@ -95,8 +95,7 @@ function reportArticlesError(report) {
95
95
  articlesErrorHandler(report);
96
96
  }
97
97
 
98
- // src/markdown.ts
99
- var DEFAULT_LINK_TARGET_STRATEGY = "external-new-tab";
98
+ // src/linkClassification.ts
100
99
  function isNonBrowserNavigationLink(href) {
101
100
  return /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href) && !href.startsWith("http://") && !href.startsWith("https://");
102
101
  }
@@ -114,6 +113,9 @@ function isExternalHttpLink(href, siteUrl) {
114
113
  if (!siteOrigin) return true;
115
114
  return getOrigin(href) !== siteOrigin;
116
115
  }
116
+
117
+ // src/markdown.ts
118
+ var DEFAULT_LINK_TARGET_STRATEGY = "external-new-tab";
117
119
  function shouldOpenInNewTab(href, options = {}) {
118
120
  var _a;
119
121
  if (!href || href.startsWith("#") || isNonBrowserNavigationLink(href)) return false;
@@ -429,6 +431,64 @@ function extractToc(markdown) {
429
431
  return headings;
430
432
  });
431
433
  }
434
+ var QUESTION_OPENERS = /^(what|how|why|when|where|who|which|can|should|does|do|is|are|will|would|must)\b/i;
435
+ function deriveFaqFromHeadings(markdown) {
436
+ const items = [];
437
+ const collector = createFaqCollector(items);
438
+ let inFence = false;
439
+ for (const line of markdown.split("\n")) {
440
+ if (FENCE.test(line)) {
441
+ inFence = !inFence;
442
+ continue;
443
+ }
444
+ if (inFence) continue;
445
+ collector.consume(line);
446
+ }
447
+ collector.flush();
448
+ return items;
449
+ }
450
+ var FENCE = /^\s*(```|~~~)/;
451
+ var ANY_HEADING = /^#{1,6}\s/;
452
+ function isSpace(char) {
453
+ return char === " " || char === " ";
454
+ }
455
+ function trimTrailingHashes(value) {
456
+ let end = value.length;
457
+ while (end > 0 && value[end - 1] === "#") end--;
458
+ return value.slice(0, end).trimEnd();
459
+ }
460
+ function readQuestionHeading(line) {
461
+ if (!line.startsWith("##") || line.startsWith("###")) return null;
462
+ if (!isSpace(line[2])) return null;
463
+ const text = trimTrailingHashes(line.slice(3).trim());
464
+ if (!text.endsWith("?") || !QUESTION_OPENERS.test(text)) return null;
465
+ return text;
466
+ }
467
+ function createFaqCollector(items) {
468
+ let pending = null;
469
+ let buffer = [];
470
+ const flush = () => {
471
+ if (pending && buffer.length > 0) {
472
+ items.push({ question: pending, answer: buffer.join(" ").trim() });
473
+ }
474
+ pending = null;
475
+ buffer = [];
476
+ };
477
+ const consume = (line) => {
478
+ if (ANY_HEADING.test(line)) {
479
+ flush();
480
+ pending = readQuestionHeading(line);
481
+ return;
482
+ }
483
+ if (!pending) return;
484
+ if (line.trim() === "") {
485
+ if (buffer.length > 0) flush();
486
+ return;
487
+ }
488
+ buffer.push(line.trim());
489
+ };
490
+ return { consume, flush };
491
+ }
432
492
 
433
493
  // src/server-articles.ts
434
494
  var articlesDirectory = path.join(
@@ -561,6 +621,58 @@ function parseHowToSteps(raw) {
561
621
  function parseOptionalString(raw) {
562
622
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
563
623
  }
624
+ function deriveFaq(markdownContent, config) {
625
+ if ((config == null ? void 0 : config.deriveFaqFromHeadings) !== true) return void 0;
626
+ const derived = deriveFaqFromHeadings(markdownContent);
627
+ return derived.length ? derived : void 0;
628
+ }
629
+ function resolveLastmod(rawLastmod, rawDate, filePath, config) {
630
+ const declared = parseDateField(rawLastmod);
631
+ if (declared) return declared;
632
+ if ((config == null ? void 0 : config.lastmodFallback) !== "fileMtime") return void 0;
633
+ try {
634
+ return parseDateField(fs.statSync(filePath).mtime);
635
+ } catch (e) {
636
+ return parseDateField(rawDate);
637
+ }
638
+ }
639
+ function resolveAiCrawl(raw, config) {
640
+ if (typeof raw === "boolean") return raw;
641
+ return (config == null ? void 0 : config.aiCrawlDefault) === true;
642
+ }
643
+ function parseEntityReferences(raw, config) {
644
+ if (!Array.isArray(raw)) return void 0;
645
+ const items = raw.map((item) => {
646
+ var _a, _b;
647
+ if (typeof item === "string") {
648
+ const key = item.trim();
649
+ if (!key) return null;
650
+ return (_b = (_a = config == null ? void 0 : config.entities) == null ? void 0 : _a[key]) != null ? _b : { name: key };
651
+ }
652
+ if (typeof item === "object" && item !== null && typeof item.name === "string") {
653
+ const entity = item;
654
+ const name = entity.name.trim();
655
+ if (!name) return null;
656
+ return entity.sameAs ? { name, sameAs: entity.sameAs } : { name };
657
+ }
658
+ return null;
659
+ }).filter((item) => item !== null);
660
+ return items.length ? items : void 0;
661
+ }
662
+ function parseCitations(raw) {
663
+ if (!Array.isArray(raw)) return void 0;
664
+ const items = raw.map((item) => {
665
+ if (typeof item === "string") return item.trim() ? { name: item.trim() } : null;
666
+ if (typeof item === "object" && item !== null && typeof item.name === "string") {
667
+ const citation = item;
668
+ const name = citation.name.trim();
669
+ if (!name) return null;
670
+ return citation.url ? { name, url: citation.url } : { name };
671
+ }
672
+ return null;
673
+ }).filter((item) => item !== null);
674
+ return items.length ? items : void 0;
675
+ }
564
676
  function parseSeriesOrder(raw) {
565
677
  return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
566
678
  }
@@ -637,6 +749,7 @@ function getAllAuthors(config) {
637
749
  }
638
750
  function getArticleSummary(slug, config) {
639
751
  return __async(this, null, function* () {
752
+ var _a;
640
753
  try {
641
754
  const found = findArticleFile(slug);
642
755
  if (!found) return null;
@@ -653,7 +766,7 @@ function getArticleSummary(slug, config) {
653
766
  title: data.title || slug.replaceAll("-", " "),
654
767
  excerpt: data.excerpt || "",
655
768
  date: parseDateField(data.date),
656
- lastmod: parseDateField(data.lastmod),
769
+ lastmod: resolveLastmod(data.lastmod, data.date, found.filePath, config),
657
770
  author,
658
771
  authors,
659
772
  authorSlug: primaryAuthorProfile == null ? void 0 : primaryAuthorProfile.slug,
@@ -666,14 +779,17 @@ function getArticleSummary(slug, config) {
666
779
  tags: data.tags || [],
667
780
  contentType: found.contentType,
668
781
  draft: data.draft === true,
669
- faq: parseFaqItems(data.faq),
782
+ faq: (_a = parseFaqItems(data.faq)) != null ? _a : deriveFaq(markdownContent, config),
670
783
  howTo: parseHowToSteps(data.howTo),
784
+ answer: parseOptionalString(data.answer),
785
+ about: parseEntityReferences(data.about, config),
786
+ citation: parseCitations(data.citation),
671
787
  canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
672
788
  articleType: typeof data.articleType === "string" ? data.articleType : void 0,
673
789
  series: typeof data.series === "string" ? data.series : void 0,
674
790
  seriesSlug: parseOptionalString(data.seriesSlug),
675
791
  seriesOrder: parseSeriesOrder(data.seriesOrder),
676
- aiCrawl: data.aiCrawl === true,
792
+ aiCrawl: resolveAiCrawl(data.aiCrawl, config),
677
793
  searchTitle: parseOptionalString(data.searchTitle),
678
794
  searchDescription: parseOptionalString(data.searchDescription),
679
795
  socialTitle: parseOptionalString(data.socialTitle),
@@ -742,10 +858,10 @@ function getAdjacentArticles(currentSlug) {
742
858
  return { previous, next };
743
859
  });
744
860
  }
745
- function getArticleMarkdown(slug) {
861
+ function getArticleMarkdown(slug, config) {
746
862
  return __async(this, null, function* () {
747
863
  try {
748
- const summary = yield getArticleSummary(slug);
864
+ const summary = yield getArticleSummary(slug, config);
749
865
  if (!(summary == null ? void 0 : summary.aiCrawl)) return null;
750
866
  const found = findArticleFile(slug);
751
867
  if (!found) return null;
@@ -763,12 +879,180 @@ function getArticleMarkdown(slug) {
763
879
  }
764
880
  });
765
881
  }
766
- function getArticleMarkdownResponse(slug, config) {
882
+ var AI_CRAWLERS = [
883
+ "GPTBot",
884
+ "ChatGPT-User",
885
+ "OAI-SearchBot",
886
+ "CCBot",
887
+ "ClaudeBot",
888
+ "Claude-User",
889
+ "Claude-SearchBot",
890
+ "anthropic-ai",
891
+ "PerplexityBot",
892
+ "Perplexity-User",
893
+ "Google-Extended",
894
+ "Applebot-Extended",
895
+ "Bytespider",
896
+ "Amazonbot",
897
+ "meta-externalagent",
898
+ "cohere-ai",
899
+ "DuckAssistBot",
900
+ "MistralAI-User"
901
+ ];
902
+ function matchAiCrawler(userAgent) {
903
+ var _a;
904
+ if (!userAgent) return null;
905
+ const normalized = userAgent.toLowerCase();
906
+ return (_a = AI_CRAWLERS.find((crawler) => normalized.includes(crawler.toLowerCase()))) != null ? _a : null;
907
+ }
908
+ function buildMarkdownTwinHeader(article, config, body) {
909
+ var _a, _b;
910
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
911
+ const firstLine = (_b = (_a = body.trimStart().split("\n", 1)[0]) == null ? void 0 : _a.trim()) != null ? _b : "";
912
+ const bodyRepeatsTitle = firstLine.toLowerCase() === `# ${article.title}`.toLowerCase();
913
+ const facts = [
914
+ `Source: ${siteUrl}/articles/${article.slug}`,
915
+ article.date ? `Published: ${article.date}` : "",
916
+ article.lastmod ? `Updated: ${article.lastmod}` : "",
917
+ config.showAuthor !== false && article.author ? `Author: ${article.author}` : "",
918
+ `Site: ${config.siteName}`
919
+ ].filter(Boolean);
920
+ const blocks = [
921
+ bodyRepeatsTitle ? "" : `# ${article.title}`,
922
+ article.excerpt ? `> ${article.excerpt}` : "",
923
+ facts.join("\n"),
924
+ article.answer ? `**Short answer:** ${article.answer}` : "",
925
+ "---"
926
+ ].filter((block) => block !== "");
927
+ return `${blocks.join("\n\n")}
928
+
929
+ `;
930
+ }
931
+ function reportAiCrawl(slug, config, headers) {
932
+ var _a, _b;
933
+ if (!config.onAiCrawl) return;
934
+ const userAgent = (_a = headers == null ? void 0 : headers.get("user-agent")) != null ? _a : "";
935
+ try {
936
+ config.onAiCrawl({ slug, crawler: (_b = matchAiCrawler(userAgent)) != null ? _b : "unknown", userAgent });
937
+ } catch (error) {
938
+ reportArticlesError({
939
+ code: "ai-crawl-handler-failed",
940
+ message: "onAiCrawl handler threw.",
941
+ error,
942
+ context: { slug }
943
+ });
944
+ }
945
+ }
946
+ function buildListingMarkdown(heading, intro, articles, config) {
947
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
948
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
949
+ const entries = crawlable.map((article) => {
950
+ var _a;
951
+ const summary = (_a = article.answer) != null ? _a : article.excerpt;
952
+ const line = `- [${article.title}](${siteUrl}/articles/${article.slug}.md)`;
953
+ return summary ? `${line}: ${summary}` : line;
954
+ });
955
+ return [
956
+ `# ${heading}`,
957
+ "",
958
+ ...intro.flatMap((line) => [line, ""]),
959
+ `Source: ${siteUrl}`,
960
+ `Site: ${config.siteName}`,
961
+ "",
962
+ "---",
963
+ "",
964
+ ...entries.length > 0 ? entries : ["_No articles available._"],
965
+ ""
966
+ ].join("\n");
967
+ }
968
+ function getCategoryMarkdown(categorySlug, config) {
969
+ return __async(this, null, function* () {
970
+ var _a;
971
+ const articles = yield getArticlesByCategory(categorySlug, config);
972
+ if (articles.length === 0) return null;
973
+ const name = (_a = articles[0].categories.find((c) => categoryToSlug(c) === categorySlug)) != null ? _a : categorySlug;
974
+ const description = resolveCategoryDescription(categorySlug, config);
975
+ return buildListingMarkdown(name, description ? [description] : [], articles, config);
976
+ });
977
+ }
978
+ function resolveCategoryDescription(categorySlug, config) {
979
+ var _a, _b;
980
+ const entry = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
981
+ if (!entry) return void 0;
982
+ return typeof entry === "string" ? entry : (_b = entry.long) != null ? _b : entry.short;
983
+ }
984
+ function getAuthorMarkdown(authorSlug, config) {
985
+ return __async(this, null, function* () {
986
+ var _a, _b, _c, _d, _e, _f, _g;
987
+ const author = getAuthorBySlug(authorSlug, config);
988
+ if (!author) return null;
989
+ const articles = yield getArticlesByAuthor(authorSlug, config);
990
+ const intro = [
991
+ (_a = author.promise) != null ? _a : "",
992
+ (_b = author.bio) != null ? _b : "",
993
+ ...((_c = author.servesWho) == null ? void 0 : _c.length) ? [`Writes for: ${author.servesWho.join(", ")}`] : [],
994
+ ...((_d = author.knowsAbout) == null ? void 0 : _d.length) ? [`Writes about: ${author.knowsAbout.join(", ")}`] : [],
995
+ ...((_e = author.credentials) == null ? void 0 : _e.length) ? ["## Stated experience", ...author.credentials.map((item) => `- ${item}`)] : [],
996
+ ...((_f = author.proof) == null ? void 0 : _f.length) ? [
997
+ "## Proof points",
998
+ ...author.proof.map(
999
+ (item) => item.url ? `- [${item.claim}](${item.url})` : `- ${item.claim}`
1000
+ )
1001
+ ] : [],
1002
+ ...((_g = author.originStory) == null ? void 0 : _g.length) ? [
1003
+ "## Background",
1004
+ ...author.originStory.flatMap((section) => [
1005
+ ...section.heading ? [`### ${section.heading}`] : [],
1006
+ ...section.paragraphs
1007
+ ])
1008
+ ] : []
1009
+ ].filter((line) => line.trim() !== "");
1010
+ return buildListingMarkdown(author.name, intro, articles, config);
1011
+ });
1012
+ }
1013
+ function getSeriesMarkdown(seriesSlug, config) {
1014
+ return __async(this, null, function* () {
1015
+ var _a;
1016
+ const articles = yield getArticlesBySeries(seriesSlug, config);
1017
+ if (articles.length === 0) return null;
1018
+ const name = (_a = articles[0].series) != null ? _a : seriesSlug;
1019
+ return buildListingMarkdown(name, [], articles, config);
1020
+ });
1021
+ }
1022
+ function getMarkdownTwinResponse(slug, config, options) {
1023
+ return __async(this, null, function* () {
1024
+ const listing = yield resolveListingMarkdown(slug, config);
1025
+ if (listing !== void 0) {
1026
+ if (listing === null) return new Response("Not Found", { status: 404 });
1027
+ reportAiCrawl(slug, config, options == null ? void 0 : options.headers);
1028
+ return new Response(listing, { headers: LISTING_MARKDOWN_HEADERS });
1029
+ }
1030
+ return getArticleMarkdownResponse(slug, config, options);
1031
+ });
1032
+ }
1033
+ var LISTING_MARKDOWN_HEADERS = {
1034
+ "Content-Type": "text/markdown; charset=utf-8",
1035
+ "Cache-Control": "public, max-age=3600, s-maxage=3600"
1036
+ };
1037
+ function resolveListingMarkdown(slug, config) {
767
1038
  return __async(this, null, function* () {
768
- const markdown = yield getArticleMarkdown(slug);
1039
+ const [prefix, ...rest] = slug.split("/");
1040
+ const key = rest.join("/");
1041
+ if (!key) return void 0;
1042
+ if (prefix === "category") return getCategoryMarkdown(key, config);
1043
+ if (prefix === "authors") return getAuthorMarkdown(key, config);
1044
+ if (prefix === "series") return getSeriesMarkdown(key, config);
1045
+ return void 0;
1046
+ });
1047
+ }
1048
+ function getArticleMarkdownResponse(slug, config, options) {
1049
+ return __async(this, null, function* () {
1050
+ const markdown = yield getArticleMarkdown(slug, config);
769
1051
  if (markdown === null) return new Response("Not Found", { status: 404 });
770
- const article = yield getArticleMetadata(slug);
771
- return new Response(markdown, {
1052
+ const article = yield getArticleMetadata(slug, config);
1053
+ reportAiCrawl(slug, config, options == null ? void 0 : options.headers);
1054
+ const body = article && config.markdownTwinHeader !== false ? `${buildMarkdownTwinHeader(article, config, markdown)}${markdown.trimStart()}` : markdown;
1055
+ return new Response(body, {
772
1056
  headers: __spreadValues({
773
1057
  "Content-Type": "text/markdown; charset=utf-8",
774
1058
  "Cache-Control": "public, max-age=3600, s-maxage=3600"
@@ -793,22 +1077,15 @@ function getArticleAiHeaders(article, config) {
793
1077
  "X-Robots-Tag": "noai, noimageai"
794
1078
  };
795
1079
  }
796
- function getAiRobotsTxtRules() {
1080
+ function getAiRobotsTxtRules(config) {
797
1081
  return __async(this, null, function* () {
798
- const articles = yield getAllArticles();
1082
+ const articles = yield getAllArticles(config);
799
1083
  const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
800
1084
  if (blockedArticles.length === 0) return "";
801
- const aiCrawlers = [
802
- "GPTBot",
803
- "ChatGPT-User",
804
- "CCBot",
805
- "ClaudeBot",
806
- "Claude-User",
807
- "PerplexityBot",
808
- "Google-Extended"
809
- ];
810
1085
  const disallowRules = blockedArticles.map((article) => `Disallow: /articles/${article.slug}`).join("\n");
811
- return aiCrawlers.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join("\n\n");
1086
+ return AI_CRAWLERS.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join(
1087
+ "\n\n"
1088
+ );
812
1089
  });
813
1090
  }
814
1091
  function searchArticles(query, config) {
@@ -948,6 +1225,7 @@ function getRelatedContent(article, config, limit = 3) {
948
1225
  }
949
1226
 
950
1227
  // src/articlesConfig.ts
1228
+ var DEFAULT_PAGE_SIZE = 6;
951
1229
  function breadcrumbsAreEnabled(config) {
952
1230
  var _a;
953
1231
  return config.breadcrumbs !== false && ((_a = config.breadcrumbs) == null ? void 0 : _a.show) !== false;
@@ -957,6 +1235,20 @@ function getBreadcrumbsConfig(config) {
957
1235
  if (config.breadcrumbs === false) return {};
958
1236
  return (_a = config.breadcrumbs) != null ? _a : {};
959
1237
  }
1238
+ function getOrganizationId(config) {
1239
+ return `${config.siteUrl.replace(/\/$/, "")}/#organization`;
1240
+ }
1241
+ function getWebSiteId(config) {
1242
+ return `${config.siteUrl.replace(/\/$/, "")}/#website`;
1243
+ }
1244
+ function getPersonId(authorUrl) {
1245
+ return `${authorUrl.replace(/\/$/, "")}#person`;
1246
+ }
1247
+ var DEFAULT_TITLE_TEMPLATE = "{title} | {siteName}";
1248
+ function formatPageTitle(title, config) {
1249
+ var _a;
1250
+ return ((_a = config.titleTemplate) != null ? _a : DEFAULT_TITLE_TEMPLATE).replaceAll("{title}", title).replaceAll("{siteName}", config.siteName);
1251
+ }
960
1252
 
961
1253
  // src/pagination.ts
962
1254
  function getTotalPages(totalCount, pageSize) {
@@ -1004,10 +1296,11 @@ function isPageOutOfRange(page, totalPages) {
1004
1296
  function escapeXml(str) {
1005
1297
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1006
1298
  }
1007
- function generateRssFeed(articles, config) {
1008
- var _a;
1299
+ function generateRssFeed(articles, config, options) {
1300
+ var _a, _b;
1009
1301
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1010
1302
  const showAuthor = config.showAuthor !== false;
1303
+ const fullContent = (options == null ? void 0 : options.fullContent) === true;
1011
1304
  const items = articles.map((article) => {
1012
1305
  const url = `${siteUrl}/articles/${article.slug}`;
1013
1306
  const pubDate = article.date ? new Date(article.date).toUTCString() : "";
@@ -1019,6 +1312,7 @@ function generateRssFeed(articles, config) {
1019
1312
  ` <guid isPermaLink="true">${url}</guid>`,
1020
1313
  pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
1021
1314
  article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : "",
1315
+ fullContent && article.htmlContent ? ` <content:encoded><![CDATA[${article.htmlContent}]]></content:encoded>` : "",
1022
1316
  showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : "",
1023
1317
  article.category ? ` <category><![CDATA[${article.category}]]></category>` : "",
1024
1318
  imageUrl ? ` <media:content url="${imageUrl}" medium="image" width="1200" height="630"/>` : "",
@@ -1027,17 +1321,82 @@ function generateRssFeed(articles, config) {
1027
1321
  }).join("\n");
1028
1322
  const description = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1029
1323
  return `<?xml version="1.0" encoding="UTF-8" ?>
1030
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
1324
+ <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/">
1031
1325
  <channel>
1032
1326
  <title><![CDATA[${config.siteName}]]></title>
1033
1327
  <link>${siteUrl}/articles</link>
1034
1328
  <description><![CDATA[${description}]]></description>
1035
- <language>en</language>
1329
+ <language>${(_b = config.language) != null ? _b : "en"}</language>
1036
1330
  <atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
1037
1331
  ${items}
1038
1332
  </channel>
1039
1333
  </rss>`;
1040
1334
  }
1335
+ function buildLlmsHeader(config) {
1336
+ var _a;
1337
+ const summary = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1338
+ return [`# ${config.siteName}`, "", `> ${summary}`, ""];
1339
+ }
1340
+ function generateLlmsTxt(articles, config) {
1341
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1342
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
1343
+ const byCategory = /* @__PURE__ */ new Map();
1344
+ for (const article of crawlable) {
1345
+ const category = article.category || "Articles";
1346
+ const existing = byCategory.get(category);
1347
+ if (existing) existing.push(article);
1348
+ else byCategory.set(category, [article]);
1349
+ }
1350
+ const sections = [...byCategory.entries()].map(([category, categoryArticles]) => {
1351
+ const lines = categoryArticles.map((article) => {
1352
+ const url = `${siteUrl}/articles/${article.slug}.md`;
1353
+ const summary = article.excerpt ? `: ${article.excerpt}` : "";
1354
+ return `- [${article.title}](${url})${summary}`;
1355
+ });
1356
+ return [`## ${category}`, "", ...lines].join("\n");
1357
+ });
1358
+ return [
1359
+ ...buildLlmsHeader(config),
1360
+ ...sections.length > 0 ? sections : ["## Articles", "", "_No articles available._"],
1361
+ ...buildLlmsListingSection(crawlable, config),
1362
+ ""
1363
+ ].join("\n");
1364
+ }
1365
+ function buildLlmsListingSection(articles, config) {
1366
+ var _a;
1367
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1368
+ const categories = [...new Set(articles.flatMap((a) => {
1369
+ var _a2;
1370
+ return (_a2 = a.categories) != null ? _a2 : [];
1371
+ }))].filter(Boolean);
1372
+ const series = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
1373
+ const authors = Object.values((_a = config.authors) != null ? _a : {});
1374
+ const lines = [
1375
+ ...categories.map(
1376
+ (name) => `- [${name}](${siteUrl}/articles/category/${categoryToSlug(name)}.md)`
1377
+ ),
1378
+ ...config.showAuthorPage === false ? [] : authors.map((a) => `- [${a.name}](${siteUrl}/articles/authors/${a.slug}.md)`),
1379
+ ...series.map((slug) => `- [${slug}](${siteUrl}/articles/series/${slug}.md)`)
1380
+ ];
1381
+ if (lines.length === 0) return [];
1382
+ return ["## Collections", "", ...lines];
1383
+ }
1384
+ function generateLlmsFullTxt(articles, config) {
1385
+ return __async(this, null, function* () {
1386
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
1387
+ const documents = yield Promise.all(
1388
+ crawlable.map((article) => __async(this, null, function* () {
1389
+ const body = yield getArticleMarkdown(article.slug, config);
1390
+ if (body === null) return null;
1391
+ return `${buildMarkdownTwinHeader(article, config, body)}${body.trimStart()}`;
1392
+ }))
1393
+ );
1394
+ return [
1395
+ ...buildLlmsHeader(config),
1396
+ ...documents.filter((doc) => doc !== null)
1397
+ ].join("\n");
1398
+ });
1399
+ }
1041
1400
  function generateArticleStaticParams() {
1042
1401
  return getAvailableArticleSlugs().map((slug) => ({ slug }));
1043
1402
  }
@@ -1103,7 +1462,7 @@ function generateArticleMetadata(slug, config) {
1103
1462
  const markdownUrl = getArticleMarkdownUrl(article, config);
1104
1463
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
1105
1464
  return {
1106
- title: `${search.title} | ${config.siteName}`,
1465
+ title: formatPageTitle(search.title, config),
1107
1466
  description,
1108
1467
  keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
1109
1468
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
@@ -1157,7 +1516,7 @@ function generateArticlesIndexMetadata(config) {
1157
1516
  var _a, _b;
1158
1517
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1159
1518
  const indexUrl = `${siteUrl}/articles`;
1160
- const title = `Articles | ${config.siteName}`;
1519
+ const title = formatPageTitle("Articles", config);
1161
1520
  const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
1162
1521
  return {
1163
1522
  title,
@@ -1205,7 +1564,7 @@ function generateCategoryMetadata(categorySlug, config) {
1205
1564
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
1206
1565
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
1207
1566
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
1208
- const title = `${categoryName} Articles | ${config.siteName}`;
1567
+ const title = formatPageTitle(`${categoryName} Articles`, config);
1209
1568
  return {
1210
1569
  title,
1211
1570
  description,
@@ -1249,7 +1608,7 @@ function generateSeriesMetadata(seriesSlug, config) {
1249
1608
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1250
1609
  const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1251
1610
  const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1252
- const title = `${seriesName} Series | ${config.siteName}`;
1611
+ const title = formatPageTitle(`${seriesName} Series`, config);
1253
1612
  return {
1254
1613
  title,
1255
1614
  description,
@@ -1291,7 +1650,7 @@ function generateAuthorMetadata(authorSlug, config) {
1291
1650
  if (!author || config.showAuthorPage === false) return { title: "Author Not Found" };
1292
1651
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1293
1652
  const authorUrl = (_a = author.url) != null ? _a : `${siteUrl}/articles/authors/${author.slug}`;
1294
- const title = `${author.name} Articles | ${config.siteName}`;
1653
+ const title = formatPageTitle(`${author.name} Articles`, config);
1295
1654
  return {
1296
1655
  title,
1297
1656
  description: author.bio,
@@ -1473,17 +1832,41 @@ function resolveAuthorAvatar(author, config) {
1473
1832
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1474
1833
  return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
1475
1834
  }
1835
+ function newestArticleDate(articles) {
1836
+ var _a;
1837
+ let newest;
1838
+ for (const article of articles) {
1839
+ const stamp = (_a = article.lastmod) != null ? _a : article.date;
1840
+ if (!stamp) continue;
1841
+ const parsed = new Date(stamp);
1842
+ if (Number.isNaN(parsed.getTime())) continue;
1843
+ if (!newest || parsed > newest) newest = parsed;
1844
+ }
1845
+ return newest;
1846
+ }
1847
+ function paginationEntries(basePath, itemCount, pageSize, lastModified, priority) {
1848
+ const totalPages = getTotalPages(itemCount, pageSize);
1849
+ const entries = [];
1850
+ for (let page = 2; page <= totalPages; page++) {
1851
+ entries.push({
1852
+ url: buildPageUrl(basePath, page),
1853
+ lastModified,
1854
+ changeFrequency: "weekly",
1855
+ priority
1856
+ });
1857
+ }
1858
+ return entries;
1859
+ }
1476
1860
  function getArticleSitemapEntries(baseUrlOrConfig) {
1477
1861
  return __async(this, null, function* () {
1478
- const baseUrl = (typeof baseUrlOrConfig === "string" ? baseUrlOrConfig : baseUrlOrConfig.siteUrl).replace(/\/$/, "");
1862
+ var _a, _b;
1863
+ const config = typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig;
1864
+ const baseUrl = ((_a = config == null ? void 0 : config.siteUrl) != null ? _a : baseUrlOrConfig).replace(/\/$/, "");
1479
1865
  try {
1480
- const [articles, categories] = yield Promise.all([
1481
- getAllArticles(typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig),
1482
- getAllCategories()
1483
- ]);
1866
+ const [articles, categories] = yield Promise.all([getAllArticles(config), getAllCategories()]);
1484
1867
  const articleEntries = articles.map((article) => {
1485
- var _a;
1486
- const dateStr = (_a = article.lastmod) != null ? _a : article.date;
1868
+ var _a2;
1869
+ const dateStr = (_a2 = article.lastmod) != null ? _a2 : article.date;
1487
1870
  const lastModified = dateStr ? new Date(dateStr) : void 0;
1488
1871
  return {
1489
1872
  url: `${baseUrl}/articles/${article.slug}`,
@@ -1494,16 +1877,86 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1494
1877
  });
1495
1878
  const categoryEntries = categories.map((cat) => ({
1496
1879
  url: `${baseUrl}/articles/category/${cat.slug}`,
1497
- lastModified: /* @__PURE__ */ new Date(),
1880
+ lastModified: newestArticleDate(
1881
+ articles.filter(
1882
+ (article) => {
1883
+ var _a2;
1884
+ return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
1885
+ }
1886
+ )
1887
+ ),
1498
1888
  changeFrequency: "weekly",
1499
1889
  priority: 0.7
1500
1890
  }));
1501
- const authorEntries = typeof baseUrlOrConfig === "string" || baseUrlOrConfig.showAuthorPage === false ? [] : getAllAuthors(baseUrlOrConfig).map((author) => ({
1891
+ const authors = config && config.showAuthorPage !== false ? getAllAuthors(config) : [];
1892
+ const authorEntries = authors.map((author) => ({
1502
1893
  url: `${baseUrl}/articles/authors/${author.slug}`,
1894
+ lastModified: newestArticleDate(
1895
+ articles.filter(
1896
+ (article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
1897
+ )
1898
+ ),
1503
1899
  changeFrequency: "monthly",
1504
1900
  priority: 0.6
1505
1901
  }));
1506
- return [...articleEntries, ...categoryEntries, ...authorEntries];
1902
+ const seriesSlugs = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
1903
+ const seriesEntries = seriesSlugs.map((seriesSlug) => ({
1904
+ url: `${baseUrl}/articles/series/${seriesSlug}`,
1905
+ lastModified: newestArticleDate(articles.filter((a) => a.seriesSlug === seriesSlug)),
1906
+ changeFrequency: "weekly",
1907
+ priority: 0.6
1908
+ }));
1909
+ const pageEntries = [];
1910
+ if ((config == null ? void 0 : config.listingPagination) === "pages") {
1911
+ const pageSize = (_b = config.pageSize) != null ? _b : DEFAULT_PAGE_SIZE;
1912
+ pageEntries.push(
1913
+ ...paginationEntries(
1914
+ `${baseUrl}/articles`,
1915
+ articles.length,
1916
+ pageSize,
1917
+ newestArticleDate(articles),
1918
+ 0.5
1919
+ )
1920
+ );
1921
+ for (const cat of categories) {
1922
+ const inCategory = articles.filter(
1923
+ (article) => {
1924
+ var _a2;
1925
+ return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
1926
+ }
1927
+ );
1928
+ pageEntries.push(
1929
+ ...paginationEntries(
1930
+ `${baseUrl}/articles/category/${cat.slug}`,
1931
+ inCategory.length,
1932
+ pageSize,
1933
+ newestArticleDate(inCategory),
1934
+ 0.4
1935
+ )
1936
+ );
1937
+ }
1938
+ for (const author of authors) {
1939
+ const byAuthor = articles.filter(
1940
+ (article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
1941
+ );
1942
+ pageEntries.push(
1943
+ ...paginationEntries(
1944
+ `${baseUrl}/articles/authors/${author.slug}`,
1945
+ byAuthor.length,
1946
+ pageSize,
1947
+ newestArticleDate(byAuthor),
1948
+ 0.4
1949
+ )
1950
+ );
1951
+ }
1952
+ }
1953
+ return [
1954
+ ...articleEntries,
1955
+ ...categoryEntries,
1956
+ ...authorEntries,
1957
+ ...seriesEntries,
1958
+ ...pageEntries
1959
+ ];
1507
1960
  } catch (e) {
1508
1961
  return [];
1509
1962
  }
@@ -1512,6 +1965,7 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1512
1965
 
1513
1966
  // src/renderMdx.tsx
1514
1967
  import React from "react";
1968
+ import Link from "next/link";
1515
1969
  import * as devRuntime from "react/jsx-dev-runtime";
1516
1970
  import * as runtime from "react/jsx-runtime";
1517
1971
  import { evaluate } from "@mdx-js/mdx";
@@ -1527,6 +1981,19 @@ function makeImgComponent(basePath) {
1527
1981
  return React.createElement("img", __spreadValues({ src: resolvedSrc, alt }, props));
1528
1982
  };
1529
1983
  }
1984
+ function isInternalNavigableHref(href, siteUrl) {
1985
+ if (!href || href.startsWith("#") || isNonBrowserNavigationLink(href)) return false;
1986
+ return !isExternalHttpLink(href, siteUrl);
1987
+ }
1988
+ function makeLinkComponent(siteUrl) {
1989
+ return function MdxLink(_a) {
1990
+ var _b = _a, { href, children } = _b, props = __objRest(_b, ["href", "children"]);
1991
+ if (typeof href === "string" && isInternalNavigableHref(href, siteUrl)) {
1992
+ return React.createElement(Link, __spreadValues({ href }, props), children);
1993
+ }
1994
+ return /* @__PURE__ */ jsx("a", __spreadProps(__spreadValues({ href }, props), { children }));
1995
+ };
1996
+ }
1530
1997
  function renderMdxSource(source, basePath, config) {
1531
1998
  return __async(this, null, function* () {
1532
1999
  const isDevelopment = process.env.NODE_ENV === "development";
@@ -1541,8 +2008,10 @@ function renderMdxSource(source, basePath, config) {
1541
2008
  ]
1542
2009
  }));
1543
2010
  const Content = mdxModule.default;
1544
- const internalComponents = basePath ? { img: makeImgComponent(basePath) } : void 0;
1545
- const components = internalComponents || (config == null ? void 0 : config.mdxComponents) ? __spreadValues(__spreadValues({}, internalComponents), config == null ? void 0 : config.mdxComponents) : void 0;
2011
+ const internalComponents = __spreadValues({
2012
+ a: makeLinkComponent(config == null ? void 0 : config.siteUrl)
2013
+ }, basePath ? { img: makeImgComponent(basePath) } : {});
2014
+ const components = __spreadValues(__spreadValues({}, internalComponents), config == null ? void 0 : config.mdxComponents);
1546
2015
  return /* @__PURE__ */ jsx(Content, { components });
1547
2016
  });
1548
2017
  }
@@ -1661,8 +2130,32 @@ function ArticleTOC({ toc, className }) {
1661
2130
  );
1662
2131
  }
1663
2132
 
2133
+ // src/ArticleAnswer.tsx
2134
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2135
+ function ArticleAnswer({
2136
+ article,
2137
+ label = "The short answer",
2138
+ className
2139
+ }) {
2140
+ var _a;
2141
+ if (!((_a = article.answer) == null ? void 0 : _a.trim())) return null;
2142
+ return /* @__PURE__ */ jsxs3(
2143
+ "aside",
2144
+ {
2145
+ className: `mb-8 rounded-lg border-l-4 border-primary bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
2146
+ style: { borderLeftWidth: "4px" },
2147
+ children: [
2148
+ /* @__PURE__ */ jsx4("p", { className: "mb-2 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
2149
+ /* @__PURE__ */ jsx4("p", { className: "text-base leading-relaxed", children: article.answer })
2150
+ ]
2151
+ }
2152
+ );
2153
+ }
2154
+
1664
2155
  // src/validateArticles.ts
1665
2156
  var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
2157
+ var THIN_CONTENT_WORDS = 300;
2158
+ var STALE_CONTENT_MONTHS = 18;
1666
2159
  var SEARCH_TITLE_MAX = 60;
1667
2160
  var SEARCH_DESCRIPTION_MAX = 160;
1668
2161
  var SOCIAL_TITLE_MAX = 95;
@@ -1806,12 +2299,15 @@ function checkRequiredFrontmatter(articles) {
1806
2299
  }
1807
2300
  return issues;
1808
2301
  }
1809
- function checkDiscoveryFieldLengths(articles) {
2302
+ function checkDiscoveryFieldLengths(articles, config) {
2303
+ var _a, _b;
1810
2304
  const issues = [];
1811
2305
  for (const article of articles) {
2306
+ const effectiveTitle = formatPageTitle((_a = article.searchTitle) != null ? _a : article.title, config);
2307
+ const effectiveDescription = (_b = article.searchDescription) != null ? _b : article.excerpt;
1812
2308
  const checks = [
1813
- [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1814
- [article.searchDescription, "search-description-too-long", SEARCH_DESCRIPTION_MAX],
2309
+ [effectiveTitle, "effective-title-too-long", SEARCH_TITLE_MAX],
2310
+ [effectiveDescription, "effective-description-too-long", SEARCH_DESCRIPTION_MAX],
1815
2311
  [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1816
2312
  [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1817
2313
  ];
@@ -1828,6 +2324,101 @@ function checkDiscoveryFieldLengths(articles) {
1828
2324
  }
1829
2325
  return issues;
1830
2326
  }
2327
+ function checkAnswerability(articles) {
2328
+ var _a, _b, _c;
2329
+ const issues = [];
2330
+ for (const article of articles) {
2331
+ const hasQuestionHeading = ((_a = article.toc) != null ? _a : []).some(
2332
+ (item) => item.depth === 2 && item.text.trim().endsWith("?")
2333
+ );
2334
+ if (!article.answer && !((_b = article.faq) == null ? void 0 : _b.length) && !hasQuestionHeading) {
2335
+ issues.push({
2336
+ severity: "warning",
2337
+ code: "no-answer",
2338
+ message: "Article has no `answer`, no `faq`, and no question-shaped heading - nothing for an answer engine to lift.",
2339
+ articleSlug: article.slug
2340
+ });
2341
+ }
2342
+ if (article.wordCount !== void 0 && article.wordCount < THIN_CONTENT_WORDS) {
2343
+ issues.push({
2344
+ severity: "warning",
2345
+ code: "thin-content",
2346
+ message: `Article is ${article.wordCount} words (under ${THIN_CONTENT_WORDS}).`,
2347
+ articleSlug: article.slug
2348
+ });
2349
+ }
2350
+ if (!((_c = article.about) == null ? void 0 : _c.length)) {
2351
+ issues.push({
2352
+ severity: "warning",
2353
+ code: "missing-about",
2354
+ message: "Article has no `about` entity references.",
2355
+ articleSlug: article.slug
2356
+ });
2357
+ }
2358
+ }
2359
+ return issues;
2360
+ }
2361
+ function checkEntityReferences(articles, config) {
2362
+ var _a;
2363
+ const registry = config.entities;
2364
+ if (!registry) return [];
2365
+ const known = new Set(Object.values(registry).map((entity) => entity.name));
2366
+ const issues = [];
2367
+ for (const article of articles) {
2368
+ for (const entity of (_a = article.about) != null ? _a : []) {
2369
+ if (!known.has(entity.name) && !entity.sameAs) {
2370
+ issues.push({
2371
+ severity: "warning",
2372
+ code: "unknown-entity",
2373
+ message: `about entry "${entity.name}" is not in config.entities and has no sameAs.`,
2374
+ articleSlug: article.slug
2375
+ });
2376
+ }
2377
+ }
2378
+ }
2379
+ return issues;
2380
+ }
2381
+ function checkStaleContent(articles, now) {
2382
+ var _a;
2383
+ const cutoff = new Date(now);
2384
+ cutoff.setMonth(cutoff.getMonth() - STALE_CONTENT_MONTHS);
2385
+ const issues = [];
2386
+ for (const article of articles) {
2387
+ const stamp = (_a = article.lastmod) != null ? _a : article.date;
2388
+ if (!stamp) continue;
2389
+ const parsed = new Date(stamp);
2390
+ if (Number.isNaN(parsed.getTime())) continue;
2391
+ if (parsed < cutoff) {
2392
+ issues.push({
2393
+ severity: "warning",
2394
+ code: "stale-content",
2395
+ message: `Last updated ${stamp}, over ${STALE_CONTENT_MONTHS} months ago.`,
2396
+ articleSlug: article.slug
2397
+ });
2398
+ }
2399
+ }
2400
+ return issues;
2401
+ }
2402
+ function checkOrphanArticles(articles) {
2403
+ const bodies = articles.filter((article) => typeof article.content === "string");
2404
+ if (bodies.length === 0) return [];
2405
+ const issues = [];
2406
+ for (const article of articles) {
2407
+ const needle = `/articles/${article.slug}`;
2408
+ const linked = bodies.some(
2409
+ (other) => other.slug !== article.slug && other.content.includes(needle)
2410
+ );
2411
+ if (!linked) {
2412
+ issues.push({
2413
+ severity: "warning",
2414
+ code: "orphan-article",
2415
+ message: "No other article links to this one.",
2416
+ articleSlug: article.slug
2417
+ });
2418
+ }
2419
+ }
2420
+ return issues;
2421
+ }
1831
2422
  function checkCategorySlugs(articles) {
1832
2423
  var _a;
1833
2424
  const issues = [];
@@ -1851,7 +2442,8 @@ function checkCategorySlugs(articles) {
1851
2442
  }
1852
2443
  return issues;
1853
2444
  }
1854
- function validateArticles(articles, config) {
2445
+ function validateArticles(articles, config, options) {
2446
+ var _a;
1855
2447
  const errors = [
1856
2448
  ...checkDuplicateCanonicalUrls(articles),
1857
2449
  ...checkAuthorReferences(articles, config),
@@ -1861,15 +2453,19 @@ function validateArticles(articles, config) {
1861
2453
  ];
1862
2454
  const warnings = [
1863
2455
  ...checkRequiredFrontmatter(articles),
1864
- ...checkDiscoveryFieldLengths(articles),
2456
+ ...checkDiscoveryFieldLengths(articles, config),
2457
+ ...checkAnswerability(articles),
2458
+ ...checkEntityReferences(articles, config),
2459
+ ...checkStaleContent(articles, (_a = options == null ? void 0 : options.now) != null ? _a : /* @__PURE__ */ new Date()),
2460
+ ...checkOrphanArticles(articles),
1865
2461
  ...checkCategorySlugs(articles)
1866
2462
  ];
1867
2463
  return { ok: errors.length === 0, errors, warnings };
1868
2464
  }
1869
- function validateAllArticles(config) {
2465
+ function validateAllArticles(config, options) {
1870
2466
  return __async(this, null, function* () {
1871
2467
  const articles = yield getAllArticles(config);
1872
- return validateArticles(articles, config);
2468
+ return validateArticles(articles, config, options);
1873
2469
  });
1874
2470
  }
1875
2471
 
@@ -1882,16 +2478,21 @@ function emitArticleEvent(handler, event) {
1882
2478
  }
1883
2479
  }
1884
2480
  export {
2481
+ AI_CRAWLERS,
2482
+ ArticleAnswer,
1885
2483
  ArticleContent,
1886
2484
  ArticleTOC,
1887
2485
  buildArticleBreadcrumbs,
1888
2486
  buildAuthorBreadcrumbs,
1889
2487
  buildCategoryBreadcrumbs,
2488
+ buildMarkdownTwinHeader,
1890
2489
  buildPageUrl,
1891
2490
  buildPaginationLinks,
1892
2491
  categoryToSlug,
2492
+ deriveFaqFromHeadings,
1893
2493
  emitArticleEvent,
1894
2494
  extractToc,
2495
+ formatPageTitle,
1895
2496
  generateArticleMetadata,
1896
2497
  generateArticleStaticParams,
1897
2498
  generateArticlesIndexMetadata,
@@ -1903,6 +2504,8 @@ export {
1903
2504
  generateCategoryPageMetadata,
1904
2505
  generateCategoryStaticParams,
1905
2506
  generateListingPageStaticParams,
2507
+ generateLlmsFullTxt,
2508
+ generateLlmsTxt,
1906
2509
  generateRssFeed,
1907
2510
  generateSeriesMetadata,
1908
2511
  generateSeriesStaticParams,
@@ -1923,16 +2526,24 @@ export {
1923
2526
  getArticlesByCategory,
1924
2527
  getArticlesBySeries,
1925
2528
  getAuthorBySlug,
2529
+ getAuthorMarkdown,
1926
2530
  getAvailableArticleSlugs,
1927
2531
  getBreadcrumbsConfig,
2532
+ getCategoryMarkdown,
1928
2533
  getContentSlotBoundaries,
2534
+ getMarkdownTwinResponse,
2535
+ getOrganizationId,
1929
2536
  getPath,
1930
2537
  getPathArticles,
2538
+ getPersonId,
1931
2539
  getRelatedArticlesByCategory,
1932
2540
  getRelatedContent,
2541
+ getSeriesMarkdown,
1933
2542
  getTotalPages,
2543
+ getWebSiteId,
1934
2544
  isPageOutOfRange,
1935
2545
  markdownToHtml,
2546
+ matchAiCrawler,
1936
2547
  paginateArticles,
1937
2548
  parsePageParam,
1938
2549
  resolveAuthorAvatar,