@fullstackdatasolutions/articles 0.12.0 → 1.1.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 +34 -0
  2. package/README.md +559 -11
  3. package/dist/index.cjs +984 -388
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +383 -21
  6. package/dist/index.d.ts +383 -21
  7. package/dist/index.js +968 -376
  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 +149 -0
  12. package/dist/nextjs.d.ts +149 -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 +357 -3
  18. package/dist/server.d.ts +357 -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 +42 -6
  23. package/src/ArticleContent.tsx +144 -5
  24. package/src/ArticleDetailHero.tsx +33 -1
  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 +56 -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 +58 -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 +45 -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.cjs CHANGED
@@ -84,17 +84,27 @@ __export(server_exports, {
84
84
  buildArticleBreadcrumbs: () => buildArticleBreadcrumbs,
85
85
  buildAuthorBreadcrumbs: () => buildAuthorBreadcrumbs,
86
86
  buildCategoryBreadcrumbs: () => buildCategoryBreadcrumbs,
87
+ buildPageUrl: () => buildPageUrl,
88
+ buildPaginationLinks: () => buildPaginationLinks,
87
89
  categoryToSlug: () => categoryToSlug,
90
+ emitArticleEvent: () => emitArticleEvent,
88
91
  extractToc: () => extractToc,
89
92
  generateArticleMetadata: () => generateArticleMetadata,
90
93
  generateArticleStaticParams: () => generateArticleStaticParams,
91
94
  generateArticlesIndexMetadata: () => generateArticlesIndexMetadata,
95
+ generateArticlesIndexPageMetadata: () => generateArticlesIndexPageMetadata,
92
96
  generateAuthorMetadata: () => generateAuthorMetadata,
97
+ generateAuthorPageMetadata: () => generateAuthorPageMetadata,
93
98
  generateAuthorStaticParams: () => generateAuthorStaticParams,
94
99
  generateCategoryMetadata: () => generateCategoryMetadata,
100
+ generateCategoryPageMetadata: () => generateCategoryPageMetadata,
95
101
  generateCategoryStaticParams: () => generateCategoryStaticParams,
102
+ generateListingPageStaticParams: () => generateListingPageStaticParams,
96
103
  generateRssFeed: () => generateRssFeed,
104
+ generateSeriesMetadata: () => generateSeriesMetadata,
105
+ generateSeriesStaticParams: () => generateSeriesStaticParams,
97
106
  getAdjacentArticles: () => getAdjacentArticles,
107
+ getAdjacentArticlesInSeries: () => getAdjacentArticlesInSeries,
98
108
  getAiRobotsTxtRules: () => getAiRobotsTxtRules,
99
109
  getAllArticles: () => getAllArticles,
100
110
  getAllAuthors: () => getAllAuthors,
@@ -108,14 +118,28 @@ __export(server_exports, {
108
118
  getArticleSitemapEntries: () => getArticleSitemapEntries,
109
119
  getArticlesByAuthor: () => getArticlesByAuthor,
110
120
  getArticlesByCategory: () => getArticlesByCategory,
121
+ getArticlesBySeries: () => getArticlesBySeries,
111
122
  getAuthorBySlug: () => getAuthorBySlug,
112
123
  getAvailableArticleSlugs: () => getAvailableArticleSlugs,
113
124
  getBreadcrumbsConfig: () => getBreadcrumbsConfig,
125
+ getContentSlotBoundaries: () => getContentSlotBoundaries,
126
+ getPath: () => getPath,
127
+ getPathArticles: () => getPathArticles,
128
+ getRelatedArticlesByCategory: () => getRelatedArticlesByCategory,
129
+ getRelatedContent: () => getRelatedContent,
130
+ getTotalPages: () => getTotalPages,
131
+ isPageOutOfRange: () => isPageOutOfRange,
114
132
  markdownToHtml: () => markdownToHtml,
133
+ paginateArticles: () => paginateArticles,
134
+ parsePageParam: () => parsePageParam,
115
135
  resolveAuthorAvatar: () => resolveAuthorAvatar,
136
+ resolveSearchMetadata: () => resolveSearchMetadata,
137
+ resolveSocialMetadata: () => resolveSocialMetadata,
116
138
  sanitizeImagePath: () => sanitizeImagePath2,
117
139
  searchArticles: () => searchArticles,
118
- setArticlesErrorHandler: () => setArticlesErrorHandler
140
+ setArticlesErrorHandler: () => setArticlesErrorHandler,
141
+ validateAllArticles: () => validateAllArticles,
142
+ validateArticles: () => validateArticles
119
143
  });
120
144
  module.exports = __toCommonJS(server_exports);
121
145
 
@@ -126,6 +150,17 @@ var import_node_fs = __toESM(require("fs"), 1);
126
150
  var import_node_path = __toESM(require("path"), 1);
127
151
  var import_reading_time = __toESM(require("reading-time"), 1);
128
152
 
153
+ // src/authorUtils.ts
154
+ function getAuthorAvatar(author, config) {
155
+ if (!author.avatar) return void 0;
156
+ if (author.avatar.startsWith("http://") || author.avatar.startsWith("https://")) {
157
+ return author.avatar;
158
+ }
159
+ const path2 = `/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, "")}`;
160
+ if (!config) return path2;
161
+ return `${config.siteUrl.replace(/\/$/, "")}${path2}`;
162
+ }
163
+
129
164
  // src/markdown.ts
130
165
  var import_rehype_prism_plus = __toESM(require("rehype-prism-plus"), 1);
131
166
  var import_rehype_sanitize = __toESM(require("rehype-sanitize"), 1);
@@ -448,6 +483,22 @@ function extractHeadingItem(node) {
448
483
  if (!id || !text) return null;
449
484
  return { id, depth: Number.parseInt(match[1], 10), text };
450
485
  }
486
+ function getContentSlotBoundaries(markdown) {
487
+ var _a, _b, _c;
488
+ try {
489
+ const tree = (0, import_remark.remark)().use(import_remark_parse.default).use(import_remark_gfm.default).parse(markdown);
490
+ const paragraphs = ((_a = tree.children) != null ? _a : []).filter(
491
+ (node) => node.type === "paragraph" && Boolean(node.position)
492
+ );
493
+ if (paragraphs.length === 0) return null;
494
+ const introEnd = (_b = paragraphs[0].position.end.offset) != null ? _b : 0;
495
+ const midIndex = Math.floor(paragraphs.length / 2);
496
+ const mid = (_c = paragraphs[midIndex].position.end.offset) != null ? _c : introEnd;
497
+ return { introEnd, mid: Math.max(mid, introEnd), paragraphCount: paragraphs.length };
498
+ } catch (e) {
499
+ return null;
500
+ }
501
+ }
451
502
  function extractToc(markdown) {
452
503
  return __async(this, null, function* () {
453
504
  const headings = [];
@@ -468,8 +519,9 @@ var articlesDirectory = import_node_path.default.join(
468
519
  process.cwd(),
469
520
  "public/articles"
470
521
  );
471
- function getReadingTime(content) {
472
- return (0, import_reading_time.default)(content).text;
522
+ function getReadingStats(content) {
523
+ const stats = (0, import_reading_time.default)(content);
524
+ return { readTime: stats.text, wordCount: stats.words };
473
525
  }
474
526
  function findArticleImage(slug) {
475
527
  try {
@@ -589,6 +641,23 @@ function parseHowToSteps(raw) {
589
641
  );
590
642
  return steps.length ? steps : void 0;
591
643
  }
644
+ function parseOptionalString(raw) {
645
+ return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : void 0;
646
+ }
647
+ function parseSeriesOrder(raw) {
648
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
649
+ }
650
+ function parsePrimaryAction(raw) {
651
+ if (typeof raw === "string") {
652
+ const actionId = raw.trim();
653
+ return actionId ? { actionId } : void 0;
654
+ }
655
+ if (typeof raw === "object" && raw !== null && typeof raw.actionId === "string") {
656
+ const actionId = raw.actionId.trim();
657
+ return actionId ? { actionId } : void 0;
658
+ }
659
+ return void 0;
660
+ }
592
661
  function parseAuthors(raw) {
593
662
  if (!Array.isArray(raw)) return void 0;
594
663
  const authors = raw.filter(
@@ -656,20 +725,26 @@ function getArticleSummary(slug, config) {
656
725
  if (!found) return null;
657
726
  const fileContent = import_node_fs.default.readFileSync(found.filePath, "utf8");
658
727
  const { data, content: markdownContent } = (0, import_gray_matter.default)(fileContent);
659
- const readTime = getReadingTime(markdownContent);
728
+ const { readTime, wordCount } = getReadingStats(markdownContent);
660
729
  const allTags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string" && String(t).trim()) : [];
661
730
  const categories = allTags.length > 0 ? allTags.map((t) => t.replaceAll("-", " ").trim()) : ["Campaigns"];
731
+ const author = resolveArticleAuthorName(data.author, data.authors, config);
732
+ const authors = parseAuthors(data.authors);
733
+ const primaryAuthorProfile = config ? getArticleAuthors({ author, authors }, config)[0] : void 0;
662
734
  return {
663
735
  slug,
664
736
  title: data.title || slug.replaceAll("-", " "),
665
737
  excerpt: data.excerpt || "",
666
738
  date: parseDateField(data.date),
667
739
  lastmod: parseDateField(data.lastmod),
668
- author: resolveArticleAuthorName(data.author, data.authors, config),
669
- authors: parseAuthors(data.authors),
740
+ author,
741
+ authors,
742
+ authorSlug: primaryAuthorProfile == null ? void 0 : primaryAuthorProfile.slug,
743
+ authorAvatar: primaryAuthorProfile ? getAuthorAvatar(primaryAuthorProfile, config) : void 0,
670
744
  category: categories[0],
671
745
  categories,
672
746
  readTime,
747
+ wordCount,
673
748
  featuredImage: resolveFeaturedImage(data.featuredImage, slug),
674
749
  tags: data.tags || [],
675
750
  contentType: found.contentType,
@@ -679,7 +754,15 @@ function getArticleSummary(slug, config) {
679
754
  canonicalUrl: typeof data.canonicalUrl === "string" ? data.canonicalUrl : void 0,
680
755
  articleType: typeof data.articleType === "string" ? data.articleType : void 0,
681
756
  series: typeof data.series === "string" ? data.series : void 0,
682
- aiCrawl: data.aiCrawl === true
757
+ seriesSlug: parseOptionalString(data.seriesSlug),
758
+ seriesOrder: parseSeriesOrder(data.seriesOrder),
759
+ aiCrawl: data.aiCrawl === true,
760
+ searchTitle: parseOptionalString(data.searchTitle),
761
+ searchDescription: parseOptionalString(data.searchDescription),
762
+ socialTitle: parseOptionalString(data.socialTitle),
763
+ socialDescription: parseOptionalString(data.socialDescription),
764
+ socialImage: parseOptionalString(data.socialImage),
765
+ primaryAction: parsePrimaryAction(data.primaryAction)
683
766
  };
684
767
  } catch (error) {
685
768
  reportArticlesError({
@@ -859,6 +942,12 @@ function getArticlesByCategory(categorySlug, config) {
859
942
  );
860
943
  });
861
944
  }
945
+ function getRelatedArticlesByCategory(currentSlug, category, limit = 3, config) {
946
+ return __async(this, null, function* () {
947
+ const articles = yield getArticlesByCategory(categoryToSlug(category), config);
948
+ return articles.filter((article) => article.slug !== currentSlug).slice(0, limit);
949
+ });
950
+ }
862
951
  function getArticlesByAuthor(authorSlug, config) {
863
952
  return __async(this, null, function* () {
864
953
  const articles = yield getAllArticles(config);
@@ -867,6 +956,79 @@ function getArticlesByAuthor(authorSlug, config) {
867
956
  );
868
957
  });
869
958
  }
959
+ function getArticlesBySeries(seriesSlug, config) {
960
+ return __async(this, null, function* () {
961
+ const articles = yield getAllArticles(config);
962
+ return articles.filter((article) => article.seriesSlug === seriesSlug).sort((a, b) => {
963
+ var _a, _b;
964
+ const orderA = (_a = a.seriesOrder) != null ? _a : Number.POSITIVE_INFINITY;
965
+ const orderB = (_b = b.seriesOrder) != null ? _b : Number.POSITIVE_INFINITY;
966
+ return orderA - orderB;
967
+ });
968
+ });
969
+ }
970
+ function getAdjacentArticlesInSeries(currentSlug, seriesSlug, config) {
971
+ return __async(this, null, function* () {
972
+ const seriesArticles = yield getArticlesBySeries(seriesSlug, config);
973
+ const currentIndex = seriesArticles.findIndex((article) => article.slug === currentSlug);
974
+ if (currentIndex === -1) return { previous: null, next: null };
975
+ return {
976
+ previous: currentIndex > 0 ? seriesArticles[currentIndex - 1] : null,
977
+ next: currentIndex < seriesArticles.length - 1 ? seriesArticles[currentIndex + 1] : null
978
+ };
979
+ });
980
+ }
981
+ function getPath(pathKey, config) {
982
+ var _a, _b;
983
+ return (_b = (_a = config.paths) == null ? void 0 : _a[pathKey]) != null ? _b : null;
984
+ }
985
+ function getPathArticles(pathKey, config) {
986
+ return __async(this, null, function* () {
987
+ const path2 = getPath(pathKey, config);
988
+ if (!path2) return [];
989
+ const articles = yield getAllArticles(config);
990
+ const bySlug = new Map(articles.map((article) => [article.slug, article]));
991
+ return path2.articles.map((slug) => bySlug.get(slug)).filter((article) => Boolean(article));
992
+ });
993
+ }
994
+ function findPathForArticle(slug, config) {
995
+ var _a;
996
+ for (const [key, path2] of Object.entries((_a = config.paths) != null ? _a : {})) {
997
+ if (path2.articles.includes(slug)) return { key, path: path2 };
998
+ }
999
+ return null;
1000
+ }
1001
+ function getRelatedContent(article, config, limit = 3) {
1002
+ return __async(this, null, function* () {
1003
+ var _a;
1004
+ const matchedPath = findPathForArticle(article.slug, config);
1005
+ if (matchedPath) {
1006
+ const pathArticles = yield getPathArticles(matchedPath.key, config);
1007
+ return {
1008
+ source: "path",
1009
+ heading: matchedPath.path.name,
1010
+ articles: pathArticles.filter((a) => a.slug !== article.slug),
1011
+ pathKey: matchedPath.key,
1012
+ nextAction: matchedPath.path.nextAction
1013
+ };
1014
+ }
1015
+ if (article.seriesSlug) {
1016
+ const seriesArticles = yield getArticlesBySeries(article.seriesSlug, config);
1017
+ return {
1018
+ source: "series",
1019
+ heading: (_a = article.series) != null ? _a : "This series",
1020
+ articles: seriesArticles.filter((a) => a.slug !== article.slug)
1021
+ };
1022
+ }
1023
+ const categoryArticles = yield getRelatedArticlesByCategory(
1024
+ article.slug,
1025
+ article.category,
1026
+ limit,
1027
+ config
1028
+ );
1029
+ return { source: "category", heading: `More in ${article.category}`, articles: categoryArticles };
1030
+ });
1031
+ }
870
1032
 
871
1033
  // src/articlesConfig.ts
872
1034
  function breadcrumbsAreEnabled(config) {
@@ -879,6 +1041,48 @@ function getBreadcrumbsConfig(config) {
879
1041
  return (_a = config.breadcrumbs) != null ? _a : {};
880
1042
  }
881
1043
 
1044
+ // src/pagination.ts
1045
+ function getTotalPages(totalCount, pageSize) {
1046
+ if (totalCount <= 0 || pageSize <= 0) return 1;
1047
+ return Math.max(1, Math.ceil(totalCount / pageSize));
1048
+ }
1049
+ function paginateArticles(articles, page, pageSize) {
1050
+ const totalPages = getTotalPages(articles.length, pageSize);
1051
+ const requestedPage = Math.trunc(page) || 1;
1052
+ const clampedPage = Math.min(Math.max(requestedPage, 1), totalPages);
1053
+ const start = (clampedPage - 1) * pageSize;
1054
+ return {
1055
+ articles: articles.slice(start, start + pageSize),
1056
+ page: clampedPage,
1057
+ totalPages,
1058
+ hasPrevious: clampedPage > 1,
1059
+ hasNext: clampedPage < totalPages
1060
+ };
1061
+ }
1062
+ function buildPageUrl(basePath, page) {
1063
+ const base = basePath.replace(/\/$/, "");
1064
+ return page > 1 ? `${base}/page/${page}` : base;
1065
+ }
1066
+ function buildPaginationLinks(basePath, page, totalPages) {
1067
+ return {
1068
+ canonicalUrl: buildPageUrl(basePath, page),
1069
+ prevUrl: page > 1 ? buildPageUrl(basePath, page - 1) : null,
1070
+ nextUrl: page < totalPages ? buildPageUrl(basePath, page + 1) : null
1071
+ };
1072
+ }
1073
+ function generateListingPageStaticParams(totalPages) {
1074
+ const params = [];
1075
+ for (let page = 2; page <= totalPages; page++) params.push({ page: String(page) });
1076
+ return params;
1077
+ }
1078
+ function parsePageParam(raw) {
1079
+ const parsed = Number.parseInt(raw != null ? raw : "", 10);
1080
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
1081
+ }
1082
+ function isPageOutOfRange(page, totalPages) {
1083
+ return page < 1 || page > totalPages;
1084
+ }
1085
+
882
1086
  // src/seoUtils.ts
883
1087
  function escapeXml(str) {
884
1088
  return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
@@ -930,6 +1134,15 @@ function generateAuthorStaticParams(config) {
930
1134
  if (config.showAuthorPage === false) return [];
931
1135
  return getAllAuthors(config).map((author) => ({ author: author.slug }));
932
1136
  }
1137
+ function generateSeriesStaticParams(config) {
1138
+ return __async(this, null, function* () {
1139
+ const articles = yield getAllArticles(config);
1140
+ const seriesSlugs = new Set(
1141
+ articles.map((article) => article.seriesSlug).filter((slug) => Boolean(slug))
1142
+ );
1143
+ return [...seriesSlugs].map((series) => ({ series }));
1144
+ });
1145
+ }
933
1146
  function resolveImageUrl(featuredImage, siteUrl) {
934
1147
  const base = siteUrl.replace(/\/$/, "");
935
1148
  if (featuredImage.startsWith("http://") || featuredImage.startsWith("https://")) {
@@ -937,9 +1150,25 @@ function resolveImageUrl(featuredImage, siteUrl) {
937
1150
  }
938
1151
  return `${base}/${featuredImage.replace(/^\/+/, "")}`;
939
1152
  }
1153
+ function resolveSearchMetadata(article, config) {
1154
+ var _a, _b, _c;
1155
+ return {
1156
+ title: (_a = article.searchTitle) != null ? _a : article.title,
1157
+ description: (_c = (_b = article.searchDescription) != null ? _b : article.excerpt) != null ? _c : `Read ${article.title} on ${config.siteName}.`
1158
+ };
1159
+ }
1160
+ function resolveSocialMetadata(article, siteUrl) {
1161
+ var _a, _b, _c, _d;
1162
+ const image = (_a = article.socialImage) != null ? _a : article.featuredImage;
1163
+ return {
1164
+ title: (_b = article.socialTitle) != null ? _b : article.title,
1165
+ description: (_d = (_c = article.socialDescription) != null ? _c : article.excerpt) != null ? _d : "",
1166
+ imageUrl: image ? resolveImageUrl(image, siteUrl) : `${siteUrl}/placeholder-logo.png`
1167
+ };
1168
+ }
940
1169
  function generateArticleMetadata(slug, config) {
941
1170
  return __async(this, null, function* () {
942
- var _a, _b, _c, _d, _e, _f, _g;
1171
+ var _a, _b, _c, _d, _e, _f;
943
1172
  const article = yield getArticleMetadata(slug, config);
944
1173
  if (!article) {
945
1174
  return {
@@ -950,31 +1179,32 @@ function generateArticleMetadata(slug, config) {
950
1179
  const siteUrl = config.siteUrl.replace(/\/$/, "");
951
1180
  const articleUrl = `${siteUrl}/articles/${slug}`;
952
1181
  const canonicalUrl = (_a = article.canonicalUrl) != null ? _a : articleUrl;
953
- const imageUrl = article.featuredImage ? resolveImageUrl(article.featuredImage, siteUrl) : `${siteUrl}/placeholder-logo.png`;
954
- const description = (_b = article.excerpt) != null ? _b : `Read ${article.title} on ${config.siteName}.`;
1182
+ const search = resolveSearchMetadata(article, config);
1183
+ const social = resolveSocialMetadata(article, siteUrl);
1184
+ const description = search.description;
955
1185
  const showAuthor = config.showAuthor !== false;
956
1186
  const markdownUrl = getArticleMarkdownUrl(article, config);
957
1187
  const authorNames = getArticleAuthors(article, config).map((author) => author.name);
958
1188
  return {
959
- title: `${article.title} | ${config.siteName}`,
1189
+ title: `${search.title} | ${config.siteName}`,
960
1190
  description,
961
- keywords: [...((_c = article.tags) != null ? _c : []).map((tag) => tag.toLowerCase())].join(", "),
1191
+ keywords: [...((_b = article.tags) != null ? _b : []).map((tag) => tag.toLowerCase())].join(", "),
962
1192
  openGraph: __spreadProps(__spreadValues(__spreadValues(__spreadValues({
963
- title: article.title,
964
- description,
1193
+ title: social.title,
1194
+ description: social.description || description,
965
1195
  url: articleUrl,
966
1196
  siteName: config.siteName,
967
- images: [{ url: imageUrl, width: 1200, height: 630, alt: article.title }],
1197
+ images: [{ url: social.imageUrl, width: 1200, height: 630, alt: social.title }],
968
1198
  locale: "en_US",
969
1199
  type: "article"
970
1200
  }, article.date && { publishedTime: article.date }), article.lastmod && { modifiedTime: new Date(article.lastmod).toISOString() }), showAuthor && authorNames.length > 0 && { authors: authorNames }), {
971
- tags: (_d = article.tags) != null ? _d : []
1201
+ tags: (_c = article.tags) != null ? _c : []
972
1202
  }),
973
1203
  twitter: {
974
1204
  card: "summary_large_image",
975
- title: article.title,
976
- description,
977
- images: [imageUrl]
1205
+ title: social.title,
1206
+ description: social.description || description,
1207
+ images: [social.imageUrl]
978
1208
  },
979
1209
  alternates: __spreadValues({
980
1210
  canonical: canonicalUrl
@@ -1000,8 +1230,8 @@ function generateArticleMetadata(slug, config) {
1000
1230
  "article:modified_time": new Date(article.lastmod).toISOString()
1001
1231
  }), {
1002
1232
  "article:section": article.category,
1003
- "article:tag": (_f = (_e = article.tags) == null ? void 0 : _e.join(",")) != null ? _f : "",
1004
- "linkedin:owner": (_g = process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID) != null ? _g : ""
1233
+ "article:tag": (_e = (_d = article.tags) == null ? void 0 : _d.join(",")) != null ? _e : "",
1234
+ "linkedin:owner": (_f = process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID) != null ? _f : ""
1005
1235
  })
1006
1236
  };
1007
1237
  });
@@ -1093,6 +1323,50 @@ function generateCategoryMetadata(categorySlug, config) {
1093
1323
  };
1094
1324
  });
1095
1325
  }
1326
+ function generateSeriesMetadata(seriesSlug, config) {
1327
+ return __async(this, null, function* () {
1328
+ var _a;
1329
+ const articles = yield getArticlesBySeries(seriesSlug, config);
1330
+ if (articles.length === 0) return { title: "Series Not Found" };
1331
+ const seriesName = (_a = articles[0].series) != null ? _a : seriesSlug;
1332
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1333
+ const seriesUrl = `${siteUrl}/articles/series/${seriesSlug}`;
1334
+ const description = `Follow the ${seriesName} series - ${articles.length} article${articles.length === 1 ? "" : "s"} on ${config.siteName}.`;
1335
+ const title = `${seriesName} Series | ${config.siteName}`;
1336
+ return {
1337
+ title,
1338
+ description,
1339
+ openGraph: {
1340
+ title: `${seriesName} Series`,
1341
+ description,
1342
+ url: seriesUrl,
1343
+ siteName: config.siteName,
1344
+ images: [{ url: articles[0].featuredImage }],
1345
+ type: "website",
1346
+ locale: "en_US"
1347
+ },
1348
+ twitter: {
1349
+ card: "summary_large_image",
1350
+ title: `${seriesName} Series`,
1351
+ description
1352
+ },
1353
+ alternates: {
1354
+ canonical: seriesUrl
1355
+ },
1356
+ robots: {
1357
+ index: true,
1358
+ follow: true,
1359
+ googleBot: {
1360
+ index: true,
1361
+ follow: true,
1362
+ "max-video-preview": -1,
1363
+ "max-image-preview": "large",
1364
+ "max-snippet": -1
1365
+ }
1366
+ }
1367
+ };
1368
+ });
1369
+ }
1096
1370
  function generateAuthorMetadata(authorSlug, config) {
1097
1371
  return __async(this, null, function* () {
1098
1372
  var _a;
@@ -1127,6 +1401,47 @@ function generateAuthorMetadata(authorSlug, config) {
1127
1401
  };
1128
1402
  });
1129
1403
  }
1404
+ function withPaginationMeta(base, basePath, page, totalPages) {
1405
+ if (!base.alternates) return base;
1406
+ const { canonicalUrl } = buildPaginationLinks(basePath, page, totalPages);
1407
+ const pageSuffix = page > 1 ? ` - Page ${page}` : "";
1408
+ const title = typeof base.title === "string" ? `${base.title}${pageSuffix}` : base.title;
1409
+ const openGraph = base.openGraph ? __spreadProps(__spreadValues({}, base.openGraph), {
1410
+ title: typeof base.openGraph.title === "string" ? `${base.openGraph.title}${pageSuffix}` : base.openGraph.title,
1411
+ url: canonicalUrl
1412
+ }) : base.openGraph;
1413
+ const twitter = base.twitter ? __spreadProps(__spreadValues({}, base.twitter), {
1414
+ title: typeof base.twitter.title === "string" ? `${base.twitter.title}${pageSuffix}` : base.twitter.title
1415
+ }) : base.twitter;
1416
+ return __spreadProps(__spreadValues({}, base), {
1417
+ title,
1418
+ openGraph,
1419
+ twitter,
1420
+ alternates: __spreadProps(__spreadValues({}, base.alternates), { canonical: canonicalUrl })
1421
+ });
1422
+ }
1423
+ function generateArticlesIndexPageMetadata(page, totalPages, config) {
1424
+ const base = generateArticlesIndexMetadata(config);
1425
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1426
+ return withPaginationMeta(base, `${siteUrl}/articles`, page, totalPages);
1427
+ }
1428
+ function generateCategoryPageMetadata(categorySlug, page, totalPages, config) {
1429
+ return __async(this, null, function* () {
1430
+ const base = yield generateCategoryMetadata(categorySlug, config);
1431
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1432
+ return withPaginationMeta(base, `${siteUrl}/articles/category/${categorySlug}`, page, totalPages);
1433
+ });
1434
+ }
1435
+ function generateAuthorPageMetadata(authorSlug, page, totalPages, config) {
1436
+ return __async(this, null, function* () {
1437
+ var _a;
1438
+ const base = yield generateAuthorMetadata(authorSlug, config);
1439
+ const author = getAuthorBySlug(authorSlug, config);
1440
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
1441
+ const basePath = (_a = author == null ? void 0 : author.url) != null ? _a : `${siteUrl}/articles/authors/${authorSlug}`;
1442
+ return withPaginationMeta(base, basePath, page, totalPages);
1443
+ });
1444
+ }
1130
1445
  function formatCategoryName(category) {
1131
1446
  return category.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1132
1447
  }
@@ -1317,13 +1632,91 @@ function renderMdxSource(source, basePath, config) {
1317
1632
 
1318
1633
  // src/ArticleContent.tsx
1319
1634
  var import_jsx_runtime2 = require("react/jsx-runtime");
1635
+ function buildSlotContext(article) {
1636
+ var _a, _b;
1637
+ return {
1638
+ slug: article.slug,
1639
+ title: article.title,
1640
+ category: article.category,
1641
+ tags: (_a = article.tags) != null ? _a : [],
1642
+ readTime: article.readTime,
1643
+ wordCount: article.wordCount,
1644
+ authorSlug: article.authorSlug,
1645
+ seriesSlug: article.seriesSlug,
1646
+ primaryActionId: (_b = article.primaryAction) == null ? void 0 : _b.actionId
1647
+ };
1648
+ }
1649
+ function resolveSlot(slot, context) {
1650
+ if (slot === void 0) return null;
1651
+ return typeof slot === "function" ? slot(context) : slot;
1652
+ }
1653
+ function renderSegment(markdown, contentType, slug, config) {
1654
+ return __async(this, null, function* () {
1655
+ if (!markdown.trim()) return null;
1656
+ if (contentType === "mdx") {
1657
+ return renderMdxSource(markdown, `/articles/${slug}`, config);
1658
+ }
1659
+ const html = yield markdownToHtml(markdown, slug, config);
1660
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { dangerouslySetInnerHTML: { __html: html } });
1661
+ });
1662
+ }
1320
1663
  function ArticleContent(_0) {
1321
- return __async(this, arguments, function* ({ article, className, config }) {
1322
- if (article.contentType === "mdx" && article.mdxSource) {
1323
- const content = yield renderMdxSource(article.mdxSource, `/articles/${article.slug}`, config);
1324
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: content });
1664
+ return __async(this, arguments, function* ({
1665
+ article,
1666
+ className,
1667
+ config,
1668
+ afterHero,
1669
+ afterIntro,
1670
+ midContent,
1671
+ afterContent
1672
+ }) {
1673
+ var _a;
1674
+ const hasAnySlot = afterHero !== void 0 || afterIntro !== void 0 || midContent !== void 0 || afterContent !== void 0;
1675
+ if (!hasAnySlot) {
1676
+ if (article.contentType === "mdx" && article.mdxSource) {
1677
+ const content = yield renderMdxSource(article.mdxSource, `/articles/${article.slug}`, config);
1678
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: content });
1679
+ }
1680
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
1681
+ }
1682
+ const slotContext = buildSlotContext(article);
1683
+ const heroNode = resolveSlot(afterHero, slotContext);
1684
+ const introNode = resolveSlot(afterIntro, slotContext);
1685
+ const midNode = resolveSlot(midContent, slotContext);
1686
+ const contentNode = resolveSlot(afterContent, slotContext);
1687
+ const needsSplit = Boolean(introNode || midNode);
1688
+ const rawSource = article.contentType === "mdx" ? article.mdxSource : (_a = article.content) != null ? _a : void 0;
1689
+ if (needsSplit && rawSource) {
1690
+ const boundaries = getContentSlotBoundaries(rawSource);
1691
+ if (boundaries) {
1692
+ try {
1693
+ const introSegment = rawSource.slice(0, boundaries.introEnd);
1694
+ const midSegment = rawSource.slice(boundaries.introEnd, boundaries.mid);
1695
+ const restSegment = rawSource.slice(boundaries.mid);
1696
+ const [introHtml, midHtml, restHtml] = yield Promise.all([
1697
+ renderSegment(introSegment, article.contentType, article.slug, config),
1698
+ renderSegment(midSegment, article.contentType, article.slug, config),
1699
+ renderSegment(restSegment, article.contentType, article.slug, config)
1700
+ ]);
1701
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, children: [
1702
+ heroNode,
1703
+ introHtml,
1704
+ introNode,
1705
+ midHtml,
1706
+ midNode,
1707
+ restHtml,
1708
+ contentNode
1709
+ ] });
1710
+ } catch (e) {
1711
+ }
1712
+ }
1325
1713
  }
1326
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
1714
+ const wholeBody = article.contentType === "mdx" && article.mdxSource ? yield renderMdxSource(article.mdxSource, `/articles/${article.slug}`, config) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { dangerouslySetInnerHTML: { __html: article.htmlContent || "" } });
1715
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className, children: [
1716
+ heroNode,
1717
+ wholeBody,
1718
+ contentNode
1719
+ ] });
1327
1720
  });
1328
1721
  }
1329
1722
 
@@ -1350,6 +1743,227 @@ function ArticleTOC({ toc, className }) {
1350
1743
  }
1351
1744
  );
1352
1745
  }
1746
+
1747
+ // src/validateArticles.ts
1748
+ var UNSAFE_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
1749
+ var SEARCH_TITLE_MAX = 60;
1750
+ var SEARCH_DESCRIPTION_MAX = 160;
1751
+ var SOCIAL_TITLE_MAX = 95;
1752
+ var SOCIAL_DESCRIPTION_MAX = 200;
1753
+ function isUnsafeUrl(href) {
1754
+ return UNSAFE_URL_SCHEME.test(href);
1755
+ }
1756
+ function checkDuplicateCanonicalUrls(articles) {
1757
+ const seen = /* @__PURE__ */ new Map();
1758
+ const issues = [];
1759
+ for (const article of articles) {
1760
+ if (!article.canonicalUrl) continue;
1761
+ const owner = seen.get(article.canonicalUrl);
1762
+ if (owner) {
1763
+ issues.push({
1764
+ severity: "error",
1765
+ code: "duplicate-canonical-url",
1766
+ message: `canonicalUrl "${article.canonicalUrl}" is also used by "${owner}".`,
1767
+ articleSlug: article.slug
1768
+ });
1769
+ } else {
1770
+ seen.set(article.canonicalUrl, article.slug);
1771
+ }
1772
+ }
1773
+ return issues;
1774
+ }
1775
+ function checkAuthorReferences(articles, config) {
1776
+ if (!config.authors || Object.keys(config.authors).length === 0) return [];
1777
+ const issues = [];
1778
+ for (const article of articles) {
1779
+ for (const resolved of getArticleAuthors(article, config)) {
1780
+ if (!getAuthorBySlug(resolved.slug, config)) {
1781
+ issues.push({
1782
+ severity: "error",
1783
+ code: "unknown-author-reference",
1784
+ message: `Author "${resolved.name}" does not match any entry in config.authors.`,
1785
+ articleSlug: article.slug
1786
+ });
1787
+ }
1788
+ }
1789
+ }
1790
+ return issues;
1791
+ }
1792
+ function checkSeriesCollisions(articles) {
1793
+ const issues = [];
1794
+ const seenSlugOrder = /* @__PURE__ */ new Map();
1795
+ for (const article of articles) {
1796
+ if (!article.seriesSlug || article.seriesOrder === void 0) continue;
1797
+ const key = `${article.seriesSlug}::${article.seriesOrder}`;
1798
+ const owner = seenSlugOrder.get(key);
1799
+ if (owner) {
1800
+ issues.push({
1801
+ severity: "error",
1802
+ code: "duplicate-series-order",
1803
+ message: `seriesOrder ${article.seriesOrder} in series "${article.seriesSlug}" collides with "${owner}".`,
1804
+ articleSlug: article.slug
1805
+ });
1806
+ } else {
1807
+ seenSlugOrder.set(key, article.slug);
1808
+ }
1809
+ }
1810
+ return issues;
1811
+ }
1812
+ function checkPaths(articles, config) {
1813
+ var _a;
1814
+ const issues = [];
1815
+ const bySlug = new Map(articles.map((article) => [article.slug, article]));
1816
+ for (const [pathKey, path2] of Object.entries((_a = config.paths) != null ? _a : {})) {
1817
+ if (path2.articles.length === 0) {
1818
+ issues.push({
1819
+ severity: "error",
1820
+ code: "empty-path",
1821
+ message: `Path "${pathKey}" has no articles.`,
1822
+ pathKey
1823
+ });
1824
+ }
1825
+ for (const slug of path2.articles) {
1826
+ const referenced = bySlug.get(slug);
1827
+ if (!referenced) {
1828
+ issues.push({
1829
+ severity: "error",
1830
+ code: "path-missing-article",
1831
+ message: `Path "${pathKey}" references missing article "${slug}".`,
1832
+ pathKey,
1833
+ articleSlug: slug
1834
+ });
1835
+ } else if (referenced.draft) {
1836
+ issues.push({
1837
+ severity: "error",
1838
+ code: "path-references-draft",
1839
+ message: `Path "${pathKey}" references unpublished (draft) article "${slug}".`,
1840
+ pathKey,
1841
+ articleSlug: slug
1842
+ });
1843
+ }
1844
+ }
1845
+ if (isUnsafeUrl(path2.nextAction.href)) {
1846
+ issues.push({
1847
+ severity: "error",
1848
+ code: "unsafe-url",
1849
+ message: `Path "${pathKey}" nextAction.href uses an unsafe URL scheme.`,
1850
+ pathKey
1851
+ });
1852
+ }
1853
+ }
1854
+ return issues;
1855
+ }
1856
+ function checkAuthorCtaUrls(config) {
1857
+ var _a;
1858
+ const issues = [];
1859
+ for (const author of Object.values((_a = config.authors) != null ? _a : {})) {
1860
+ if (author.primaryCta && isUnsafeUrl(author.primaryCta.href)) {
1861
+ issues.push({
1862
+ severity: "error",
1863
+ code: "unsafe-url",
1864
+ message: `Author "${author.slug}" primaryCta.href uses an unsafe URL scheme.`
1865
+ });
1866
+ }
1867
+ }
1868
+ return issues;
1869
+ }
1870
+ function checkRequiredFrontmatter(articles) {
1871
+ const issues = [];
1872
+ for (const article of articles) {
1873
+ if (!article.excerpt) {
1874
+ issues.push({
1875
+ severity: "warning",
1876
+ code: "missing-excerpt",
1877
+ message: "Article has no excerpt.",
1878
+ articleSlug: article.slug
1879
+ });
1880
+ }
1881
+ if (!article.date) {
1882
+ issues.push({
1883
+ severity: "warning",
1884
+ code: "missing-date",
1885
+ message: "Article has no date.",
1886
+ articleSlug: article.slug
1887
+ });
1888
+ }
1889
+ }
1890
+ return issues;
1891
+ }
1892
+ function checkDiscoveryFieldLengths(articles) {
1893
+ const issues = [];
1894
+ for (const article of articles) {
1895
+ const checks = [
1896
+ [article.searchTitle, "search-title-too-long", SEARCH_TITLE_MAX],
1897
+ [article.searchDescription, "search-description-too-long", SEARCH_DESCRIPTION_MAX],
1898
+ [article.socialTitle, "social-title-too-long", SOCIAL_TITLE_MAX],
1899
+ [article.socialDescription, "social-description-too-long", SOCIAL_DESCRIPTION_MAX]
1900
+ ];
1901
+ for (const [value, code, max] of checks) {
1902
+ if (value && value.length > max) {
1903
+ issues.push({
1904
+ severity: "warning",
1905
+ code,
1906
+ message: `${code.replaceAll("-", " ")} (${value.length} > ${max} recommended chars).`,
1907
+ articleSlug: article.slug
1908
+ });
1909
+ }
1910
+ }
1911
+ }
1912
+ return issues;
1913
+ }
1914
+ function checkCategorySlugs(articles) {
1915
+ var _a;
1916
+ const issues = [];
1917
+ const slugToNames = /* @__PURE__ */ new Map();
1918
+ for (const article of articles) {
1919
+ for (const category of article.categories) {
1920
+ const slug = categoryToSlug(category);
1921
+ const names = (_a = slugToNames.get(slug)) != null ? _a : /* @__PURE__ */ new Set();
1922
+ names.add(category);
1923
+ slugToNames.set(slug, names);
1924
+ }
1925
+ }
1926
+ for (const [slug, names] of slugToNames) {
1927
+ if (names.size > 1) {
1928
+ issues.push({
1929
+ severity: "warning",
1930
+ code: "category-slug-collision",
1931
+ message: `Categories [${[...names].join(", ")}] all collapse to slug "${slug}".`
1932
+ });
1933
+ }
1934
+ }
1935
+ return issues;
1936
+ }
1937
+ function validateArticles(articles, config) {
1938
+ const errors = [
1939
+ ...checkDuplicateCanonicalUrls(articles),
1940
+ ...checkAuthorReferences(articles, config),
1941
+ ...checkSeriesCollisions(articles),
1942
+ ...checkPaths(articles, config),
1943
+ ...checkAuthorCtaUrls(config)
1944
+ ];
1945
+ const warnings = [
1946
+ ...checkRequiredFrontmatter(articles),
1947
+ ...checkDiscoveryFieldLengths(articles),
1948
+ ...checkCategorySlugs(articles)
1949
+ ];
1950
+ return { ok: errors.length === 0, errors, warnings };
1951
+ }
1952
+ function validateAllArticles(config) {
1953
+ return __async(this, null, function* () {
1954
+ const articles = yield getAllArticles(config);
1955
+ return validateArticles(articles, config);
1956
+ });
1957
+ }
1958
+
1959
+ // src/events.ts
1960
+ function emitArticleEvent(handler, event) {
1961
+ if (!handler) return;
1962
+ try {
1963
+ handler(__spreadProps(__spreadValues({}, event), { timestamp: Date.now() }));
1964
+ } catch (e) {
1965
+ }
1966
+ }
1353
1967
  // Annotate the CommonJS export names for ESM import in node:
1354
1968
  0 && (module.exports = {
1355
1969
  ArticleContent,
@@ -1357,17 +1971,27 @@ function ArticleTOC({ toc, className }) {
1357
1971
  buildArticleBreadcrumbs,
1358
1972
  buildAuthorBreadcrumbs,
1359
1973
  buildCategoryBreadcrumbs,
1974
+ buildPageUrl,
1975
+ buildPaginationLinks,
1360
1976
  categoryToSlug,
1977
+ emitArticleEvent,
1361
1978
  extractToc,
1362
1979
  generateArticleMetadata,
1363
1980
  generateArticleStaticParams,
1364
1981
  generateArticlesIndexMetadata,
1982
+ generateArticlesIndexPageMetadata,
1365
1983
  generateAuthorMetadata,
1984
+ generateAuthorPageMetadata,
1366
1985
  generateAuthorStaticParams,
1367
1986
  generateCategoryMetadata,
1987
+ generateCategoryPageMetadata,
1368
1988
  generateCategoryStaticParams,
1989
+ generateListingPageStaticParams,
1369
1990
  generateRssFeed,
1991
+ generateSeriesMetadata,
1992
+ generateSeriesStaticParams,
1370
1993
  getAdjacentArticles,
1994
+ getAdjacentArticlesInSeries,
1371
1995
  getAiRobotsTxtRules,
1372
1996
  getAllArticles,
1373
1997
  getAllAuthors,
@@ -1381,13 +2005,27 @@ function ArticleTOC({ toc, className }) {
1381
2005
  getArticleSitemapEntries,
1382
2006
  getArticlesByAuthor,
1383
2007
  getArticlesByCategory,
2008
+ getArticlesBySeries,
1384
2009
  getAuthorBySlug,
1385
2010
  getAvailableArticleSlugs,
1386
2011
  getBreadcrumbsConfig,
2012
+ getContentSlotBoundaries,
2013
+ getPath,
2014
+ getPathArticles,
2015
+ getRelatedArticlesByCategory,
2016
+ getRelatedContent,
2017
+ getTotalPages,
2018
+ isPageOutOfRange,
1387
2019
  markdownToHtml,
2020
+ paginateArticles,
2021
+ parsePageParam,
1388
2022
  resolveAuthorAvatar,
2023
+ resolveSearchMetadata,
2024
+ resolveSocialMetadata,
1389
2025
  sanitizeImagePath,
1390
2026
  searchArticles,
1391
- setArticlesErrorHandler
2027
+ setArticlesErrorHandler,
2028
+ validateAllArticles,
2029
+ validateArticles
1392
2030
  });
1393
2031
  //# sourceMappingURL=server.cjs.map