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