@fullstackdatasolutions/articles 0.12.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +550 -3
  3. package/dist/index.cjs +960 -383
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +372 -20
  6. package/dist/index.d.ts +372 -20
  7. package/dist/index.js +944 -371
  8. package/dist/index.js.map +1 -1
  9. package/dist/nextjs.cjs +74 -6
  10. package/dist/nextjs.cjs.map +1 -1
  11. package/dist/nextjs.d.cts +141 -0
  12. package/dist/nextjs.d.ts +141 -0
  13. package/dist/nextjs.js +74 -6
  14. package/dist/nextjs.js.map +1 -1
  15. package/dist/server.cjs +665 -27
  16. package/dist/server.cjs.map +1 -1
  17. package/dist/server.d.cts +349 -3
  18. package/dist/server.d.ts +349 -3
  19. package/dist/server.js +643 -29
  20. package/dist/server.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/ArticleCard.tsx +37 -1
  23. package/src/ArticleContent.tsx +144 -5
  24. package/src/ArticleDetailHero.tsx +23 -0
  25. package/src/ArticleNavigation.tsx +32 -1
  26. package/src/ArticleSchemas.tsx +43 -39
  27. package/src/ArticleSocialShare.tsx +54 -10
  28. package/src/ArticlesPage.tsx +55 -5
  29. package/src/AuthorArticlesPage.tsx +308 -14
  30. package/src/AuthorCard.tsx +1 -1
  31. package/src/CategoryArticlesPage.tsx +34 -2
  32. package/src/LatestArticles.tsx +28 -1
  33. package/src/LatestArticlesSection.tsx +15 -1
  34. package/src/PaginationNav.tsx +78 -0
  35. package/src/RelatedArticlesSection.tsx +55 -0
  36. package/src/SeriesArticlesPage.tsx +66 -0
  37. package/src/__tests__/ArticleCard.test.tsx +63 -3
  38. package/src/__tests__/ArticleContent.test.tsx +143 -0
  39. package/src/__tests__/ArticleDetailHero.test.tsx +30 -0
  40. package/src/__tests__/ArticleNavigation.test.tsx +81 -3
  41. package/src/__tests__/ArticleSchemas.test.tsx +155 -81
  42. package/src/__tests__/ArticleSocialShare.test.tsx +54 -0
  43. package/src/__tests__/ArticlesPage.test.tsx +131 -0
  44. package/src/__tests__/AuthorArticlesPage.test.tsx +304 -3
  45. package/src/__tests__/CategoryArticlesPage.test.tsx +116 -1
  46. package/src/__tests__/LatestArticles.test.tsx +52 -0
  47. package/src/__tests__/LatestArticlesSection.test.tsx +28 -0
  48. package/src/__tests__/PaginationNav.test.tsx +73 -0
  49. package/src/__tests__/RelatedArticlesSection.test.tsx +132 -0
  50. package/src/__tests__/SeriesArticlesPage.test.tsx +121 -0
  51. package/src/__tests__/eventTracking.test.tsx +145 -0
  52. package/src/__tests__/events.test.ts +82 -0
  53. package/src/__tests__/markdown.test.ts +78 -1
  54. package/src/__tests__/pagination.test.ts +178 -0
  55. package/src/__tests__/seoUtils-authors.test.ts +37 -0
  56. package/src/__tests__/seoUtils.test.ts +246 -0
  57. package/src/__tests__/server-articles.test.ts +356 -1
  58. package/src/__tests__/validateArticles.test.ts +312 -0
  59. package/src/articleTypes.ts +109 -0
  60. package/src/articlesConfig.ts +37 -1
  61. package/src/eventTracking.tsx +97 -0
  62. package/src/events.ts +105 -0
  63. package/src/index.ts +26 -1
  64. package/src/markdown.ts +41 -0
  65. package/src/pagination.ts +93 -0
  66. package/src/seoUtils.ts +198 -11
  67. package/src/server-articles.ts +199 -6
  68. package/src/server.ts +46 -2
  69. package/src/validateArticles.ts +260 -0
package/dist/server.js CHANGED
@@ -57,6 +57,17 @@ import fs from "node:fs";
57
57
  import path from "node:path";
58
58
  import readingTime from "reading-time";
59
59
 
