@fullstackdatasolutions/articles 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +313 -1
  3. package/dist/index.cjs +308 -79
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +267 -16
  6. package/dist/index.d.ts +267 -16
  7. package/dist/index.js +300 -79
  8. package/dist/index.js.map +1 -1
  9. package/dist/nextjs.cjs +325 -31
  10. package/dist/nextjs.cjs.map +1 -1
  11. package/dist/nextjs.d.cts +179 -2
  12. package/dist/nextjs.d.ts +179 -2
  13. package/dist/nextjs.js +325 -31
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +660 -50
  16. package/dist/server.cjs.map +1 -1
  17. package/dist/server.d.cts +333 -12
  18. package/dist/server.d.ts +333 -12
  19. package/dist/server.js +645 -50
  20. package/dist/server.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/ArticleAnswer.tsx +35 -0
  23. package/src/ArticleSchemas.tsx +263 -23
  24. package/src/AuthorArticlesPage.tsx +38 -8
  25. package/src/__tests__/ArticleAnswer.test.tsx +25 -0
  26. package/src/__tests__/ArticleSchemas.test.tsx +516 -0
  27. package/src/__tests__/AuthorArticlesPage.test.tsx +76 -0
  28. package/src/__tests__/authorUtils.test.ts +50 -0
  29. package/src/__tests__/markdown.test.ts +77 -1
  30. package/src/__tests__/nextjs.test.ts +31 -15
  31. package/src/__tests__/seoUtils.test.ts +279 -0
  32. package/src/__tests__/server-articles.test.ts +434 -1
  33. package/src/__tests__/validateArticles.test.ts +167 -6
  34. package/src/articleTypes.ts +57 -0
  35. package/src/articlesConfig.ts +176 -1
  36. package/src/authorUtils.ts +19 -1
  37. package/src/errorReporting.ts +1 -0
  38. package/src/index.ts +17 -1
  39. package/src/markdown.ts +100 -1
  40. package/src/nextjs.ts +7 -4
  41. package/src/seoUtils.ts +247 -26
  42. package/src/server-articles.ts +385 -25
  43. package/src/server.ts +35 -4
  44. package/src/validateArticles.ts +157 -12
package/dist/server.js CHANGED
@@ -431,6 +431,64 @@ function extractToc(markdown) {
431
431
  return headings;
432
432
  });
433
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
+ }
434
492
 
435
493
  // src/server-articles.ts
