@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.
- package/CHANGELOG.md +46 -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 +325 -31
- 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 +325 -31
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +660 -50
- 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 +645 -50
- 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 +434 -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 +385 -25
- 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,16 +879,186 @@ 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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1488
|
-
const dateStr = (
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
1832
|
-
[
|
|
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,
|