@fullstackdatasolutions/articles 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +313 -1
  3. package/dist/index.cjs +308 -79
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +267 -16
  6. package/dist/index.d.ts +267 -16
  7. package/dist/index.js +300 -79
  8. package/dist/index.js.map +1 -1
  9. package/dist/nextjs.cjs +320 -11
  10. package/dist/nextjs.cjs.map +1 -1
  11. package/dist/nextjs.d.cts +179 -2
  12. package/dist/nextjs.d.ts +179 -2
  13. package/dist/nextjs.js +320 -11
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +655 -47
  16. package/dist/server.cjs.map +1 -1
  17. package/dist/server.d.cts +333 -12
  18. package/dist/server.d.ts +333 -12
  19. package/dist/server.js +640 -47
  20. package/dist/server.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/ArticleAnswer.tsx +35 -0
  23. package/src/ArticleSchemas.tsx +263 -23
  24. package/src/AuthorArticlesPage.tsx +38 -8
  25. package/src/__tests__/ArticleAnswer.test.tsx +25 -0
  26. package/src/__tests__/ArticleSchemas.test.tsx +516 -0
  27. package/src/__tests__/AuthorArticlesPage.test.tsx +76 -0
  28. package/src/__tests__/authorUtils.test.ts +50 -0
  29. package/src/__tests__/markdown.test.ts +77 -1
  30. package/src/__tests__/nextjs.test.ts +31 -15
  31. package/src/__tests__/seoUtils.test.ts +279 -0
  32. package/src/__tests__/server-articles.test.ts +413 -1
  33. package/src/__tests__/validateArticles.test.ts +167 -6
  34. package/src/articleTypes.ts +57 -0
  35. package/src/articlesConfig.ts +176 -1
  36. package/src/authorUtils.ts +19 -1
  37. package/src/errorReporting.ts +1 -0
  38. package/src/index.ts +17 -1
  39. package/src/markdown.ts +100 -1
  40. package/src/nextjs.ts +7 -4
  41. package/src/seoUtils.ts +247 -26
  42. package/src/server-articles.ts +375 -24
  43. package/src/server.ts +35 -4
  44. package/src/validateArticles.ts +157 -12