60
+ // src/authorUtils.ts
61
+ function getAuthorAvatar(author, config) {
62
+ if (!author.avatar) return void 0;
63
+ if (author.avatar.startsWith("http://") || author.avatar.startsWith("https://")) {
64
+ return author.avatar;
65
+ }
66
+ const path2 = `/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
67
+ if (!config) return path2;
68
+ return `${config.siteUrl.replace(/\/$/, "")}${path2}`;
69
+ }
70
+
60
71
  // src/markdown.ts
61
72
  import rehypePrism from "rehype-prism-plus";
62
73
  import rehypeSanitize from "rehype-sanitize";
@@ -379,6 +390,22 @@ function extractHeadingItem(node) {
379
390
  if (!id || !text) return null;
380
391
  return { id, depth: Number.parseInt(match[1], 10), text };
381
392
  }
393
+ function getContentSlotBoundaries(markdown) {
394
+ var _a, _b, _c;
395
+ try {
396
+ const tree = remark().use(remarkParse).use(remarkGfm).parse(markdown);
397
+ const paragraphs = ((_a = tree.children) != null ? _a : []).filter(
398
+ (node) => node.type === "paragraph" && Boolean(node.position)
399
+ );
400
+ if (paragraphs.length === 0) return null;
401
+ const introEnd = (_b = paragraphs[0].position.end.offset) != null ? _b : 0;
402
+ const midIndex = Math.floor(paragraphs.length / 2);
403
+ const mid = (_c = paragraphs[midIndex].position.end.offset) != null ? _c : introEnd;
404
+ return { introEnd, mid: Math.max(mid, introEnd), paragraphCount: paragraphs.length };
405
+ } catch (e) {
406
+ return null;
407
+ }
408
+ }
382
409
  function extractToc(markdown) {
383
410
  return __async(this, null, function* () {
384
411
  const headings = [];
@@ -399,8 +426,9 @@ var articlesDirectory = path.join(
399
426
  process.cwd(),
400
427
  "public/articles"
401
428
  );
402
- function getReadingTime(content) {
403
- return readingTime(content).text;
429
+ function getReadingStats(content) {
430
+ const stats = readingTime(content);
431
+ return { readTime: stats.text, wordCount: stats.words };
404
432
  }
405
433
  function findArticleImage(slug) {
406
434
  try {
@@ -520,6 +548,23 @@ function parseHowToSteps(raw) {
520
548
  );
521
549
  return steps.length ? steps : void 0;
522
550
  }
551
+ function parseOptionalString(raw) {
552
+ return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
553
+ }
554
+ function parseSeriesOrder(raw) {
555
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
556
+ }
557
+ function parsePrimaryAction(raw) {
558
+ if (typeof raw === "string") {
559
+ const actionId = raw.trim();
560
+ return actionId ? { actionId } : void 0;
561
+ }
562
+ if (typeof raw === "object" && raw !== null && typeof raw.actionId === "string") {
563
+ const actionId = raw.actionId.trim();
564
+ return actionId ? { actionId } : void 0;
565
+ }
566
+ return void 0;
567
+ }
523
568
  function parseAuthors(raw) {
524
569
  if (!Array.isArray(raw)) return void 0;
525
570
  const authors = raw.filter(
@@ -587,20 +632,26 @@ function getArticleSummary(slug, config) {
587
632
  if (!found) return null;
588
633
  const fileContent = fs.readFileSync(found.filePath, "utf8");
589
634
  const { data, content: markdownContent } = matter(fileContent);
590
- const readTime = getReadingTime(markdownContent);
635
+ const { readTime, wordCount } = getReadingStats(markdownContent);
591
636
  const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
592
637
  const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
638
+ const author = resolveArticleAuthorName(data.author, data.authors, config);
639
+ const authors = parseAuthors(data.authors);
640
+ const primaryAuthorProfile = config ? getArticleAuthors({ author, authors }, config)[0] : void 0;
593
641
  return {
594
642
  slug,
595
643
  title: data.title || slug.replaceAll("-", " "),
596
644
  excerpt: data.excerpt || "",
597
645
  date: parseDateField(data.date),
598
646
  lastmod: parseDateField(data.lastmod),
599
- author: resolveArticleAuthorName(data.author, data.authors, config),
600
- authors: parseAuthors(data.authors),
647
+ author,
648
+ authors,
649
+ authorSlug: primaryAuthorProfile == null ? void 0 : primaryAuthorProfile.slug,
650
+ authorAvatar: primaryAuthorProfile ? getAuthorAvatar(primaryAuthorProfile, config) : void 0,
601
651
  category: categories[0],
602
652
  categories,
603
653
  readTime,
654
+ wordCount,
604
655
  featuredImage: resolveFeaturedImage(data.featuredImage, slug),
605
656
  tags: data.tags || [],
606
657
  contentType: found.contentType,
@@ -610,7 +661,15 @@ function getArticleSummary(slug, config) {
610
661
  canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
611
662
  articleType: typeof data.articleType === "string" ? data.articleType : void 0,
612
663
  series: typeof data.series === "string" ? data.series : void 0,
613
- aiCrawl: data.aiCrawl === true
664
+ seriesSlug: parseOptionalString(data.seriesSlug),
665
+ seriesOrder: parseSeriesOrder(data.seriesOrder),
666
+ aiCrawl: data.aiCrawl === true,
667
+ searchTitle: parseOptionalString(data.searchTitle),
668
+ searchDescription: parseOptionalString(data.searchDescription),
669
+ socialTitle: parseOptionalString(data.socialTitle),
670
+ socialDescription: parseOptionalString(data.socialDescription),
671
+ socialImage: parseOptionalString(data.socialImage),
672
+ primaryAction: parsePrimaryAction(data.primaryAction)
614
673
  };
615
674
  } catch (error) {
616
675
  reportArticlesError({
@@ -790,6 +849,12 @@ function getArticlesByCategory(categorySlug, config) {
790
849
  );
791
850
  });
792
851
  }
852
+ function getRelatedArticlesByCategory(currentSlug, category, limit = 3, config) {
853
+ return __async(this, null, function* () {
854
+ const articles = yield getArticlesByCategory(categoryToSlug(category), config);
855
+ return articles.filter((article) => article.slug !== currentSlug).slice(0, limit);
856
+ });
857
+ }
793
858
  function getArticlesByAuthor(authorSlug, config) {
794
859
  return __async(this, null, function* () {
795
860
  const articles = yield getAllArticles(config);
@@ -798,6 +863,79 @@ function getArticlesByAuthor(authorSlug, config) {
798
863
  );
799
864
  });
800
865
  }
866
+ function getArticlesBySeries(seriesSlug, config) {
867
+ return __async(this, null, function* () {
868
+ const articles = yield getAllArticles(config);
869
+ return articles.filter((article) => article.seriesSlug === seriesSlug).sort((a, b) => {
870
+ var _a, _b;
871
+ const orderA = (_a = a.seriesOrder) != null ? _a : Number.POSITIVE_INFINITY;
872
+ const orderB = (_b = b.seriesOrder) != null ? _b : Number.POSITIVE_INFINITY;
873
+ return orderA - orderB;
874
+ });
875
+ });
876
+ }
877
+ function getAdjacentArticlesInSeries(currentSlug, seriesSlug, config) {
878
+ return __async(this, null, function* () {
879
+ const seriesArticles = yield getArticlesBySeries(seriesSlug, config);
880
+ const currentIndex = seriesArticles.findIndex((article) => article.slug === currentSlug);
881
+ if (currentIndex === -1) return { previous: null, next: null };
882
+ return {
883
+ previous: currentIndex > 0 ? seriesArticles[currentIndex - 1] : null,
884
+ next: currentIndex < seriesArticles.length - 1 ? seriesArticles[currentIndex + 1] : null
885
+ };
886
+ });
887
+ }
888
+ function getPath(pathKey, config) {
889
+ var _a, _b;
890
+ return (_b = (_a = config.paths) == null ? void 0 : _a[pathKey]) != null ? _b : null;
891
+ }
892
+ function getPathArticles(pathKey, config) {
893
+ return __async(this, null, function* () {
894
+ const path2 = getPath(pathKey, config);
895
+ if (!path2) return [];
896
+ const articles = yield getAllArticles(config);
897
+ const bySlug = new Map(articles.map((article) => [article.slug, article]));
898
+ return path2.articles.map((slug) => bySlug.get(slug)).filter((article) => Boolean(article));
899
+ });
900
+ }
901
+ function findPathForArticle(slug, config) {
902
+ var _a;
903
+ for (const [key, path2] of Object.entries((_a = config.paths) != null ? _a : {})) {
904
+ if (path2.articles.includes(slug)) return { key, path: path2 };
905
+ }
906
+ return null;
907
+ }
908
+ function getRelatedContent(article, config, limit = 3) {
909
+ return __async(this, null, function* () {
910
+ var _a;
911
+ const matchedPath = findPathForArticle(article.slug, config);
912
+ if (matchedPath) {
913
+ const pathArticles = yield getPathArticles(matchedPath.key, config);
914
+ return {
915
+ source: "path",
916
+ heading: matchedPath.path.name,
917
+ articles: pathArticles.filter((a) => a.slug !== article.slug),
918
+ pathKey: matchedPath.key,
919
+ nextAction: matchedPath.path.nextAction
920
+ };
921
+ }
922
+ if (article.seriesSlug) {
923
+ const seriesArticles = yield getArticlesBySeries(article.seriesSlug, config);
924
+ return {
925
+ source: "series",
926
+ heading: (_a = article.series) != null ? _a : "This series",
927
+ articles: seriesArticles.filter((a) => a.slug !== article.slug)
928
+ };
929
+ }
930
+ const categoryArticles = yield getRelatedArticlesByCategory(
931
+ article.slug,
932
+ article.category,
933
+ limit,
934
+ config
935
+ );
936
+ return { source: "category", heading: `More in ${article.category}`, articles: categoryArticles };
937
+ });
938
+ }
801
939
 
802
940
  // src/articlesConfig.ts
803
941
  function breadcrumbsAreEnabled(config) {
@@ -810,6 +948,48 @@ function getBreadcrumbsConfig(config) {
810
948
  return (_a = config.breadcrumbs) != null ? _a : {};
811
949
  }
812
950
 
951
+ // src/pagination.ts
952
+ function getTotalPages(totalCount, pageSize) {
953
+ if (totalCount <= 0 || pageSize <= 0) return 1;
954
+ return Math.max(1, Math.ceil(totalCount / pageSize));
955
+ }
956
+ function paginateArticles(articles, page, pageSize) {
957
+ const totalPages = getTotalPages(articles.length, pageSize);
958
+ const requestedPage = Math.trunc(page) || 1;
959
+ const clampedPage = Math.min(Math.max(requestedPage, 1), totalPages);
960
+ const start = (clampedPage - 1) * pageSize;
961
+ return {
962
+ articles: articles.slice(start, start + pageSize),
963
+ page: clampedPage,
964
+ totalPages,
965
+ hasPrevious: clampedPage > 1,
966
+ hasNext: clampedPage < totalPages
967
+ };
968
+ }
969
+ function buildPageUrl(basePath, page) {
970
+ const base = basePath.replace(/\/$/, "");
971
+ return page > 1 ? `${base}/page/${page}` : base;
972
+ }
973
+ function buildPaginationLinks(basePath, page, totalPages) {
974
+ return {
975
+ canonicalUrl: buildPageUrl(basePath, page),
976
+ prevUrl: page > 1 ? buildPageUrl(basePath, page - 1) : null,
977
+ nextUrl: page < totalPages ? buildPageUrl(basePath, page + 1) : null
978
+ };
979
+ }
980
+ function generateListingPageStaticParams(totalPages) {
981
+ const params = [];
982
+ for (let page = 2; page <= totalPages; page++) params.push({ page: String(page) });
983
+ return params;
984
+ }
985
+ function parsePageParam(raw) {
986
+ const parsed = Number.parseInt(raw != null ? raw : "", 10);
987
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
988
+ }
989
+ function isPageOutOfRange(page, totalPages) {
990
+ return page < 1 || page > totalPages;
991
+ }
992
+
813
993
  // src/seoUtils.ts
814
994
  function escapeXml(str) {
815
995
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
@@ -861,6 +1041,15 @@ function generateAuthorStaticParams(config) {
861
1041
  if (config.showAuthorPage === false) return [];
862
1042
  return getAllAuthors(config).map((author) => ({ author: author.slug }));
863
1043
  }
1044
+ function generateSeriesStaticParams(config) {
1045
+ return __async(this, null, function* () {
1046
+ const articles = yield getAllArticles(config);
1047
+ const seriesSlugs = new Set(
1048
+ articles.map((article) => article.seriesSlug).filter((slug) => Boolean(slug))
1049
+ );
1050
+ return [...seriesSlugs].map((series) => ({ series }));
1051
+ });
1052
+ }
864
1053
  function resolveImageUrl(featuredImage, siteUrl) {
865
1054
  const base = siteUrl.replace(/\/$/, "");
866
1055
  if (featuredImage.startsWith("http://") || featuredImage.startsWith("https://")) {
@@ -868,9 +1057,25 @@ function resolveImageUrl(featuredImage, siteUrl) {
868
1057
  }
869
1058
  return `${base}/${featuredImage.replace(/^\/+/, "")}`;
870
1059
  }
1060
+ function resolveSearchMetadata(article, config) {
1061
+ var _a, _b, _c;
1062
+ return {
1063
+ title: (_a = article.searchTitle) != null ? _a : article.title,
1064
+ description: (_c = (_b = article.searchDescription) != null ? _b : article.excerpt) != null ? _c : `Read ${article.title} on ${config.siteName}.`
1065
+ };
1066
+ }
1067
+ function resolveSocialMetadata(article, siteUrl) {
1068
+ var _a, _b, _c, _d;
1069
+ const image = (_a = article.socialImage) != null ? _a : article.featuredImage;
1070
+ return {
1071
+ title: (_b = article.socialTitle) != null ? _b : article.title,
1072
+ description: (_d = (_c = article.socialDescription) != null ? _c : article.excerpt) != null ? _d : "",
1073
+ imageUrl: image ? resolveImageUrl(image, siteUrl) : `${siteUrl}/placeholder-logo.png`
1074
+ };
1075
+ }
871
1076
  function generateArticleMetadata(slug, config) {
872
1077
  return __async(this, null, function* () {
873
- var _a, _b, _c, _d, _e, _f, _g;
1078
+ var _a, _b, _c, _d, _e, _f;
874
1079
  const article = yield getArticleMetadata(slug, config);
875
1080
  if (!article) {
876
1081
  return {
@@ -881,31 +1086,32 @@ function generateArticleMetadata(slug, config) {
881
1086
  const siteUrl = config.siteUrl.replace(/\/$/, "");
882
1087
  const articleUrl = `${siteUrl}/articles/${slug}`;
883
1088
  const canonicalUrl = (_a = article.canonicalUrl) != null ? _a : articleUrl;
884
- const imageUrl = article.featuredImage ? resolveImageUrl(article.featuredImage, siteUrl) : `${siteUrl}/placeholder-logo.png`;
885
- const description = (_b = article.excerpt) != null ? _b : `Read ${article.title} on ${config.siteName}.`;
1089
+ const search = resolveSearchMetadata(article, config);
1090
+ const social = resolveSocialMetadata(article, siteUrl);
1091
+ const description = search.description;
886
1092
  const showAuthor = config.showAuthor !== false;
887
1093
  const markdownUrl = getArticleMarkdownUrl(article, config);
888
1094
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
889
1095
  return {
890
- title: `${article.title} | ${config.siteName}`,
1096
+ title: `${search.title} | ${config.siteName}`,
891
1097
  description,
892
- keywords: [...((_c = article.tags) != null ? _c : []).map((tag) => tag.toLowerCase())].join(", "),
1098
+ keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
893
1099
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
894
- title: article.title,
895
- description,
1100
+ title: social.title,
1101
+ description: social.description || description,
896
1102
  url: articleUrl,
897
1103
  siteName: config.siteName,
898
- images: [{ url: imageUrl, width: 1200, height: 630, alt: article.title }],
1104
+ images: [{ url: social.imageUrl, width: 1200, height: 630, alt: social.title }],
899
1105
  locale: "en_US",
900
1106
  type: "article"
901
1107
  }, article.date && { publishedTime: article.date }), article.lastmod && { modifiedTime: new Date(article.lastmod).toISOString() }), showAuthor && authorNames.length > 0 && { authors: authorNames }), {
902
- tags: (_d = article.tags) != null ? _d : []
1108
+ tags: (_c = article.tags) != null ? _c : []
903
1109
  }),
904
1110
  twitter: {
905
1111
  card: "summary_large_image",
906
- title: article.title,
907
- description,
908
- images: [imageUrl]
1112
+ title: social.title,
1113
+ description: social.description || description,
1114
+ images: [social.imageUrl]
909
1115
  },
910
1116
  alternates: __spreadValues({
911
1117
  canonical: canonicalUrl
@@ -931,8 +1137,8 @@ function generateArticleMetadata(slug, config) {
931
1137
  "article:modified_time": new Date(article.lastmod).toISOString()
932
1138
  }), {
933
1139
  "article:section": article.category,
934
- "article:tag": (_f = (_e = article.tags) == null ? void 0 : _e.join(",")) != null ? _f : "",
935
- "linkedin:owner": (_g = process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID) != null ? _g : ""
1140
+ "article:tag": (_e = (_d = article.tags) == null ? void 0 : _d.join(",")) != null ? _e : "",
1141
+ "linkedin:owner": (_f = process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID) != null ? _f : ""
936
1142
  })
937
1143
  };
938
1144
  });
@@ -1024,6 +1230,50 @@ function generateCategoryMetadata(categorySlug, config) {
1024
1230
  };
1025
1231
  });
1026
1232
  }
1233
+ function generateSeriesMetadata(seriesSlug, config) {
1234
+ return __async(this, null, function* () {
1235
+ var _a;
1236
+ const articles = yield getArticlesBySeries(seriesSlug, config);
1237
+ if (articles.length === 0) return { title: "Series Not Found" };
1238
+ const seriesName = (_a = articles[0].series) != null ? _a : seriesSlug;
1239
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1240
+ const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1241
+ const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1242
+ const title = `${seriesName} Series | ${config.siteName}`;
1243
+ return {
1244
+ title,
1245
+ description,
1246
+ openGraph: {
1247
+ title: `${seriesName} Series`,
1248
+ description,
1249
+ url: seriesUrl,
1250
+ siteName: config.siteName,
1251
+ images: [{ url: articles[0].featuredImage }],
1252
+ type: "website",
1253
+ locale: "en_US"
1254
+ },
1255
+ twitter: {
1256
+ card: "summary_large_image",
1257
+ title: `${seriesName} Series`,
1258
+ description
1259
+ },
1260
+ alternates: {
1261
+ canonical: seriesUrl
1262
+ },
1263
+ robots: {
1264
+ index: true,
1265
+ follow: true,
1266
+ googleBot: {
1267
+ index: true,
1268
+ follow: true,
1269
+ "max-video-preview": -1,
1270
+ "max-image-preview": "large",
1271
+ "max-snippet": -1
1272
+ }
1273
+ }
1274
+ };
1275
+ });
1276
+ }
1027
1277
  function generateAuthorMetadata(authorSlug, config) {
1028
1278
  return __async(this, null, function* () {
1029
1279
  var _a;
@@ -1058,6 +1308,47 @@ function generateAuthorMetadata(authorSlug, config) {
1058
1308
  };
1059
1309
  });
1060
1310
  }
1311
+ function withPaginationMeta(base, basePath, page, totalPages) {
1312
+ if (!base.alternates) return base;
1313
+ const { canonicalUrl } = buildPaginationLinks(basePath, page, totalPages);
1314
+ const pageSuffix = page > 1 ? ` - Page ${page}` : "";
1315
+ const title = typeof base.title === "string" ? `${base.title}${pageSuffix}` : base.title;
1316
+ const openGraph = base.openGraph ? __spreadProps(__spreadValues({}, base.openGraph), {
1317
+ title: typeof base.openGraph.title === "string" ? `${base.openGraph.title}${pageSuffix}` : base.openGraph.title,
1318
+ url: canonicalUrl
1319
+ }) : base.openGraph;
1320
+ const twitter = base.twitter ? __spreadProps(__spreadValues({}, base.twitter), {
1321
+ title: typeof base.twitter.title === "string" ? `${base.twitter.title}${pageSuffix}` : base.twitter.title
1322
+ }) : base.twitter;
1323
+ return __spreadProps(__spreadValues({}, base), {
1324
+ title,
1325
+ openGraph,
1326
+ twitter,
1327
+ alternates: __spreadProps(__spreadValues({}, base.alternates), { canonical: canonicalUrl })
1328
+ });
1329
+ }
1330
+ function generateArticlesIndexPageMetadata(page, totalPages, config) {
1331
+ const base = generateArticlesIndexMetadata(config);
1332
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1333
+ return withPaginationMeta(base, `${siteUrl}/articles`, page, totalPages);
1334
+ }
1335
+ function generateCategoryPageMetadata(categorySlug, page, totalPages, config) {
1336
+ return __async(this, null, function* () {
1337
+ const base = yield generateCategoryMetadata(categorySlug, config);
1338
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1339
+ return withPaginationMeta(base, `${siteUrl}/articles/category/${categorySlug}`, page, totalPages);
1340
+ });
1341
+ }
1342
+ function generateAuthorPageMetadata(authorSlug, page, totalPages, config) {
1343
+ return __async(this, null, function* () {
1344
+ var _a;
1345
+ const base = yield generateAuthorMetadata(authorSlug, config);
1346
+ const author = getAuthorBySlug(authorSlug, config);
1347
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1348
+ const basePath = (_a = author == null ? void 0 : author.url) != null ? _a : `${siteUrl}/articles/authors/${authorSlug}`;
1349
+ return withPaginationMeta(base, basePath, page, totalPages);
1350
+ });
1351
+ }
1061
1352
  function formatCategoryName(category) {
1062
1353
  return category.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1063
1354
  }
@@ -1247,22 +1538,100 @@ function renderMdxSource(source, basePath, config) {
1247
1538
  }
1248
1539
 
1249
1540
  // src/ArticleContent.tsx
1250
- import { jsx as jsx2 } from "react/jsx-runtime";
1541
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1542
+ function buildSlotContext(article) {
1543
+ var _a, _b;
1544
+ return {
1545
+ slug: article.slug,
1546
+ title: article.title,
1547
+ category: article.category,
1548
+ tags: (_a = article.tags) != null ? _a : [],
1549
+ readTime: article.readTime,
1550
+ wordCount: article.wordCount,
1551
+ authorSlug: article.authorSlug,
1552
+ seriesSlug: article.seriesSlug,
1553
+ primaryActionId: (_b = article.primaryAction) == null ? void 0 : _b.actionId
1554
+ };
1555
+ }
1556
+ function resolveSlot(slot, context) {
1557
+ if (slot === void 0) return null;
1558
+ return typeof slot === "function" ? slot(context) : slot;
1559
+ }
1560
+ function renderSegment(markdown, contentType, slug, config) {
1561
+ return __async(this, null, function* () {
1562
+ if (!markdown.trim()) return null;
1563
+ if (contentType === "mdx") {
1564
+ return renderMdxSource(markdown, `/articles/${slug}`, config);
1565
+ }
1566
+ const html = yield markdownToHtml(markdown, slug, config);
1567
+ return /* @__PURE__ */ jsx2("div", { dangerouslySetInnerHTML: { __html: html } });
1568
+ });
1569
+ }
1251
1570
  function ArticleContent(_0) {
1252
- return __async(this, arguments, function* ({ article, className, config }) {
1253
- if (article.contentType === "mdx" && article.mdxSource) {
1254
- const content = yield renderMdxSource(article.mdxSource, `/articles/${article.slug}`, config);
1255
- return /* @__PURE__ */ jsx2("div", { className, children: content });
1571
+ return __async(this, arguments, function* ({
1572
+ article,
1573
+ className,
1574
+ config,
1575
+ afterHero,
1576
+ afterIntro,
1577
+ midContent,
1578
+ afterContent
1579
+ }) {
1580
+ var _a;
1581
+ const hasAnySlot = afterHero !== void 0 || afterIntro !== void 0 || midContent !== void 0 || afterContent !== void 0;
1582
+ if (!hasAnySlot) {
1583
+ if (article.contentType === "mdx" && article.mdxSource) {
1584
+ const content = yield renderMdxSource(article.mdxSource, `/articles/${article.slug}`, config);
1585
+ return /* @__PURE__ */ jsx2("div", { className, children: content });
1586
+ }
1587
+ return /* @__PURE__ */ jsx2("div", { className, dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
1588
+ }
1589
+ const slotContext = buildSlotContext(article);
1590
+ const heroNode = resolveSlot(afterHero, slotContext);
1591
+ const introNode = resolveSlot(afterIntro, slotContext);
1592
+ const midNode = resolveSlot(midContent, slotContext);
1593
+ const contentNode = resolveSlot(afterContent, slotContext);
1594
+ const needsSplit = Boolean(introNode || midNode);
1595
+ const rawSource = article.contentType === "mdx" ? article.mdxSource : (_a = article.content) != null ? _a : void 0;
1596
+ if (needsSplit && rawSource) {
1597
+ const boundaries = getContentSlotBoundaries(rawSource);
1598
+ if (boundaries) {
1599
+ try {
1600
+ const introSegment = rawSource.slice(0, boundaries.introEnd);
1601
+ const midSegment = rawSource.slice(boundaries.introEnd, boundaries.mid);
1602
+ const restSegment = rawSource.slice(boundaries.mid);
1603
+ const [introHtml, midHtml, restHtml] = yield Promise.all([
1604
+ renderSegment(introSegment, article.contentType, article.slug, config),
1605
+ renderSegment(midSegment, article.contentType, article.slug, config),
1606
+ renderSegment(restSegment, article.contentType, article.slug, config)
1607
+ ]);
1608
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1609
+ heroNode,
1610
+ introHtml,
1611
+ introNode,
1612
+ midHtml,
1613
+ midNode,
1614
+ restHtml,
1615
+ contentNode
1616
+ ] });
1617
+ } catch (e) {
1618
+ }
1619
+ }
1256
1620
  }
1257
- return /* @__PURE__ */ jsx2("div", { className, dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
1621
+ const wholeBody = article.contentType === "mdx" && article.mdxSource ? yield renderMdxSource(article.mdxSource, `/articles/${article.slug}`, config) : /* @__PURE__ */ jsx2("div", { dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
1622
+ return /* @__PURE__ */ jsxs("div", { className, children: [
1623
+ heroNode,
1624
+ wholeBody,
1625
+ contentNode
1626
+ ] });
1258
1627
  });
1259
1628
  }
1260
1629
 
1261
1630
  // src/ArticleTOC.tsx
1262
- import { jsx as jsx3, jsxs } from "react/jsx-runtime";
1631
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1263
1632
  function ArticleTOC({ toc, className }) {
1264
1633
  if (!toc.length) return null;
1265
- return /* @__PURE__ */ jsxs(
1634
+ return /* @__PURE__ */ jsxs2(
1266
1635
  "nav",
1267
1636
  {
1268
1637
  "aria-label": "Table of contents",
@@ -1281,23 +1650,254 @@ function ArticleTOC({ toc, className }) {
1281
1650
  }
1282
1651
  );
1283
1652
  }
1653
+
1654
+ // src/validateArticles.ts
1655
+ var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
1656
+ var SEARCH_TITLE_MAX = 60;
1657
+ var SEARCH_DESCRIPTION_MAX = 160;
1658
+ var SOCIAL_TITLE_MAX = 95;
1659
+ var SOCIAL_DESCRIPTION_MAX = 200;
1660
+ function isUnsafeUrl(href) {
1661
+ return UNSAFE_URL_SCHEME.test(href);
1662
+ }
1663
+ function checkDuplicateCanonicalUrls(articles) {
1664
+ const seen = /* @__PURE__ */ new Map();
1665
+ const issues = [];
1666
+ for (const article of articles) {
1667
+ if (!article.canonicalUrl) continue;
1668
+ const owner = seen.get(article.canonicalUrl);
1669
+ if (owner) {
1670
+ issues.push({
1671
+ severity: "error",
1672
+ code: "duplicate-canonical-url",
1673
+ message: `canonicalUrl "${article.canonicalUrl}" is also used by "${owner}".`,
1674
+ articleSlug: article.slug
1675
+ });
1676
+ } else {
1677
+ seen.set(article.canonicalUrl, article.slug);
1678
+ }
1679
+ }
1680
+ return issues;
1681
+ }
1682
+ function checkAuthorReferences(articles, config) {
1683
+ if (!config.authors || Object.keys(config.authors).length === 0) return [];
1684
+ const issues = [];
1685
+ for (const article of articles) {
1686
+ for (const resolved of getArticleAuthors(article, config)) {
1687
+ if (!getAuthorBySlug(resolved.slug, config)) {
1688
+ issues.push({
1689
+ severity: "error",
1690
+ code: "unknown-author-reference",
1691
+ message: `Author "${resolved.name}" does not match any entry in config.authors.`,
1692
+ articleSlug: article.slug
1693
+ });
1694
+ }
1695
+ }
1696
+ }
1697
+ return issues;
1698
+ }
1699
+ function checkSeriesCollisions(articles) {
1700
+ const issues = [];
1701
+ const seenSlugOrder = /* @__PURE__ */ new Map();
1702
+ for (const article of articles) {
1703
+ if (!article.seriesSlug || article.seriesOrder === void 0) continue;
1704
+ const key = `${article.seriesSlug}::${article.seriesOrder}`;
1705
+ const owner = seenSlugOrder.get(key);
1706
+ if (owner) {
1707
+ issues.push({
1708
+ severity: "error",
1709
+ code: "duplicate-series-order",
1710
+ message: `seriesOrder ${article.seriesOrder} in series "${article.seriesSlug}" collides with "${owner}".`,
1711
+ articleSlug: article.slug
1712
+ });
1713
+ } else {
1714
+ seenSlugOrder.set(key, article.slug);
1715
+ }
1716
+ }
1717
+ return issues;
1718
+ }
1719
+ function checkPaths(articles, config) {
1720
+ var _a;
1721
+ const issues = [];
1722
+ const bySlug = new Map(articles.map((article) => [article.slug, article]));
1723
+ for (const [pathKey, path2] of Object.entries((_a = config.paths) != null ? _a : {})) {
1724
+ if (path2.articles.length === 0) {
1725
+ issues.push({
1726
+ severity: "error",
1727
+ code: "empty-path",
1728
+ message: `Path "${pathKey}" has no articles.`,
1729
+ pathKey
1730
+ });
1731
+ }
1732
+ for (const slug of path2.articles) {
1733
+ const referenced = bySlug.get(slug);
1734
+ if (!referenced) {
1735
+ issues.push({
1736
+ severity: "error",
1737
+ code: "path-missing-article",
1738
+ message: `Path "${pathKey}" references missing article "${slug}".`,
1739
+ pathKey,
1740
+ articleSlug: slug
1741
+ });
1742
+ } else if (referenced.draft) {
1743
+ issues.push({
1744
+ severity: "error",
1745
+ code: "path-references-draft",
1746
+ message: `Path "${pathKey}" references unpublished (draft) article "${slug}".`,
1747
+ pathKey,
1748
+ articleSlug: slug
1749
+ });
1750
+ }
1751
+ }
1752
+ if (isUnsafeUrl(path2.nextAction.href)) {
1753
+ issues.push({
1754
+ severity: "error",
1755
+ code: "unsafe-url",
1756
+ message: `Path "${pathKey}" nextAction.href uses an unsafe URL scheme.`,
1757
+ pathKey
1758
+ });
1759
+ }
1760
+ }
1761
+ return issues;
1762
+ }
1763
+ function checkAuthorCtaUrls(config) {
1764
+ var _a;
1765
+ const issues = [];
1766
+ for (const author of Object.values((_a = config.authors) != null ? _a : {})) {
1767
+ if (author.primaryCta && isUnsafeUrl(author.primaryCta.href)) {
1768
+ issues.push({
1769
+ severity: "error",
1770
+ code: "unsafe-url",
1771
+ message: `Author "${author.slug}" primaryCta.href uses an unsafe URL scheme.`
1772
+ });
1773
+ }
1774
+ }
1775
+ return issues;
1776
+ }
1777
+ function checkRequiredFrontmatter(articles) {
1778
+ const issues = [];
1779
+ for (const article of articles) {
1780
+ if (!article.excerpt) {
1781
+ issues.push({
1782
+ severity: "warning",
1783
+ code: "missing-excerpt",
1784
+ message: "Article has no excerpt.",
1785
+ articleSlug: article.slug
1786
+ });
1787
+ }
1788
+ if (!article.date) {
1789
+ issues.push({
1790
+ severity: "warning",
1791
+ code: "missing-date",
1792
+ message: "Article has no date.",
1793
+ articleSlug: article.slug
1794
+ });
1795
+ }
1796
+ }
1797
+ return issues;
1798
+ }
1799
+ function checkDiscoveryFieldLengths(articles) {
1800
+ const issues = [];
1801
+ for (const article of articles) {
1802
+ const checks = [
1803
+ [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1804
+ [article.searchDescription, "search-description-too-long", SEARCH_DESCRIPTION_MAX],
1805
+ [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1806
+ [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1807
+ ];
1808
+ for (const [value, code, max] of checks) {
1809
+ if (value && value.length > max) {
1810
+ issues.push({
1811
+ severity: "warning",
1812
+ code,
1813
+ message: `${code.replaceAll("-", " ")} (${value.length} > ${max} recommended chars).`,
1814
+ articleSlug: article.slug
1815
+ });
1816
+ }
1817
+ }
1818
+ }
1819
+ return issues;
1820
+ }
1821
+ function checkCategorySlugs(articles) {
1822
+ var _a;
1823
+ const issues = [];
1824
+ const slugToNames = /* @__PURE__ */ new Map();
1825
+ for (const article of articles) {
1826
+ for (const category of article.categories) {
1827
+ const slug = categoryToSlug(category);
1828
+ const names = (_a = slugToNames.get(slug)) != null ? _a : /* @__PURE__ */ new Set();
1829
+ names.add(category);
1830
+ slugToNames.set(slug, names);
1831
+ }
1832
+ }
1833
+ for (const [slug, names] of slugToNames) {
1834
+ if (names.size > 1) {
1835
+ issues.push({
1836
+ severity: "warning",
1837
+ code: "category-slug-collision",
1838
+ message: `Categories [${[...names].join(", ")}] all collapse to slug "${slug}".`
1839
+ });
1840
+ }
1841
+ }
1842
+ return issues;
1843
+ }
1844
+ function validateArticles(articles, config) {
1845
+ const errors = [
1846
+ ...checkDuplicateCanonicalUrls(articles),
1847
+ ...checkAuthorReferences(articles, config),
1848
+ ...checkSeriesCollisions(articles),
1849
+ ...checkPaths(articles, config),
1850
+ ...checkAuthorCtaUrls(config)
1851
+ ];
1852
+ const warnings = [
1853
+ ...checkRequiredFrontmatter(articles),
1854
+ ...checkDiscoveryFieldLengths(articles),
1855
+ ...checkCategorySlugs(articles)
1856
+ ];
1857
+ return { ok: errors.length === 0, errors, warnings };
1858
+ }
1859
+ function validateAllArticles(config) {
1860
+ return __async(this, null, function* () {
1861
+ const articles = yield getAllArticles(config);
1862
+ return validateArticles(articles, config);
1863
+ });
1864
+ }
1865
+
1866
+ // src/events.ts
1867
+ function emitArticleEvent(handler, event) {
1868
+ if (!handler) return;
1869
+ try {
1870
+ handler(__spreadProps(__spreadValues({}, event), { timestamp: Date.now() }));
1871
+ } catch (e) {
1872
+ }
1873
+ }
1284
1874
  export {
1285
1875
  ArticleContent,
1286
1876
  ArticleTOC,
1287
1877
  buildArticleBreadcrumbs,
1288
1878
  buildAuthorBreadcrumbs,
1289
1879
  buildCategoryBreadcrumbs,
1880
+ buildPageUrl,
1881
+ buildPaginationLinks,
1290
1882
  categoryToSlug,
1883
+ emitArticleEvent,
1291
1884
  extractToc,
1292
1885
  generateArticleMetadata,
1293
1886
  generateArticleStaticParams,
1294
1887
  generateArticlesIndexMetadata,
1888
+ generateArticlesIndexPageMetadata,
1295
1889
  generateAuthorMetadata,
1890
+ generateAuthorPageMetadata,
1296
1891
  generateAuthorStaticParams,
1297
1892
  generateCategoryMetadata,
1893
+ generateCategoryPageMetadata,
1298
1894
  generateCategoryStaticParams,
1895
+ generateListingPageStaticParams,
1299
1896
  generateRssFeed,
1897
+ generateSeriesMetadata,
1898
+ generateSeriesStaticParams,
1300
1899
  getAdjacentArticles,
1900
+ getAdjacentArticlesInSeries,
1301
1901
  getAiRobotsTxtRules,
1302
1902
  getAllArticles,
1303
1903
  getAllAuthors,
@@ -1311,13 +1911,27 @@ export {
1311
1911
  getArticleSitemapEntries,
1312
1912
  getArticlesByAuthor,
1313
1913
  getArticlesByCategory,
1914
+ getArticlesBySeries,
1314
1915
  getAuthorBySlug,
1315
1916
  getAvailableArticleSlugs,
1316
1917
  getBreadcrumbsConfig,
1918
+ getContentSlotBoundaries,
1919
+ getPath,
1920
+ getPathArticles,
1921
+ getRelatedArticlesByCategory,
1922
+ getRelatedContent,
1923
+ getTotalPages,
1924
+ isPageOutOfRange,
1317
1925
  markdownToHtml,
1926
+ paginateArticles,
1927
+ parsePageParam,
1318
1928
  resolveAuthorAvatar,
1929
+ resolveSearchMetadata,
1930
+ resolveSocialMetadata,
1319
1931
  sanitizeImagePath2 as sanitizeImagePath,
1320
1932
  searchArticles,
1321
- setArticlesErrorHandler
1933
+ setArticlesErrorHandler,
1934
+ validateAllArticles,
1935
+ validateArticles
1322
1936
  };
1323
1937
  //# sourceMappingURL=server.js.map