@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.
- package/CHANGELOG.md +40 -0
- package/README.md +313 -1
- package/dist/index.cjs +308 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +267 -16
- package/dist/index.d.ts +267 -16
- package/dist/index.js +300 -79
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +320 -11
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +179 -2
- package/dist/nextjs.d.ts +179 -2
- package/dist/nextjs.js +320 -11
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +655 -47
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +333 -12
- package/dist/server.d.ts +333 -12
- package/dist/server.js +640 -47
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticleAnswer.tsx +35 -0
- package/src/ArticleSchemas.tsx +263 -23
- package/src/AuthorArticlesPage.tsx +38 -8
- package/src/__tests__/ArticleAnswer.test.tsx +25 -0
- package/src/__tests__/ArticleSchemas.test.tsx +516 -0
- package/src/__tests__/AuthorArticlesPage.test.tsx +76 -0
- package/src/__tests__/authorUtils.test.ts +50 -0
- package/src/__tests__/markdown.test.ts +77 -1
- package/src/__tests__/nextjs.test.ts +31 -15
- package/src/__tests__/seoUtils.test.ts +279 -0
- package/src/__tests__/server-articles.test.ts +413 -1
- package/src/__tests__/validateArticles.test.ts +167 -6
- package/src/articleTypes.ts +57 -0
- package/src/articlesConfig.ts +176 -1
- package/src/authorUtils.ts +19 -1
- package/src/errorReporting.ts +1 -0
- package/src/index.ts +17 -1
- package/src/markdown.ts +100 -1
- package/src/nextjs.ts +7 -4
- package/src/seoUtils.ts +247 -26
- package/src/server-articles.ts +375 -24
- package/src/server.ts +35 -4
- 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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1488
|
-
const dateStr = (
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
1832
|
-
[
|
|
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,
|