package/dist/server.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,12 +879,180 @@ 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, {
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, {
774
1056
  headers: __spreadValues({
775
1057
  "Content-Type": "text/markdown; charset=utf-8",
776
1058
  "Cache-Control": "public, max-age=3600, s-maxage=3600"
@@ -795,22 +1077,15 @@ function getArticleAiHeaders(article, config) {
795
1077
  "X-Robots-Tag": "noai, noimageai"
796
1078
  };
797
1079
  }
798
- function getAiRobotsTxtRules() {
1080
+ function getAiRobotsTxtRules(config) {
799
1081
  return __async(this, null, function* () {
800
- const articles = yield getAllArticles();
1082
+ const articles = yield getAllArticles(config);
801
1083
  const blockedArticles = articles.filter((article) => article.aiCrawl !== true);
802
1084
  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
1085
  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");
1086
+ return AI_CRAWLERS.map((crawler) => [`User-agent: ${crawler}`, disallowRules].join("\n")).join(
1087
+ "\n\n"
1088
+ );
814
1089
  });
815
1090
  }
816
1091
  function searchArticles(query, config) {
@@ -950,6 +1225,7 @@ function getRelatedContent(article, config, limit = 3) {
950
1225
  }
951
1226
 
952
1227
  // src/articlesConfig.ts
1228
+ var DEFAULT_PAGE_SIZE = 6;
953
1229
  function breadcrumbsAreEnabled(config) {
954
1230
  var _a;
955
1231
  return config.breadcrumbs !== false && ((_a = config.breadcrumbs) == null ? void 0 : _a.show) !== false;
@@ -959,6 +1235,20 @@ function getBreadcrumbsConfig(config) {
959
1235
  if (config.breadcrumbs === false) return {};
960
1236
  return (_a = config.breadcrumbs) != null ? _a : {};
961
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
+ }
962
1252
 
963
1253
  // src/pagination.ts
964
1254
  function getTotalPages(totalCount, pageSize) {
@@ -1006,10 +1296,11 @@ function isPageOutOfRange(page, totalPages) {
1006
1296
  function escapeXml(str) {
1007
1297
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1008
1298
  }
1009
- function generateRssFeed(articles, config) {
1010
- var _a;
1299
+ function generateRssFeed(articles, config, options) {
1300
+ var _a, _b;
1011
1301
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1012
1302
  const showAuthor = config.showAuthor !== false;
1303
+ const fullContent = (options == null ? void 0 : options.fullContent) === true;
1013
1304
  const items = articles.map((article) => {
1014
1305
  const url = `${siteUrl}/articles/${article.slug}`;
1015
1306
  const pubDate = article.date ? new Date(article.date).toUTCString() : "";
@@ -1021,6 +1312,7 @@ function generateRssFeed(articles, config) {
1021
1312
  ` <guid isPermaLink="true">${url}</guid>`,
1022
1313
  pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
1023
1314
  article.excerpt ? ` <description><![CDATA[${article.excerpt}]]></description>` : "",
1315
+ fullContent && article.htmlContent ? ` <content:encoded><![CDATA[${article.htmlContent}]]></content:encoded>` : "",
1024
1316
  showAuthor && article.author ? ` <author>${escapeXml(article.author)}</author>` : "",
1025
1317
  article.category ? ` <category><![CDATA[${article.category}]]></category>` : "",
1026
1318
  imageUrl ? ` <media:content url="${imageUrl}" medium="image" width="1200" height="630"/>` : "",
@@ -1029,17 +1321,82 @@ function generateRssFeed(articles, config) {
1029
1321
  }).join("\n");
1030
1322
  const description = (_a = config.description) != null ? _a : `${config.siteName} articles`;
1031
1323
  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/">
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/">
1033
1325
  <channel>
1034
1326
  <title><![CDATA[${config.siteName}]]></title>
1035
1327
  <link>${siteUrl}/articles</link>
1036
1328
  <description><![CDATA[${description}]]></description>
1037
- <language>en</language>
1329
+ <language>${(_b = config.language) != null ? _b : "en"}</language>
1038
1330
  <atom:link href="${siteUrl}/articles/feed.xml" rel="self" type="application/rss+xml" />
1039
1331
  ${items}
1040
1332
  </channel>
1041
1333
  </rss>`;
1042
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
+ }
1043
1400
  function generateArticleStaticParams() {
1044
1401
  return getAvailableArticleSlugs().map((slug) => ({ slug }));
1045
1402
  }
@@ -1105,7 +1462,7 @@ function generateArticleMetadata(slug, config) {
1105
1462
  const markdownUrl = getArticleMarkdownUrl(article, config);
1106
1463
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
1107
1464
  return {
1108
- title: `${search.title} | ${config.siteName}`,
1465
+ title: formatPageTitle(search.title, config),
1109
1466
  description,
1110
1467
  keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
1111
1468
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
@@ -1159,7 +1516,7 @@ function generateArticlesIndexMetadata(config) {
1159
1516
  var _a, _b;
1160
1517
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1161
1518
  const indexUrl = `${siteUrl}/articles`;
1162
- const title = `Articles | ${config.siteName}`;
1519
+ const title = formatPageTitle("Articles", config);
1163
1520
  const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
1164
1521
  return {
1165
1522
  title,
@@ -1207,7 +1564,7 @@ function generateCategoryMetadata(categorySlug, config) {
1207
1564
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
1208
1565
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
1209
1566
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
1210
- const title = `${categoryName} Articles | ${config.siteName}`;
1567
+ const title = formatPageTitle(`${categoryName} Articles`, config);
1211
1568
  return {
1212
1569
  title,
1213
1570
  description,
@@ -1251,7 +1608,7 @@ function generateSeriesMetadata(seriesSlug, config) {
1251
1608
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1252
1609
  const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1253
1610
  const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1254
- const title = `${seriesName} Series | ${config.siteName}`;
1611
+ const title = formatPageTitle(`${seriesName} Series`, config);
1255
1612
  return {
1256
1613
  title,
1257
1614
  description,
@@ -1293,7 +1650,7 @@ function generateAuthorMetadata(authorSlug, config) {
1293
1650
  if (!author || config.showAuthorPage === false) return { title: "Author Not Found" };
1294
1651
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1295
1652
  const authorUrl = (_a = author.url) != null ? _a : `${siteUrl}/articles/authors/${author.slug}`;
1296
- const title = `${author.name} Articles | ${config.siteName}`;
1653
+ const title = formatPageTitle(`${author.name} Articles`, config);
1297
1654
  return {
1298
1655
  title,
1299
1656
  description: author.bio,
@@ -1475,17 +1832,41 @@ function resolveAuthorAvatar(author, config) {
1475
1832
  const siteUrl = config.siteUrl.replace(/\/$/, "");
1476
1833
  return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
1477
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
+ }
1478
1860
  function getArticleSitemapEntries(baseUrlOrConfig) {
1479
1861
  return __async(this, null, function* () {
1480
- 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(/\/$/, "");
1481
1865
  try {
1482
- const [articles, categories] = yield Promise.all([
1483
- getAllArticles(typeof baseUrlOrConfig === "string" ? void 0 : baseUrlOrConfig),
1484
- getAllCategories()
1485
- ]);
1866
+ const [articles, categories] = yield Promise.all([getAllArticles(config), getAllCategories()]);
1486
1867
  const articleEntries = articles.map((article) => {
1487
- var _a;
1488
- const dateStr = (_a = article.lastmod) != null ? _a : article.date;
1868
+ var _a2;
1869
+ const dateStr = (_a2 = article.lastmod) != null ? _a2 : article.date;
1489
1870
  const lastModified = dateStr ? new Date(dateStr) : void 0;
1490
1871
  return {
1491
1872
  url: `${baseUrl}/articles/${article.slug}`,
@@ -1496,16 +1877,86 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
1496
1877
  });
1497
1878
  const categoryEntries = categories.map((cat) => ({
1498
1879
  url: `${baseUrl}/articles/category/${cat.slug}`,
1499
- 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
+ ),
1500
1888
  changeFrequency: "weekly",
1501
1889
  priority: 0.7
1502
1890
  }));
1503
- 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) => ({
1504
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
+ ),
1505
1899
  changeFrequency: "monthly",
1506
1900
  priority: 0.6
1507
1901
  }));
1508
- 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
+ ];
1509
1960
  } catch (e) {
1510
1961
  return [];
1511
1962
  }
@@ -1679,8 +2130,32 @@ function ArticleTOC({ toc, className }) {
1679
2130
  );
1680
2131
  }
1681
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
+
1682
2155
  // src/validateArticles.ts
1683
2156
  var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
2157
+ var THIN_CONTENT_WORDS = 300;
2158
+ var STALE_CONTENT_MONTHS = 18;
1684
2159
  var SEARCH_TITLE_MAX = 60;
1685
2160
  var SEARCH_DESCRIPTION_MAX = 160;
1686
2161
  var SOCIAL_TITLE_MAX = 95;
@@ -1824,12 +2299,15 @@ function checkRequiredFrontmatter(articles) {
1824
2299
  }
1825
2300
  return issues;
1826
2301
  }
1827
- function checkDiscoveryFieldLengths(articles) {
2302
+ function checkDiscoveryFieldLengths(articles, config) {
2303
+ var _a, _b;
1828
2304
  const issues = [];
1829
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;
1830
2308
  const checks = [
1831
- [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1832
- [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],
1833
2311
  [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1834
2312
  [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1835
2313
  ];
@@ -1846,6 +2324,101 @@ function checkDiscoveryFieldLengths(articles) {
1846
2324
  }
1847
2325
  return issues;
1848
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
+ }
1849
2422
  function checkCategorySlugs(articles) {
1850
2423
  var _a;
1851
2424
  const issues = [];
@@ -1869,7 +2442,8 @@ function checkCategorySlugs(articles) {
1869
2442
  }
1870
2443
  return issues;
1871
2444
  }
1872
- function validateArticles(articles, config) {
2445
+ function validateArticles(articles, config, options) {
2446
+ var _a;
1873
2447
  const errors = [
1874
2448
  ...checkDuplicateCanonicalUrls(articles),
1875
2449
  ...checkAuthorReferences(articles, config),
@@ -1879,15 +2453,19 @@ function validateArticles(articles, config) {
1879
2453
  ];
1880
2454
  const warnings = [
1881
2455
  ...checkRequiredFrontmatter(articles),
1882
- ...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),
1883
2461
  ...checkCategorySlugs(articles)
1884
2462
  ];
1885
2463
  return { ok: errors.length === 0, errors, warnings };
1886
2464
  }
1887
- function validateAllArticles(config) {
2465
+ function validateAllArticles(config, options) {
1888
2466
  return __async(this, null, function* () {
1889
2467
  const articles = yield getAllArticles(config);
1890
- return validateArticles(articles, config);
2468
+ return validateArticles(articles, config, options);
1891
2469
  });
1892
2470
  }
1893
2471
 
@@ -1900,16 +2478,21 @@ function emitArticleEvent(handler, event) {
1900
2478
  }
1901
2479
  }
1902
2480
  export {
2481
+ AI_CRAWLERS,
2482
+ ArticleAnswer,
1903
2483
  ArticleContent,
1904
2484
  ArticleTOC,
1905
2485
  buildArticleBreadcrumbs,
1906
2486
  buildAuthorBreadcrumbs,
1907
2487
  buildCategoryBreadcrumbs,
2488
+ buildMarkdownTwinHeader,
1908
2489
  buildPageUrl,
1909
2490
  buildPaginationLinks,
1910
2491
  categoryToSlug,
2492
+ deriveFaqFromHeadings,
1911
2493
  emitArticleEvent,
1912
2494
  extractToc,
2495
+ formatPageTitle,
1913
2496
  generateArticleMetadata,
1914
2497
  generateArticleStaticParams,
1915
2498
  generateArticlesIndexMetadata,
@@ -1921,6 +2504,8 @@ export {
1921
2504
  generateCategoryPageMetadata,
1922
2505
  generateCategoryStaticParams,
1923
2506
  generateListingPageStaticParams,
2507
+ generateLlmsFullTxt,
2508
+ generateLlmsTxt,
1924
2509
  generateRssFeed,
1925
2510
  generateSeriesMetadata,
1926
2511
  generateSeriesStaticParams,
@@ -1941,16 +2526,24 @@ export {
1941
2526
  getArticlesByCategory,
1942
2527
  getArticlesBySeries,
1943
2528
  getAuthorBySlug,
2529
+ getAuthorMarkdown,
1944
2530
  getAvailableArticleSlugs,
1945
2531
  getBreadcrumbsConfig,
2532
+ getCategoryMarkdown,
1946
2533
  getContentSlotBoundaries,
2534
+ getMarkdownTwinResponse,
2535
+ getOrganizationId,
1947
2536
  getPath,
1948
2537
  getPathArticles,
2538
+ getPersonId,
1949
2539
  getRelatedArticlesByCategory,
1950
2540
  getRelatedContent,
2541
+ getSeriesMarkdown,
1951
2542
  getTotalPages,
2543
+ getWebSiteId,
1952
2544
  isPageOutOfRange,
1953
2545
  markdownToHtml,
2546
+ matchAiCrawler,
1954
2547
  paginateArticles,
1955
2548
  parsePageParam,
1956
2549
  resolveAuthorAvatar,