436
494
  var articlesDirectory = path.join(
@@ -563,6 +621,58 @@ function parseHowToSteps(raw) {
563
621
  function parseOptionalString(raw) {
564
622
  return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
565
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
+ }
566
676
  function parseSeriesOrder(raw) {
567
677
  return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
568
678
  }
@@ -639,6 +749,7 @@ function getAllAuthors(config) {
639
749
  }
640
750
  function getArticleSummary(slug, config) {
641
751
  return __async(this, null, function* () {
752
+ var _a;
642
753
  try {
643
754
  const found = findArticleFile(slug);
644
755
  if (!found) return null;
@@ -655,7 +766,7 @@ function getArticleSummary(slug, config) {
655
766
  title: data.title || slug.replaceAll("-", " "),
656
767
  excerpt: data.excerpt || "",
657
768
  date: parseDateField(data.date),
658
- lastmod: parseDateField(data.lastmod),
769
+ lastmod: resolveLastmod(data.lastmod, data.date, found.filePath, config),
659
770
  author,
660
771
  authors,
661
772
  authorSlug: primaryAuthorProfile == null ? void 0 : primaryAuthorProfile.slug,
@@ -668,14 +779,17 @@ function getArticleSummary(slug, config) {
668
779
  tags: data.tags || [],
669
780
  contentType: found.contentType,
670
781
  draft: data.draft === true,
671
- faq: parseFaqItems(data.faq),
782
+ faq: (_a = parseFaqItems(data.faq)) != null ? _a : deriveFaq(markdownContent, config),
672
783
  howTo: parseHowToSteps(data.howTo),
784
+ answer: parseOptionalString(data.answer),
785
+ about: parseEntityReferences(data.about, config),
786
+ citation: parseCitations(data.citation),
673
787
  canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
674
788
  articleType: typeof data.articleType === "string" ? data.articleType : void 0,
675
789
  series: typeof data.series === "string" ? data.series : void 0,
676
790
  seriesSlug: parseOptionalString(data.seriesSlug),
677
791
  seriesOrder: parseSeriesOrder(data.seriesOrder),
678
- aiCrawl: data.aiCrawl === true,
792
+ aiCrawl: resolveAiCrawl(data.aiCrawl, config),
679
793
  searchTitle: parseOptionalString(data.searchTitle),
680
794
  searchDescription: parseOptionalString(data.searchDescription),
681
795
  socialTitle: parseOptionalString(data.socialTitle),
@@ -744,10 +858,10 @@ function getAdjacentArticles(currentSlug) {
744
858
  return { previous, next };
745
859
  });
746
860
  }
747
- function getArticleMarkdown(slug) {
861
+ function getArticleMarkdown(slug, config) {
748
862
  return __async(this, null, function* () {
749
863
  try {
750
- const summary = yield getArticleSummary(slug);
864
+ const summary = yield getArticleSummary(slug, config);
751
865
  if (!(summary == null ? void 0 : summary.aiCrawl)) return null;
752
866
  const found = findArticleFile(slug);
753
867
  if (!found) return null;
@@ -765,16 +879,186 @@ function getArticleMarkdown(slug) {
765
879
  }
766
880
  });
767
881
  }
768
- 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) {
1038
+ return __async(this, null, function* () {
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) {
769
1049
  return __async(this, null, function* () {
770
- const markdown = yield getArticleMarkdown(slug);
1050
+ const markdown = yield getArticleMarkdown(slug, config);
771
1051
  if (markdown === null) return new Response("Not Found", { status: 404 });
772
- const article = yield getArticleMetadata(slug);
773
- return new Response(markdown, {
774
- headers: __spreadValues({
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
+ const canonicalUrl = `${config.siteUrl.replace(/\/$/, "")}/articles/${slug}`;
1056
+ return new Response(body, {
1057
+ headers: {
775
1058
  "Content-Type": "text/markdown; charset=utf-8",
776
- "Cache-Control": "public, max-age=3600, s-maxage=3600"
777
- }, article ? getArticleAiHeaders(article, config) : {})
1059
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1060
+ Link: `<${canonicalUrl}>; rel="canonical"`
1061
+ }
778
1062
  });
779
1063
  });
780
1064
  }
@@ -795,22 +1079,15 @@ function getArticleAiHeaders(article, config) {
795
1079
  "X-Robots-Tag": "noai, noimageai"
796
1080
  };
797
1081
  }
798
- function getAiRobotsTxtRules() {
1082
+ function getAiRobotsTxtRules(config) {
799
1083
  return __async(this, null, function* () {
800
- const articles = yield getAllArticles();
1084
+ const articles = yield getAllArticles(config);
801
1085
  const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
802
1086
  if (blockedArticles.length === 0) return "";
803
- const aiCrawlers = [
804
- "GPTBot",
805
- "ChatGPT-User",
806
- "CCBot",
807
- "ClaudeBot",
808
- "Claude-User",
809
- "PerplexityBot",
810
- "Google-Extended"
811
- ];
812
1087
  const disallowRules = blockedArticles.map((article) => `Disallow: /articles/${article.slug}`).join("\n");
813
- return aiCrawlers.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join("\n\n");
1088
+ return AI_CRAWLERS.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join(
1089
+ "\n\n"
1090
+ );
814
1091
  });
815
1092
  }
816
1093
  function searchArticles(query, config) {
@@ -950,6 +1227,7 @@ function getRelatedContent(article, config, limit = 3) {
950
1227
  }
951
1228
 
952
1229
  // src/articlesConfig.ts
1230
+ var DEFAULT_PAGE_SIZE = 6;
953
1231
  function breadcrumbsAreEnabled(config) {
954
1232
  var _a;
955
1233
  return config.breadcrumbs !== false && ((_a = config.breadcrumbs) == null ? void 0 : _a.show) !== false;
@@ -959,6 +1237,20 @@ function getBreadcrumbsConfig(config) {
959
1237
  if (config.breadcrumbs === false) return {};
960
1238
  return (_a = config.breadcrumbs) != null ? _a : {};
961
1239
  }
1240
+ function getOrganizationId(config) {
1241
+ return `${config.siteUrl.replace(/\/$/, "")}/#organization`;
1242
+ }
1243
+ function getWebSiteId(config) {
1244
+ return `${config.siteUrl.replace(/\/$/, "")}/#website`;
1245
+ }
1246
+ function getPersonId(authorUrl) {
1247
+ return `${authorUrl.replace(/\/$/, "")}#person`;
1248
+ }
1249
+ var DEFAULT_TITLE_TEMPLATE = "{title} | {siteName}";
1250
+ function formatPageTitle(title, config) {
1251
+ var _a;
1252
+ return ((_a = config.titleTemplate) != null ? _a : DEFAULT_TITLE_TEMPLATE).replaceAll("{title}", title).replaceAll("{siteName}", config.siteName);
1253
+ }
962
1254
 
963
1255
  // src/pagination.ts
964
1256
  function getTotalPages(totalCount, pageSize) {
@@ -1006,10 +1298,11 @@ function isPageOutOfRange(page, totalPages) {
1006
1298
  function escapeXml(str) {
1007
1299
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1008
1300
  }
1009
- function generateRssFeed(articles, config) {
1010
- var _a;
1301
+ function generateRssFeed(articles, config, options) {
1302
+ var _a, _b;
1011
1303
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1012
1304
  const showAuthor = config.showAuthor !== false;
1305
+ const fullContent = (options == null ? void 0 : options.fullContent) === true;
1013
1306
  const items = articles.map((article) => {
1014
1307
  const url = `${siteUrl}/articles/${article.slug}`;
1015
1308
  const pubDate = article.date ? new Date(article.date).toUTCString() : "";
@@ -1021,6 +1314,7 @@ function generateRssFeed(articles, config) {
1021
1314
  ` <guid isPermaLink="true">${url}</guid>`,
1022
1315
  pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
1023
1316
  article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : "",
1317
+ fullContent && article.htmlContent ? ` <content:encoded><![CDATA[${article.htmlContent}]]></content:encoded>` : "",
1024
1318
  showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : "",
1025
1319
  article.category ? ` <category><![CDATA[${article.category}]]></category>` : "",
1026
1320
  imageUrl ? ` <media:content url="${imageUrl}" medium="image" width="1200" height="630"/>` : "",
@@ -1029,17 +1323,82 @@ function generateRssFeed(articles, config) {
1029
1323
  }).join("\n");
1030
1324
  const description = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1031
1325
  return `<?xml version="1.0" encoding="UTF-8" ?>
1032
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
1326
+ <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/">
1033
1327
  <channel>
1034
1328
  <title><![CDATA[${config.siteName}]]></title>
1035
1329
  <link>${siteUrl}/articles</link>
1036
1330
  <description><![CDATA[${description}]]></description>
1037
- <language>en</language>
1331
+ <language>${(_b = config.language) != null ? _b : "en"}</language>
1038
1332
  <atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
1039
1333
  ${items}
1040
1334
  </channel>
1041
1335
  </rss>`;
1042
1336
  }
1337
+ function buildLlmsHeader(config) {
1338
+ var _a;
1339
+ const summary = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1340
+ return [`# ${config.siteName}`, "", `> ${summary}`, ""];
1341
+ }
1342
+ function generateLlmsTxt(articles, config) {
1343
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1344
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
1345
+ const byCategory = /* @__PURE__ */ new Map();
1346
+ for (const article of crawlable) {
1347
+ const category = article.category || "Articles";
1348
+ const existing = byCategory.get(category);
1349
+ if (existing) existing.push(article);
1350
+ else byCategory.set(category, [article]);
1351
+ }
1352
+ const sections = [...byCategory.entries()].map(([category, categoryArticles]) => {
1353
+ const lines = categoryArticles.map((article) => {
1354
+ const url = `${siteUrl}/articles/${article.slug}.md`;
1355
+ const summary = article.excerpt ? `: ${article.excerpt}` : "";
1356
+ return `- [${article.title}](${url})${summary}`;
1357
+ });
1358
+ return [`## ${category}`, "", ...lines].join("\n");
1359
+ });
1360
+ return [
1361
+ ...buildLlmsHeader(config),
1362
+ ...sections.length > 0 ? sections : ["## Articles", "", "_No articles available._"],
1363
+ ...buildLlmsListingSection(crawlable, config),
1364
+ ""
1365
+ ].join("\n");
1366
+ }
1367
+ function buildLlmsListingSection(articles, config) {
1368
+ var _a;
1369
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1370
+ const categories = [...new Set(articles.flatMap((a) => {
1371
+ var _a2;
1372
+ return (_a2 = a.categories) != null ? _a2 : [];
1373
+ }))].filter(Boolean);
1374
+ const series = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
1375
+ const authors = Object.values((_a = config.authors) != null ? _a : {});
1376
+ const lines = [
1377
+ ...categories.map(
1378
+ (name) => `- [${name}](${siteUrl}/articles/category/${categoryToSlug(name)}.md)`
1379
+ ),
1380
+ ...config.showAuthorPage === false ? [] : authors.map((a) => `- [${a.name}](${siteUrl}/articles/authors/${a.slug}.md)`),
1381
+ ...series.map((slug) => `- [${slug}](${siteUrl}/articles/series/${slug}.md)`)
1382
+ ];
1383
+ if (lines.length === 0) return [];
1384
+ return ["## Collections", "", ...lines];
1385
+ }
1386
+ function generateLlmsFullTxt(articles, config) {
1387
+ return __async(this, null, function* () {
1388
+ const crawlable = articles.filter((article) => article.aiCrawl === true);
1389
+ const documents = yield Promise.all(
1390
+ crawlable.map((article) => __async(this, null, function* () {
1391
+ const body = yield getArticleMarkdown(article.slug, config);
1392
+ if (body === null) return null;
1393
+ return `${buildMarkdownTwinHeader(article, config, body)}${body.trimStart()}`;
1394
+ }))
1395
+ );
1396
+ return [
1397
+ ...buildLlmsHeader(config),
1398
+ ...documents.filter((doc) => doc !== null)
1399
+ ].join("\n");
1400
+ });
1401
+ }
1043
1402
  function generateArticleStaticParams() {
1044
1403
  return getAvailableArticleSlugs().map((slug) => ({ slug }));
1045
1404
  }
@@ -1105,7 +1464,7 @@ function generateArticleMetadata(slug, config) {
1105
1464
  const markdownUrl = getArticleMarkdownUrl(article, config);
1106
1465
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
1107
1466
  return {
1108
- title: `${search.title} | ${config.siteName}`,
1467
+ title: formatPageTitle(search.title, config),
1109
1468
  description,
1110
1469
  keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
1111
1470
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
@@ -1159,7 +1518,7 @@ function generateArticlesIndexMetadata(config) {
1159
1518
  var _a, _b;
1160
1519
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1161
1520
  const indexUrl = `${siteUrl}/articles`;
1162
- const title = `Articles | ${config.siteName}`;
1521
+ const title = formatPageTitle("Articles", config);
1163
1522
  const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
1164
1523
  return {
1165
1524
  title,
@@ -1207,7 +1566,7 @@ function generateCategoryMetadata(categorySlug, config) {
1207
1566
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
1208
1567
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
1209
1568
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
1210
- const title = `${categoryName} Articles | ${config.siteName}`;
1569
+ const title = formatPageTitle(`${categoryName} Articles`, config);
1211
1570
  return {
1212
1571
  title,
1213
1572
  description,
@@ -1251,7 +1610,7 @@ function generateSeriesMetadata(seriesSlug, config) {
1251
1610
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1252
1611
  const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1253
1612
  const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1254
- const title = `${seriesName} Series | ${config.siteName}`;
1613
+ const title = formatPageTitle(`${seriesName} Series`, config);
1255
1614
  return {
1256
1615
  title,
1257
1616
  description,
@@ -1293,7 +1652,7 @@ function generateAuthorMetadata(authorSlug, config) {
1293
1652
  if (!author || config.showAuthorPage === false) return { title: "Author Not Found" };
1294
1653
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1295
1654
  const authorUrl = (_a = author.url) != null ? _a : `${siteUrl}/articles/authors/${author.slug}`;
1296
- const title = `${author.name} Articles | ${config.siteName}`;
1655
+ const title = formatPageTitle(`${author.name} Articles`, config);
1297
1656
  return {
1298
1657
  title,
1299
1658
  description: author.bio,
@@ -1475,17 +1834,41 @@ function resolveAuthorAvatar(author, config) {
1475
1834
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1476
1835
  return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
1477
1836
  }
1837
+ function newestArticleDate(articles) {
1838
+ var _a;
1839
+ let newest;
1840
+ for (const article of articles) {
1841
+ const stamp = (_a = article.lastmod) != null ? _a : article.date;
1842
+ if (!stamp) continue;
1843
+ const parsed = new Date(stamp);
1844
+ if (Number.isNaN(parsed.getTime())) continue;
1845
+ if (!newest || parsed > newest) newest = parsed;
1846
+ }
1847
+ return newest;
1848
+ }
1849
+ function paginationEntries(basePath, itemCount, pageSize, lastModified, priority) {
1850
+ const totalPages = getTotalPages(itemCount, pageSize);
1851
+ const entries = [];
1852
+ for (let page = 2; page <= totalPages; page++) {
1853
+ entries.push({
1854
+ url: buildPageUrl(basePath, page),
1855
+ lastModified,
1856
+ changeFrequency: "weekly",
1857
+ priority
1858
+ });
1859
+ }
1860
+ return entries;
1861
+ }
1478
1862
  function getArticleSitemapEntries(baseUrlOrConfig) {
1479
1863
  return __async(this, null, function* () {
1480
- const baseUrl = (typeof baseUrlOrConfig === "string" ? baseUrlOrConfig : baseUrlOrConfig.siteUrl).replace(/\/$/, "");
1864
+ var _a, _b;
1865
+ const config = typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig;
1866
+ const baseUrl = ((_a = config == null ? void 0 : config.siteUrl) != null ? _a : baseUrlOrConfig).replace(/\/$/, "");
1481
1867
  try {
1482
- const [articles, categories] = yield Promise.all([
1483
- getAllArticles(typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig),
1484
- getAllCategories()
1485
- ]);
1868
+ const [articles, categories] = yield Promise.all([getAllArticles(config), getAllCategories()]);
1486
1869
  const articleEntries = articles.map((article) => {
1487
- var _a;
1488
- const dateStr = (_a = article.lastmod) != null ? _a : article.date;
1870
+ var _a2;
1871
+ const dateStr = (_a2 = article.lastmod) != null ? _a2 : article.date;
1489
1872
  const lastModified = dateStr ? new Date(dateStr) : void 0;
1490
1873
  return {
1491
1874
  url: `${baseUrl}/articles/${article.slug}`,
@@ -1496,16 +1879,86 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1496
1879
  });
1497
1880
  const categoryEntries = categories.map((cat) => ({
1498
1881
  url: `${baseUrl}/articles/category/${cat.slug}`,
1499
- lastModified: /* @__PURE__ */ new Date(),
1882
+ lastModified: newestArticleDate(
1883
+ articles.filter(
1884
+ (article) => {
1885
+ var _a2;
1886
+ return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
1887
+ }
1888
+ )
1889
+ ),
1500
1890
  changeFrequency: "weekly",
1501
1891
  priority: 0.7
1502
1892
  }));
1503
- const authorEntries = typeof baseUrlOrConfig === "string" || baseUrlOrConfig.showAuthorPage === false ? [] : getAllAuthors(baseUrlOrConfig).map((author) => ({
1893
+ const authors = config && config.showAuthorPage !== false ? getAllAuthors(config) : [];
1894
+ const authorEntries = authors.map((author) => ({
1504
1895
  url: `${baseUrl}/articles/authors/${author.slug}`,
1896
+ lastModified: newestArticleDate(
1897
+ articles.filter(
1898
+ (article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
1899
+ )
1900
+ ),
1505
1901
  changeFrequency: "monthly",
1506
1902
  priority: 0.6
1507
1903
  }));
1508
- return [...articleEntries, ...categoryEntries, ...authorEntries];
1904
+ const seriesSlugs = [...new Set(articles.map((a) => a.seriesSlug).filter(Boolean))];
1905
+ const seriesEntries = seriesSlugs.map((seriesSlug) => ({
1906
+ url: `${baseUrl}/articles/series/${seriesSlug}`,
1907
+ lastModified: newestArticleDate(articles.filter((a) => a.seriesSlug === seriesSlug)),
1908
+ changeFrequency: "weekly",
1909
+ priority: 0.6
1910
+ }));
1911
+ const pageEntries = [];
1912
+ if ((config == null ? void 0 : config.listingPagination) === "pages") {
1913
+ const pageSize = (_b = config.pageSize) != null ? _b : DEFAULT_PAGE_SIZE;
1914
+ pageEntries.push(
1915
+ ...paginationEntries(
1916
+ `${baseUrl}/articles`,
1917
+ articles.length,
1918
+ pageSize,
1919
+ newestArticleDate(articles),
1920
+ 0.5
1921
+ )
1922
+ );
1923
+ for (const cat of categories) {
1924
+ const inCategory = articles.filter(
1925
+ (article) => {
1926
+ var _a2;
1927
+ return ((_a2 = article.categories) != null ? _a2 : []).some((name) => categoryToSlug(name) === cat.slug);
1928
+ }
1929
+ );
1930
+ pageEntries.push(
1931
+ ...paginationEntries(
1932
+ `${baseUrl}/articles/category/${cat.slug}`,
1933
+ inCategory.length,
1934
+ pageSize,
1935
+ newestArticleDate(inCategory),
1936
+ 0.4
1937
+ )
1938
+ );
1939
+ }
1940
+ for (const author of authors) {
1941
+ const byAuthor = articles.filter(
1942
+ (article) => getArticleAuthors(article, config).some((profile) => profile.slug === author.slug)
1943
+ );
1944
+ pageEntries.push(
1945
+ ...paginationEntries(
1946
+ `${baseUrl}/articles/authors/${author.slug}`,
1947
+ byAuthor.length,
1948
+ pageSize,
1949
+ newestArticleDate(byAuthor),
1950
+ 0.4
1951
+ )
1952
+ );
1953
+ }
1954
+ }
1955
+ return [
1956
+ ...articleEntries,
1957
+ ...categoryEntries,
1958
+ ...authorEntries,
1959
+ ...seriesEntries,
1960
+ ...pageEntries
1961
+ ];
1509
1962
  } catch (e) {
1510
1963
  return [];
1511
1964
  }
@@ -1679,8 +2132,32 @@ function ArticleTOC({ toc, className }) {
1679
2132
  );
1680
2133
  }
1681
2134
 
2135
+ // src/ArticleAnswer.tsx
2136
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2137
+ function ArticleAnswer({
2138
+ article,
2139
+ label = "The short answer",
2140
+ className
2141
+ }) {
2142
+ var _a;
2143
+ if (!((_a = article.answer) == null ? void 0 : _a.trim())) return null;
2144
+ return /* @__PURE__ */ jsxs3(
2145
+ "aside",
2146
+ {
2147
+ className: `mb-8 rounded-lg border-l-4 border-primary bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
2148
+ style: { borderLeftWidth: "4px" },
2149
+ children: [
2150
+ /* @__PURE__ */ jsx4("p", { className: "mb-2 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: label }),
2151
+ /* @__PURE__ */ jsx4("p", { className: "text-base leading-relaxed", children: article.answer })
2152
+ ]
2153
+ }
2154
+ );
2155
+ }
2156
+
1682
2157
  // src/validateArticles.ts
1683
2158
  var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
2159
+ var THIN_CONTENT_WORDS = 300;
2160
+ var STALE_CONTENT_MONTHS = 18;
1684
2161
  var SEARCH_TITLE_MAX = 60;
1685
2162
  var SEARCH_DESCRIPTION_MAX = 160;
1686
2163
  var SOCIAL_TITLE_MAX = 95;
@@ -1824,12 +2301,15 @@ function checkRequiredFrontmatter(articles) {
1824
2301
  }
1825
2302
  return issues;
1826
2303
  }
1827
- function checkDiscoveryFieldLengths(articles) {
2304
+ function checkDiscoveryFieldLengths(articles, config) {
2305
+ var _a, _b;
1828
2306
  const issues = [];
1829
2307
  for (const article of articles) {
2308
+ const effectiveTitle = formatPageTitle((_a = article.searchTitle) != null ? _a : article.title, config);
2309
+ const effectiveDescription = (_b = article.searchDescription) != null ? _b : article.excerpt;
1830
2310
  const checks = [
1831
- [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1832
- [article.searchDescription, "search-description-too-long", SEARCH_DESCRIPTION_MAX],
2311
+ [effectiveTitle, "effective-title-too-long", SEARCH_TITLE_MAX],
2312
+ [effectiveDescription, "effective-description-too-long", SEARCH_DESCRIPTION_MAX],
1833
2313
  [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1834
2314
  [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1835
2315
  ];
@@ -1846,6 +2326,101 @@ function checkDiscoveryFieldLengths(articles) {
1846
2326
  }
1847
2327
  return issues;
1848
2328
  }
2329
+ function checkAnswerability(articles) {
2330
+ var _a, _b, _c;
2331
+ const issues = [];
2332
+ for (const article of articles) {
2333
+ const hasQuestionHeading = ((_a = article.toc) != null ? _a : []).some(
2334
+ (item) => item.depth === 2 && item.text.trim().endsWith("?")
2335
+ );
2336
+ if (!article.answer && !((_b = article.faq) == null ? void 0 : _b.length) && !hasQuestionHeading) {
2337
+ issues.push({
2338
+ severity: "warning",
2339
+ code: "no-answer",
2340
+ message: "Article has no `answer`, no `faq`, and no question-shaped heading - nothing for an answer engine to lift.",
2341
+ articleSlug: article.slug
2342
+ });
2343
+ }
2344
+ if (article.wordCount !== void 0 && article.wordCount < THIN_CONTENT_WORDS) {
2345
+ issues.push({
2346
+ severity: "warning",
2347
+ code: "thin-content",
2348
+ message: `Article is ${article.wordCount} words (under ${THIN_CONTENT_WORDS}).`,
2349
+ articleSlug: article.slug
2350
+ });
2351
+ }
2352
+ if (!((_c = article.about) == null ? void 0 : _c.length)) {
2353
+ issues.push({
2354
+ severity: "warning",
2355
+ code: "missing-about",
2356
+ message: "Article has no `about` entity references.",
2357
+ articleSlug: article.slug
2358
+ });
2359
+ }
2360
+ }
2361
+ return issues;
2362
+ }
2363
+ function checkEntityReferences(articles, config) {
2364
+ var _a;
2365
+ const registry = config.entities;
2366
+ if (!registry) return [];
2367
+ const known = new Set(Object.values(registry).map((entity) => entity.name));
2368
+ const issues = [];
2369
+ for (const article of articles) {
2370
+ for (const entity of (_a = article.about) != null ? _a : []) {
2371
+ if (!known.has(entity.name) && !entity.sameAs) {
2372
+ issues.push({
2373
+ severity: "warning",
2374
+ code: "unknown-entity",
2375
+ message: `about entry "${entity.name}" is not in config.entities and has no sameAs.`,
2376
+ articleSlug: article.slug
2377
+ });
2378
+ }
2379
+ }
2380
+ }
2381
+ return issues;
2382
+ }
2383
+ function checkStaleContent(articles, now) {
2384
+ var _a;
2385
+ const cutoff = new Date(now);
2386
+ cutoff.setMonth(cutoff.getMonth() - STALE_CONTENT_MONTHS);
2387
+ const issues = [];
2388
+ for (const article of articles) {
2389
+ const stamp = (_a = article.lastmod) != null ? _a : article.date;
2390
+ if (!stamp) continue;
2391
+ const parsed = new Date(stamp);
2392
+ if (Number.isNaN(parsed.getTime())) continue;
2393
+ if (parsed < cutoff) {
2394
+ issues.push({
2395
+ severity: "warning",
2396
+ code: "stale-content",
2397
+ message: `Last updated ${stamp}, over ${STALE_CONTENT_MONTHS} months ago.`,
2398
+ articleSlug: article.slug
2399
+ });
2400
+ }
2401
+ }
2402
+ return issues;
2403
+ }
2404
+ function checkOrphanArticles(articles) {
2405
+ const bodies = articles.filter((article) => typeof article.content === "string");
2406
+ if (bodies.length === 0) return [];
2407
+ const issues = [];
2408
+ for (const article of articles) {
2409
+ const needle = `/articles/${article.slug}`;
2410
+ const linked = bodies.some(
2411
+ (other) => other.slug !== article.slug && other.content.includes(needle)
2412
+ );
2413
+ if (!linked) {
2414
+ issues.push({
2415
+ severity: "warning",
2416
+ code: "orphan-article",
2417
+ message: "No other article links to this one.",
2418
+ articleSlug: article.slug
2419
+ });
2420
+ }
2421
+ }
2422
+ return issues;
2423
+ }
1849
2424
  function checkCategorySlugs(articles) {
1850
2425
  var _a;
1851
2426
  const issues = [];
@@ -1869,7 +2444,8 @@ function checkCategorySlugs(articles) {
1869
2444
  }
1870
2445
  return issues;
1871
2446
  }
1872
- function validateArticles(articles, config) {
2447
+ function validateArticles(articles, config, options) {
2448
+ var _a;
1873
2449
  const errors = [
1874
2450
  ...checkDuplicateCanonicalUrls(articles),
1875
2451
  ...checkAuthorReferences(articles, config),
@@ -1879,15 +2455,19 @@ function validateArticles(articles, config) {
1879
2455
  ];
1880
2456
  const warnings = [
1881
2457
  ...checkRequiredFrontmatter(articles),
1882
- ...checkDiscoveryFieldLengths(articles),
2458
+ ...checkDiscoveryFieldLengths(articles, config),
2459
+ ...checkAnswerability(articles),
2460
+ ...checkEntityReferences(articles, config),
2461
+ ...checkStaleContent(articles, (_a = options == null ? void 0 : options.now) != null ? _a : /* @__PURE__ */ new Date()),
2462
+ ...checkOrphanArticles(articles),
1883
2463
  ...checkCategorySlugs(articles)
1884
2464
  ];
1885
2465
  return { ok: errors.length === 0, errors, warnings };
1886
2466
  }
1887
- function validateAllArticles(config) {
2467
+ function validateAllArticles(config, options) {
1888
2468
  return __async(this, null, function* () {
1889
2469
  const articles = yield getAllArticles(config);
1890
- return validateArticles(articles, config);
2470
+ return validateArticles(articles, config, options);
1891
2471
  });
1892
2472
  }
1893
2473
 
@@ -1900,16 +2480,21 @@ function emitArticleEvent(handler, event) {
1900
2480
  }
1901
2481
  }
1902
2482
  export {
2483
+ AI_CRAWLERS,
2484
+ ArticleAnswer,
1903
2485
  ArticleContent,
1904
2486
  ArticleTOC,
1905
2487
  buildArticleBreadcrumbs,
1906
2488
  buildAuthorBreadcrumbs,
1907
2489
  buildCategoryBreadcrumbs,
2490
+ buildMarkdownTwinHeader,
1908
2491
  buildPageUrl,
1909
2492
  buildPaginationLinks,
1910
2493
  categoryToSlug,
2494
+ deriveFaqFromHeadings,
1911
2495
  emitArticleEvent,
1912
2496
  extractToc,
2497
+ formatPageTitle,
1913
2498
  generateArticleMetadata,
1914
2499
  generateArticleStaticParams,
1915
2500
  generateArticlesIndexMetadata,
@@ -1921,6 +2506,8 @@ export {
1921
2506
  generateCategoryPageMetadata,
1922
2507
  generateCategoryStaticParams,
1923
2508
  generateListingPageStaticParams,
2509
+ generateLlmsFullTxt,
2510
+ generateLlmsTxt,
1924
2511
  generateRssFeed,
1925
2512
  generateSeriesMetadata,
1926
2513
  generateSeriesStaticParams,
@@ -1941,16 +2528,24 @@ export {
1941
2528
  getArticlesByCategory,
1942
2529
  getArticlesBySeries,
1943
2530
  getAuthorBySlug,
2531
+ getAuthorMarkdown,
1944
2532
  getAvailableArticleSlugs,
1945
2533
  getBreadcrumbsConfig,
2534
+ getCategoryMarkdown,
1946
2535
  getContentSlotBoundaries,
2536
+ getMarkdownTwinResponse,
2537
+ getOrganizationId,
1947
2538
  getPath,
1948
2539
  getPathArticles,
2540
+ getPersonId,
1949
2541
  getRelatedArticlesByCategory,
1950
2542
  getRelatedContent,
2543
+ getSeriesMarkdown,
1951
2544
  getTotalPages,
2545
+ getWebSiteId,
1952
2546
  isPageOutOfRange,
1953
2547
  markdownToHtml,
2548
+ matchAiCrawler,
1954
2549
  paginateArticles,
1955
2550
  parsePageParam,
1956
2551
  resolveAuthorAvatar,