@next-md-blog/core 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 (134) hide show
  1. package/README.md +242 -0
  2. package/dist/components/BlogPostSEO.d.ts +28 -0
  3. package/dist/components/BlogPostSEO.d.ts.map +1 -0
  4. package/dist/components/BlogPostSEO.js +22 -0
  5. package/dist/components/MarkdownContent.d.ts +48 -0
  6. package/dist/components/MarkdownContent.d.ts.map +1 -0
  7. package/dist/components/MarkdownContent.js +44 -0
  8. package/dist/components/OgImage.d.ts +35 -0
  9. package/dist/components/OgImage.d.ts.map +1 -0
  10. package/dist/components/OgImage.js +52 -0
  11. package/dist/components/markdown/a.d.ts +6 -0
  12. package/dist/components/markdown/a.d.ts.map +1 -0
  13. package/dist/components/markdown/a.js +7 -0
  14. package/dist/components/markdown/blockquote.d.ts +6 -0
  15. package/dist/components/markdown/blockquote.d.ts.map +1 -0
  16. package/dist/components/markdown/blockquote.js +7 -0
  17. package/dist/components/markdown/code.d.ts +6 -0
  18. package/dist/components/markdown/code.d.ts.map +1 -0
  19. package/dist/components/markdown/code.js +7 -0
  20. package/dist/components/markdown/defaults.d.ts +12 -0
  21. package/dist/components/markdown/defaults.d.ts.map +1 -0
  22. package/dist/components/markdown/defaults.js +59 -0
  23. package/dist/components/markdown/em.d.ts +6 -0
  24. package/dist/components/markdown/em.d.ts.map +1 -0
  25. package/dist/components/markdown/em.js +7 -0
  26. package/dist/components/markdown/h1.d.ts +6 -0
  27. package/dist/components/markdown/h1.d.ts.map +1 -0
  28. package/dist/components/markdown/h1.js +7 -0
  29. package/dist/components/markdown/h2.d.ts +6 -0
  30. package/dist/components/markdown/h2.d.ts.map +1 -0
  31. package/dist/components/markdown/h2.js +7 -0
  32. package/dist/components/markdown/h3.d.ts +6 -0
  33. package/dist/components/markdown/h3.d.ts.map +1 -0
  34. package/dist/components/markdown/h3.js +7 -0
  35. package/dist/components/markdown/h4.d.ts +6 -0
  36. package/dist/components/markdown/h4.d.ts.map +1 -0
  37. package/dist/components/markdown/h4.js +7 -0
  38. package/dist/components/markdown/h5.d.ts +6 -0
  39. package/dist/components/markdown/h5.d.ts.map +1 -0
  40. package/dist/components/markdown/h5.js +7 -0
  41. package/dist/components/markdown/h6.d.ts +6 -0
  42. package/dist/components/markdown/h6.d.ts.map +1 -0
  43. package/dist/components/markdown/h6.js +7 -0
  44. package/dist/components/markdown/hr.d.ts +6 -0
  45. package/dist/components/markdown/hr.d.ts.map +1 -0
  46. package/dist/components/markdown/hr.js +7 -0
  47. package/dist/components/markdown/img.d.ts +6 -0
  48. package/dist/components/markdown/img.d.ts.map +1 -0
  49. package/dist/components/markdown/img.js +9 -0
  50. package/dist/components/markdown/index.d.ts +29 -0
  51. package/dist/components/markdown/index.d.ts.map +1 -0
  52. package/dist/components/markdown/index.js +28 -0
  53. package/dist/components/markdown/li.d.ts +6 -0
  54. package/dist/components/markdown/li.d.ts.map +1 -0
  55. package/dist/components/markdown/li.js +7 -0
  56. package/dist/components/markdown/ol.d.ts +6 -0
  57. package/dist/components/markdown/ol.d.ts.map +1 -0
  58. package/dist/components/markdown/ol.js +7 -0
  59. package/dist/components/markdown/p.d.ts +6 -0
  60. package/dist/components/markdown/p.d.ts.map +1 -0
  61. package/dist/components/markdown/p.js +7 -0
  62. package/dist/components/markdown/pre.d.ts +6 -0
  63. package/dist/components/markdown/pre.d.ts.map +1 -0
  64. package/dist/components/markdown/pre.js +7 -0
  65. package/dist/components/markdown/strong.d.ts +6 -0
  66. package/dist/components/markdown/strong.d.ts.map +1 -0
  67. package/dist/components/markdown/strong.js +7 -0
  68. package/dist/components/markdown/table.d.ts +6 -0
  69. package/dist/components/markdown/table.d.ts.map +1 -0
  70. package/dist/components/markdown/table.js +7 -0
  71. package/dist/components/markdown/tbody.d.ts +6 -0
  72. package/dist/components/markdown/tbody.d.ts.map +1 -0
  73. package/dist/components/markdown/tbody.js +7 -0
  74. package/dist/components/markdown/td.d.ts +6 -0
  75. package/dist/components/markdown/td.d.ts.map +1 -0
  76. package/dist/components/markdown/td.js +7 -0
  77. package/dist/components/markdown/th.d.ts +6 -0
  78. package/dist/components/markdown/th.d.ts.map +1 -0
  79. package/dist/components/markdown/th.js +7 -0
  80. package/dist/components/markdown/thead.d.ts +6 -0
  81. package/dist/components/markdown/thead.d.ts.map +1 -0
  82. package/dist/components/markdown/thead.js +7 -0
  83. package/dist/components/markdown/tr.d.ts +6 -0
  84. package/dist/components/markdown/tr.d.ts.map +1 -0
  85. package/dist/components/markdown/tr.js +7 -0
  86. package/dist/components/markdown/ul.d.ts +6 -0
  87. package/dist/components/markdown/ul.d.ts.map +1 -0
  88. package/dist/components/markdown/ul.js +7 -0
  89. package/dist/components/markdown/utils.d.ts +6 -0
  90. package/dist/components/markdown/utils.d.ts.map +1 -0
  91. package/dist/components/markdown/utils.js +7 -0
  92. package/dist/core/config.d.ts +36 -0
  93. package/dist/core/config.d.ts.map +1 -0
  94. package/dist/core/config.js +63 -0
  95. package/dist/core/constants.d.ts +36 -0
  96. package/dist/core/constants.d.ts.map +1 -0
  97. package/dist/core/constants.js +44 -0
  98. package/dist/core/errors.d.ts +48 -0
  99. package/dist/core/errors.d.ts.map +1 -0
  100. package/dist/core/errors.js +57 -0
  101. package/dist/core/file-utils.d.ts +22 -0
  102. package/dist/core/file-utils.d.ts.map +1 -0
  103. package/dist/core/file-utils.js +180 -0
  104. package/dist/core/seo-feeds.d.ts +16 -0
  105. package/dist/core/seo-feeds.d.ts.map +1 -0
  106. package/dist/core/seo-feeds.js +73 -0
  107. package/dist/core/seo-metadata.d.ts +17 -0
  108. package/dist/core/seo-metadata.d.ts.map +1 -0
  109. package/dist/core/seo-metadata.js +197 -0
  110. package/dist/core/seo-schema.d.ts +20 -0
  111. package/dist/core/seo-schema.d.ts.map +1 -0
  112. package/dist/core/seo-schema.js +131 -0
  113. package/dist/core/seo-utils.d.ts +66 -0
  114. package/dist/core/seo-utils.d.ts.map +1 -0
  115. package/dist/core/seo-utils.js +135 -0
  116. package/dist/core/seo.d.ts +11 -0
  117. package/dist/core/seo.d.ts.map +1 -0
  118. package/dist/core/seo.js +12 -0
  119. package/dist/core/type-guards.d.ts +58 -0
  120. package/dist/core/type-guards.d.ts.map +1 -0
  121. package/dist/core/type-guards.js +83 -0
  122. package/dist/core/types.d.ts +116 -0
  123. package/dist/core/types.d.ts.map +1 -0
  124. package/dist/core/types.js +1 -0
  125. package/dist/core/utils.d.ts +49 -0
  126. package/dist/core/utils.d.ts.map +1 -0
  127. package/dist/core/utils.js +175 -0
  128. package/dist/core/validation.d.ts +22 -0
  129. package/dist/core/validation.d.ts.map +1 -0
  130. package/dist/core/validation.js +50 -0
  131. package/dist/index.d.ts +15 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +17 -0
  134. package/package.json +80 -0
@@ -0,0 +1,197 @@
1
+ import { calculateReadingTime, calculateWordCount } from './utils.js';
2
+ import { getConfig } from './config.js';
3
+ import { resolveFrontmatterField, isStringArray } from './type-guards.js';
4
+ import { DEFAULT_SITE_NAME, DEFAULT_LANG } from './constants.js';
5
+ import { normalizeKeywords, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolvePostUrl, } from './seo-utils.js';
6
+ /**
7
+ * Generates comprehensive metadata for a blog post
8
+ * @param post - The blog post
9
+ * @param config - SEO configuration
10
+ * @returns Metadata object for Next.js
11
+ */
12
+ export function generateBlogPostMetadata(post, config) {
13
+ const blogConfig = config || getConfig();
14
+ const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, twitterHandle, defaultOgImage, defaultLang = DEFAULT_LANG, alternateLanguages, } = blogConfig;
15
+ const fm = post.frontmatter;
16
+ // Title resolution: seoTitle > title > slug
17
+ const baseTitle = resolveFrontmatterField(['title'], fm, post.slug) || post.slug;
18
+ const seoTitle = resolveFrontmatterField(['seoTitle', 'title'], fm, baseTitle) || baseTitle;
19
+ const pageTitle = `${seoTitle} | ${siteName}`;
20
+ // Description resolution: seoDescription > description > excerpt > empty
21
+ const description = resolveFrontmatterField(['seoDescription', 'description', 'excerpt'], fm, '') || '';
22
+ // Use normalized authors from post, or fallback to default author (resolved from config if available)
23
+ // Note: post.authors should already be resolved from config.authors when the post was loaded (in file-utils.ts)
24
+ // However, we ensure they are resolved here as a safety net in case resolution didn't happen earlier
25
+ const resolvedDefaultAuthor = resolveDefaultAuthor(defaultAuthor, configAuthors);
26
+ const postAuthors = (post.authors && post.authors.length > 0)
27
+ ? post.authors
28
+ : (resolvedDefaultAuthor ? [resolvedDefaultAuthor] : []);
29
+ // Ensure all authors are resolved from config (safety net)
30
+ const authors = ensureAuthorsResolved(postAuthors, configAuthors);
31
+ const authorNames = getAuthorNames(authors);
32
+ // Extract author Twitter handles for Twitter metadata
33
+ // Works for both frontmatter authors (already resolved) and default author (just resolved above)
34
+ const authorTwitterHandles = authors
35
+ .map((author) => {
36
+ if (typeof author === 'string')
37
+ return undefined;
38
+ return author.twitter;
39
+ })
40
+ .filter((handle) => Boolean(handle));
41
+ // Date resolution: publishedDate > date
42
+ const publishedDate = resolveFrontmatterField(['publishedDate', 'date'], fm);
43
+ const modifiedDate = resolveFrontmatterField(['modifiedDate'], fm);
44
+ // Tags and keywords
45
+ const tags = isStringArray(fm.tags) ? fm.tags : [];
46
+ const keywords = normalizeKeywords(fm.keywords);
47
+ const allKeywords = [...tags, ...keywords].filter((k, i, arr) => arr.indexOf(k) === i); // Remove duplicates
48
+ // Calculate reading time if not provided
49
+ const readingTime = resolveFrontmatterField(['readingTime'], fm) || calculateReadingTime(post.content);
50
+ const wordCount = calculateWordCount(post.content);
51
+ // OG image resolution: ogImage > image > defaultOgImage
52
+ // If not provided, Next.js will automatically use opengraph-image.tsx file convention
53
+ const ogImageUrl = resolveFrontmatterField(['ogImage', 'image'], fm) ||
54
+ defaultOgImage;
55
+ const imageAlt = resolveFrontmatterField(['imageAlt'], fm) || seoTitle;
56
+ // OG title/description: ogTitle/ogDescription > seoTitle/seoDescription > title/description
57
+ const ogTitle = resolveFrontmatterField(['ogTitle'], fm) || seoTitle;
58
+ const ogDescription = resolveFrontmatterField(['ogDescription'], fm) || description;
59
+ // Twitter title/description: twitterTitle/twitterDescription > ogTitle/ogDescription > seoTitle/description
60
+ const twitterTitle = resolveFrontmatterField(['twitterTitle'], fm) || ogTitle;
61
+ const twitterDescription = resolveFrontmatterField(['twitterDescription'], fm) || ogDescription;
62
+ // Canonical URL - supports both absolute and relative URLs
63
+ // Relative URLs will be resolved against siteUrl
64
+ const canonicalUrl = resolvePostUrl(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl);
65
+ // Language
66
+ const lang = resolveFrontmatterField(['lang'], fm) || defaultLang;
67
+ // Robots meta
68
+ const robots = buildRobotsMeta(fm);
69
+ // Post URL
70
+ const postUrl = `${siteUrl}/blog/${post.slug}`;
71
+ // Article meta tags (for better SEO)
72
+ const articleMeta = {};
73
+ if (publishedDate)
74
+ articleMeta['article:published_time'] = publishedDate;
75
+ if (modifiedDate)
76
+ articleMeta['article:modified_time'] = modifiedDate;
77
+ const category = resolveFrontmatterField(['category'], fm);
78
+ if (category)
79
+ articleMeta['article:section'] = category;
80
+ if (tags.length > 0) {
81
+ tags.forEach((tag, index) => {
82
+ articleMeta[`article:tag${index > 0 ? index + 1 : ''}`] = tag;
83
+ });
84
+ }
85
+ if (authorNames.length > 0) {
86
+ authorNames.forEach((authorName, index) => {
87
+ articleMeta[`article:author${index > 0 ? index + 1 : ''}`] = authorName;
88
+ });
89
+ }
90
+ // Add author email and other details from Author objects to meta tags
91
+ authors.forEach((author, index) => {
92
+ if (typeof author === 'object') {
93
+ // Add author email if available
94
+ if (author.email) {
95
+ articleMeta[`author:email${index > 0 ? index + 1 : ''}`] = author.email;
96
+ }
97
+ // Add author URL if available
98
+ if (author.url) {
99
+ articleMeta[`author:url${index > 0 ? index + 1 : ''}`] = author.url;
100
+ }
101
+ }
102
+ });
103
+ // Build alternates object
104
+ const alternates = {};
105
+ if (canonicalUrl)
106
+ alternates.canonical = canonicalUrl;
107
+ if (alternateLanguages && Object.keys(alternateLanguages).length > 0) {
108
+ alternates.languages = alternateLanguages;
109
+ }
110
+ // Merge all custom meta tags into the 'other' field
111
+ const otherMeta = {};
112
+ if (lang)
113
+ otherMeta['lang'] = lang;
114
+ Object.assign(otherMeta, articleMeta);
115
+ const metadata = {
116
+ title: pageTitle,
117
+ description,
118
+ ...(Object.keys(alternates).length > 0 && { alternates }),
119
+ ...(robots && { robots }),
120
+ ...(Object.keys(otherMeta).length > 0 && { other: otherMeta }),
121
+ openGraph: {
122
+ title: ogTitle,
123
+ description: ogDescription,
124
+ type: 'article',
125
+ url: postUrl,
126
+ siteName,
127
+ ...(ogImageUrl && {
128
+ images: [
129
+ {
130
+ url: ogImageUrl,
131
+ width: 1200,
132
+ height: 630,
133
+ alt: imageAlt,
134
+ },
135
+ ],
136
+ }),
137
+ ...(publishedDate && { publishedTime: publishedDate }),
138
+ ...(modifiedDate && { modifiedTime: modifiedDate }),
139
+ ...(authorNames.length > 0 && { authors: authorNames }),
140
+ ...(tags.length > 0 && { tags }),
141
+ ...(lang && { locale: lang }),
142
+ },
143
+ twitter: {
144
+ card: 'summary_large_image',
145
+ title: twitterTitle,
146
+ description: twitterDescription,
147
+ ...(ogImageUrl && { images: [ogImageUrl] }),
148
+ // Use author's Twitter handle if available, otherwise fallback to site Twitter handle
149
+ ...(authorTwitterHandles.length > 0
150
+ ? { creator: `@${authorTwitterHandles[0].replace('@', '')}` }
151
+ : twitterHandle && { creator: `@${twitterHandle.replace('@', '')}` }),
152
+ },
153
+ ...(allKeywords.length > 0 && { keywords: allKeywords }),
154
+ ...(authors.length > 0 && {
155
+ authors: authors.map((author) => {
156
+ if (typeof author === 'string') {
157
+ return { name: author };
158
+ }
159
+ return {
160
+ name: author.name,
161
+ ...(author.email && { email: author.email }),
162
+ ...(author.url && { url: author.url }),
163
+ };
164
+ })
165
+ }),
166
+ };
167
+ return metadata;
168
+ }
169
+ /**
170
+ * Generates metadata for the blog listing page
171
+ * @param posts - Array of blog posts
172
+ * @param config - SEO configuration
173
+ * @returns Metadata object for Next.js
174
+ */
175
+ export function generateBlogListMetadata(posts, config) {
176
+ const blogConfig = config || getConfig();
177
+ const { siteName = DEFAULT_SITE_NAME, siteUrl = '' } = blogConfig;
178
+ const title = 'Blog Posts';
179
+ const description = `Browse all ${posts.length} blog posts`;
180
+ const metadata = {
181
+ title: `${title} | ${siteName}`,
182
+ description,
183
+ openGraph: {
184
+ title,
185
+ description,
186
+ type: 'website',
187
+ url: `${siteUrl}/blogs`,
188
+ siteName,
189
+ },
190
+ twitter: {
191
+ card: 'summary',
192
+ title,
193
+ description,
194
+ },
195
+ };
196
+ return metadata;
197
+ }
@@ -0,0 +1,20 @@
1
+ import type { BlogPost, Config } from './types.js';
2
+ /**
3
+ * Generates JSON-LD structured data (Schema.org) for a blog post
4
+ * @param post - The blog post
5
+ * @param config - SEO configuration
6
+ * @returns JSON-LD schema object
7
+ */
8
+ export declare function generateBlogPostSchema(post: BlogPost, config?: Config): Record<string, unknown>;
9
+ /**
10
+ * Generates breadcrumbs schema for a blog post
11
+ * @param post - The blog post
12
+ * @param config - SEO configuration
13
+ * @param breadcrumbs - Optional custom breadcrumb items (e.g., [{ name: 'Home', url: '/' }, { name: 'Blog', url: '/blog' }])
14
+ * @returns Breadcrumbs JSON-LD schema object
15
+ */
16
+ export declare function generateBreadcrumbsSchema(post: BlogPost, config?: Config, breadcrumbs?: Array<{
17
+ name: string;
18
+ url: string;
19
+ }>): Record<string, unknown>;
20
+ //# sourceMappingURL=seo-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-schema.d.ts","sourceRoot":"","sources":["../../src/core/seo-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAU,MAAM,EAAE,MAAM,YAAY,CAAC;AAY3D;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA8GzB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GACjD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiCzB"}
@@ -0,0 +1,131 @@
1
+ import { calculateReadingTime, calculateWordCount } from './utils.js';
2
+ import { getConfig } from './config.js';
3
+ import { resolveFrontmatterField, isStringArray } from './type-guards.js';
4
+ import { DEFAULT_SITE_NAME } from './constants.js';
5
+ import { ensureAuthorsResolved, resolveDefaultAuthor, getAuthorNames, resolvePostUrl, } from './seo-utils.js';
6
+ /**
7
+ * Generates JSON-LD structured data (Schema.org) for a blog post
8
+ * @param post - The blog post
9
+ * @param config - SEO configuration
10
+ * @returns JSON-LD schema object
11
+ */
12
+ export function generateBlogPostSchema(post, config) {
13
+ const blogConfig = config || getConfig();
14
+ const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, } = blogConfig;
15
+ const fm = post.frontmatter;
16
+ const title = resolveFrontmatterField(['seoTitle', 'title'], fm, post.slug) || post.slug;
17
+ const description = resolveFrontmatterField(['seoDescription', 'description', 'excerpt'], fm, '') || '';
18
+ // Use normalized authors from post, or fallback to default author (resolved from config if available)
19
+ // Note: post.authors should already be resolved from config.authors when the post was loaded (in file-utils.ts)
20
+ // However, we ensure they are resolved here as a safety net in case resolution didn't happen earlier
21
+ const resolvedDefaultAuthor = resolveDefaultAuthor(defaultAuthor, configAuthors);
22
+ const postAuthors = (post.authors && post.authors.length > 0)
23
+ ? post.authors
24
+ : (resolvedDefaultAuthor ? [resolvedDefaultAuthor] : []);
25
+ // Ensure all authors are resolved from config (safety net)
26
+ const authors = ensureAuthorsResolved(postAuthors, configAuthors);
27
+ const authorNames = getAuthorNames(authors);
28
+ const publishedDate = resolveFrontmatterField(['publishedDate', 'date'], fm);
29
+ const modifiedDate = resolveFrontmatterField(['modifiedDate'], fm) || publishedDate;
30
+ const postUrl = resolvePostUrl(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl);
31
+ const ogImageUrl = resolveFrontmatterField(['ogImage', 'image'], fm);
32
+ // Calculate reading time and word count if not provided
33
+ const readingTime = resolveFrontmatterField(['readingTime'], fm) || calculateReadingTime(post.content);
34
+ const wordCount = calculateWordCount(post.content);
35
+ // Build author schema with full author info if available
36
+ const buildAuthorSchema = (author) => {
37
+ if (typeof author === 'string') {
38
+ return {
39
+ '@type': 'Person',
40
+ name: author,
41
+ };
42
+ }
43
+ return {
44
+ '@type': 'Person',
45
+ name: author.name,
46
+ ...(author.email && { email: author.email }),
47
+ ...(author.url && { url: author.url }),
48
+ ...(author.avatar && { image: author.avatar }),
49
+ };
50
+ };
51
+ // Base schema
52
+ const schema = {
53
+ '@context': 'https://schema.org',
54
+ '@type': resolveFrontmatterField(['type'], fm, 'BlogPosting') || 'BlogPosting',
55
+ headline: title,
56
+ description,
57
+ url: postUrl,
58
+ mainEntityOfPage: {
59
+ '@type': 'WebPage',
60
+ '@id': postUrl,
61
+ },
62
+ ...(publishedDate && { datePublished: publishedDate }),
63
+ ...(modifiedDate && { dateModified: modifiedDate }),
64
+ ...(authors.length > 0 && {
65
+ author: authors.length === 1
66
+ ? buildAuthorSchema(authors[0])
67
+ : authors.map(buildAuthorSchema),
68
+ }),
69
+ ...(siteName && {
70
+ publisher: {
71
+ '@type': 'Organization',
72
+ name: siteName,
73
+ ...(siteUrl && { url: siteUrl }),
74
+ },
75
+ }),
76
+ ...(ogImageUrl && {
77
+ image: {
78
+ '@type': 'ImageObject',
79
+ url: ogImageUrl,
80
+ ...(resolveFrontmatterField(['imageAlt'], fm) ? { caption: resolveFrontmatterField(['imageAlt'], fm) } : {}),
81
+ },
82
+ }),
83
+ ...(resolveFrontmatterField(['category'], fm) ? { articleSection: resolveFrontmatterField(['category'], fm) } : {}),
84
+ ...(isStringArray(fm.tags) && fm.tags.length > 0 && {
85
+ keywords: fm.tags.join(', '),
86
+ }),
87
+ ...(resolveFrontmatterField(['lang'], fm) ? { inLanguage: resolveFrontmatterField(['lang'], fm) } : {}),
88
+ ...(wordCount > 0 && { wordCount }),
89
+ ...(readingTime > 0 && {
90
+ timeRequired: `PT${readingTime}M`,
91
+ }),
92
+ };
93
+ // Merge with custom schema from frontmatter
94
+ if (fm.schema && typeof fm.schema === 'object') {
95
+ return {
96
+ ...schema,
97
+ ...fm.schema,
98
+ };
99
+ }
100
+ return schema;
101
+ }
102
+ /**
103
+ * Generates breadcrumbs schema for a blog post
104
+ * @param post - The blog post
105
+ * @param config - SEO configuration
106
+ * @param breadcrumbs - Optional custom breadcrumb items (e.g., [{ name: 'Home', url: '/' }, { name: 'Blog', url: '/blog' }])
107
+ * @returns Breadcrumbs JSON-LD schema object
108
+ */
109
+ export function generateBreadcrumbsSchema(post, config, breadcrumbs) {
110
+ const blogConfig = config || getConfig();
111
+ const { siteUrl = '' } = blogConfig;
112
+ const title = resolveFrontmatterField(['seoTitle', 'title'], post.frontmatter, post.slug) || post.slug;
113
+ const postUrl = resolvePostUrl(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl);
114
+ // Default breadcrumbs: Home > Blog > Post
115
+ const defaultBreadcrumbs = [
116
+ { name: 'Home', url: siteUrl || '/' },
117
+ { name: 'Blog', url: `${siteUrl}/blogs` },
118
+ { name: title, url: postUrl },
119
+ ];
120
+ const items = breadcrumbs || defaultBreadcrumbs;
121
+ return {
122
+ '@context': 'https://schema.org',
123
+ '@type': 'BreadcrumbList',
124
+ itemListElement: items.map((item, index) => ({
125
+ '@type': 'ListItem',
126
+ position: index + 1,
127
+ name: item.name,
128
+ item: item.url,
129
+ })),
130
+ };
131
+ }
@@ -0,0 +1,66 @@
1
+ import type { BlogPost, Author } from './types.js';
2
+ /**
3
+ * Normalizes keywords to an array of strings
4
+ * Handles both string (comma-separated) and array formats
5
+ * @param keywords - Keywords as string (comma-separated) or array of strings
6
+ * @returns Array of normalized keyword strings
7
+ */
8
+ export declare function normalizeKeywords(keywords?: string | string[]): string[];
9
+ /**
10
+ * Extracts author name from string or Author object
11
+ * @param author - Author as string or Author object
12
+ * @returns Author name as string
13
+ */
14
+ export declare function getAuthorName(author: string | Author): string;
15
+ /**
16
+ * Converts authors array (string | Author)[] to array of author names
17
+ * @param authors - Array of authors (strings or Author objects)
18
+ * @returns Array of author names as strings
19
+ */
20
+ export declare function getAuthorNames(authors: (string | Author)[]): string[];
21
+ /**
22
+ * Ensures all authors in the array are resolved from config
23
+ * This is a safety net to ensure authors are resolved even if they weren't resolved earlier
24
+ * @param authors - Array of authors (strings or Author objects)
25
+ * @param configAuthors - Array of authors from config
26
+ * @returns Array of resolved authors (Author objects or strings)
27
+ */
28
+ export declare function ensureAuthorsResolved(authors: (string | Author)[], configAuthors?: Author[]): (string | Author)[];
29
+ /**
30
+ * Resolves defaultAuthor from config.authors array if available
31
+ * @param defaultAuthor - Default author name from config
32
+ * @param configAuthors - Array of authors from config
33
+ * @returns Author object if found, otherwise returns the name as string
34
+ */
35
+ export declare function resolveDefaultAuthor(defaultAuthor: string | undefined, configAuthors?: Author[]): string | Author | undefined;
36
+ /**
37
+ * Builds robots meta string from frontmatter
38
+ * Combines robots directive string with boolean flags (noindex, nofollow)
39
+ * @param frontmatter - Blog post frontmatter containing robots directives
40
+ * @returns Robots meta string (e.g., 'noindex, nofollow') or undefined
41
+ */
42
+ export declare function buildRobotsMeta(frontmatter: BlogPost['frontmatter']): string | undefined;
43
+ /**
44
+ * Resolves a URL to an absolute URL
45
+ * If the URL is already absolute (starts with http:// or https://), returns it as-is
46
+ * If the URL is relative (starts with /), resolves it against siteUrl
47
+ * @param url - The URL to resolve (can be absolute or relative)
48
+ * @param siteUrl - The base site URL to resolve relative URLs against
49
+ * @returns Absolute URL
50
+ */
51
+ export declare function resolveCanonicalUrl(url: string, siteUrl: string): string;
52
+ /**
53
+ * Resolves a post URL from canonical URL or generates default
54
+ * @param canonicalUrl - Optional canonical URL from frontmatter
55
+ * @param slug - Post slug
56
+ * @param siteUrl - Base site URL
57
+ * @returns Resolved absolute URL
58
+ */
59
+ export declare function resolvePostUrl(canonicalUrl: string | undefined, slug: string, siteUrl: string): string;
60
+ /**
61
+ * Escapes XML special characters to prevent injection attacks
62
+ * @param unsafe - String that may contain XML special characters
63
+ * @returns Escaped string safe for XML output
64
+ */
65
+ export declare function escapeXml(unsafe: string): string;
66
+ //# sourceMappingURL=seo-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-utils.d.ts","sourceRoot":"","sources":["../../src/core/seo-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAInD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAOxE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,CAErE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAcrB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,GAAG,MAAM,GAAG,SAAS,CAI7B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,GAAG,SAAS,CAqBxF;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgBxE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOhD"}
@@ -0,0 +1,135 @@
1
+ import { resolveAuthorFromConfig } from './utils.js';
2
+ /**
3
+ * Normalizes keywords to an array of strings
4
+ * Handles both string (comma-separated) and array formats
5
+ * @param keywords - Keywords as string (comma-separated) or array of strings
6
+ * @returns Array of normalized keyword strings
7
+ */
8
+ export function normalizeKeywords(keywords) {
9
+ if (!keywords)
10
+ return [];
11
+ if (Array.isArray(keywords))
12
+ return keywords;
13
+ if (typeof keywords === 'string') {
14
+ return keywords.split(',').map((k) => k.trim()).filter(Boolean);
15
+ }
16
+ return [];
17
+ }
18
+ /**
19
+ * Extracts author name from string or Author object
20
+ * @param author - Author as string or Author object
21
+ * @returns Author name as string
22
+ */
23
+ export function getAuthorName(author) {
24
+ return typeof author === 'string' ? author : author.name;
25
+ }
26
+ /**
27
+ * Converts authors array (string | Author)[] to array of author names
28
+ * @param authors - Array of authors (strings or Author objects)
29
+ * @returns Array of author names as strings
30
+ */
31
+ export function getAuthorNames(authors) {
32
+ return authors.map(getAuthorName);
33
+ }
34
+ /**
35
+ * Ensures all authors in the array are resolved from config
36
+ * This is a safety net to ensure authors are resolved even if they weren't resolved earlier
37
+ * @param authors - Array of authors (strings or Author objects)
38
+ * @param configAuthors - Array of authors from config
39
+ * @returns Array of resolved authors (Author objects or strings)
40
+ */
41
+ export function ensureAuthorsResolved(authors, configAuthors) {
42
+ if (!configAuthors || configAuthors.length === 0) {
43
+ return authors;
44
+ }
45
+ return authors.map((author) => {
46
+ // If already an Author object, return as-is
47
+ if (typeof author === 'object') {
48
+ return author;
49
+ }
50
+ // If it's a string, try to resolve it from config
51
+ return resolveAuthorFromConfig(author, configAuthors);
52
+ });
53
+ }
54
+ /**
55
+ * Resolves defaultAuthor from config.authors array if available
56
+ * @param defaultAuthor - Default author name from config
57
+ * @param configAuthors - Array of authors from config
58
+ * @returns Author object if found, otherwise returns the name as string
59
+ */
60
+ export function resolveDefaultAuthor(defaultAuthor, configAuthors) {
61
+ if (!defaultAuthor)
62
+ return undefined;
63
+ return resolveAuthorFromConfig(defaultAuthor, configAuthors);
64
+ }
65
+ /**
66
+ * Builds robots meta string from frontmatter
67
+ * Combines robots directive string with boolean flags (noindex, nofollow)
68
+ * @param frontmatter - Blog post frontmatter containing robots directives
69
+ * @returns Robots meta string (e.g., 'noindex, nofollow') or undefined
70
+ */
71
+ export function buildRobotsMeta(frontmatter) {
72
+ const directives = [];
73
+ const robots = typeof frontmatter.robots === 'string' ? frontmatter.robots : undefined;
74
+ if (frontmatter.noindex || (robots && robots.includes('noindex'))) {
75
+ directives.push('noindex');
76
+ }
77
+ if (frontmatter.nofollow || (robots && robots.includes('nofollow'))) {
78
+ directives.push('nofollow');
79
+ }
80
+ // If explicit robots string is provided, use it (unless overridden by boolean flags)
81
+ if (robots && !frontmatter.noindex && !frontmatter.nofollow) {
82
+ return robots;
83
+ }
84
+ if (directives.length > 0) {
85
+ return directives.join(', ');
86
+ }
87
+ return undefined;
88
+ }
89
+ /**
90
+ * Resolves a URL to an absolute URL
91
+ * If the URL is already absolute (starts with http:// or https://), returns it as-is
92
+ * If the URL is relative (starts with /), resolves it against siteUrl
93
+ * @param url - The URL to resolve (can be absolute or relative)
94
+ * @param siteUrl - The base site URL to resolve relative URLs against
95
+ * @returns Absolute URL
96
+ */
97
+ export function resolveCanonicalUrl(url, siteUrl) {
98
+ // If already absolute, return as-is
99
+ if (url.startsWith('http://') || url.startsWith('https://')) {
100
+ return url;
101
+ }
102
+ // If relative, resolve against siteUrl
103
+ if (url.startsWith('/')) {
104
+ // Remove trailing slash from siteUrl if present
105
+ const baseUrl = siteUrl.replace(/\/$/, '');
106
+ return `${baseUrl}${url}`;
107
+ }
108
+ // If it's a relative path without leading slash, treat it as relative to root
109
+ const baseUrl = siteUrl.replace(/\/$/, '');
110
+ return `${baseUrl}/${url}`;
111
+ }
112
+ /**
113
+ * Resolves a post URL from canonical URL or generates default
114
+ * @param canonicalUrl - Optional canonical URL from frontmatter
115
+ * @param slug - Post slug
116
+ * @param siteUrl - Base site URL
117
+ * @returns Resolved absolute URL
118
+ */
119
+ export function resolvePostUrl(canonicalUrl, slug, siteUrl) {
120
+ const urlRaw = canonicalUrl || `${siteUrl}/blog/${slug}`;
121
+ return siteUrl ? resolveCanonicalUrl(urlRaw, siteUrl) : urlRaw;
122
+ }
123
+ /**
124
+ * Escapes XML special characters to prevent injection attacks
125
+ * @param unsafe - String that may contain XML special characters
126
+ * @returns Escaped string safe for XML output
127
+ */
128
+ export function escapeXml(unsafe) {
129
+ return unsafe
130
+ .replace(/&/g, '&amp;')
131
+ .replace(/</g, '&lt;')
132
+ .replace(/>/g, '&gt;')
133
+ .replace(/"/g, '&quot;')
134
+ .replace(/'/g, '&apos;');
135
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SEO utilities and generators for next-md-blog
3
+ *
4
+ * This module provides functions for generating metadata, schema.org structured data,
5
+ * and RSS/sitemap feeds for blog posts.
6
+ */
7
+ export { generateBlogPostMetadata, generateBlogListMetadata, } from './seo-metadata.js';
8
+ export { generateBlogPostSchema, generateBreadcrumbsSchema, } from './seo-schema.js';
9
+ export { generateSitemap, generateRSSFeed, } from './seo-feeds.js';
10
+ export { normalizeKeywords, getAuthorName, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolveCanonicalUrl, resolvePostUrl, escapeXml, } from './seo-utils.js';
11
+ //# sourceMappingURL=seo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/core/seo.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,eAAe,EACf,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,SAAS,GACV,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SEO utilities and generators for next-md-blog
3
+ *
4
+ * This module provides functions for generating metadata, schema.org structured data,
5
+ * and RSS/sitemap feeds for blog posts.
6
+ */
7
+ // Re-export all SEO functions from their respective modules
8
+ export { generateBlogPostMetadata, generateBlogListMetadata, } from './seo-metadata.js';
9
+ export { generateBlogPostSchema, generateBreadcrumbsSchema, } from './seo-schema.js';
10
+ export { generateSitemap, generateRSSFeed, } from './seo-feeds.js';
11
+ // Re-export utility functions that might be useful
12
+ export { normalizeKeywords, getAuthorName, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolveCanonicalUrl, resolvePostUrl, escapeXml, } from './seo-utils.js';
@@ -0,0 +1,58 @@
1
+ import type { BlogPostFrontmatter, Author } from './types.js';
2
+ /**
3
+ * Type guard to check if a value is a non-empty string
4
+ */
5
+ export declare function isString(value: unknown): value is string;
6
+ /**
7
+ * Type guard to check if a value is a string array
8
+ */
9
+ export declare function isStringArray(value: unknown): value is string[];
10
+ /**
11
+ * Type guard to check if a value is a number
12
+ */
13
+ export declare function isNumber(value: unknown): value is number;
14
+ /**
15
+ * Type guard to check if a value is an Author object
16
+ */
17
+ export declare function isAuthorObject(value: unknown): value is {
18
+ name: string;
19
+ [key: string]: unknown;
20
+ };
21
+ /**
22
+ * Type guard to check if a value is an Author
23
+ */
24
+ export declare function isAuthor(value: unknown): value is Author;
25
+ /**
26
+ * Gets a string field from frontmatter with fallback
27
+ * @param frontmatter - Blog post frontmatter object
28
+ * @param field - Field name to extract
29
+ * @param fallback - Optional fallback value if field is missing or invalid
30
+ * @returns String value or undefined
31
+ */
32
+ export declare function getStringField(frontmatter: BlogPostFrontmatter, field: keyof BlogPostFrontmatter, fallback?: string): string | undefined;
33
+ /**
34
+ * Gets a number field from frontmatter with fallback
35
+ * @param frontmatter - Blog post frontmatter object
36
+ * @param field - Field name to extract
37
+ * @param fallback - Optional fallback value if field is missing or invalid
38
+ * @returns Number value or undefined
39
+ */
40
+ export declare function getNumberField(frontmatter: BlogPostFrontmatter, field: keyof BlogPostFrontmatter, fallback?: number): number | undefined;
41
+ /**
42
+ * Resolves a frontmatter field from multiple possible field names
43
+ * Checks fields in order and returns the first valid value found
44
+ * @param fields - Array of field names to check (in priority order)
45
+ * @param frontmatter - Blog post frontmatter object
46
+ * @param fallback - Optional fallback value if no fields are found
47
+ * @returns First valid field value or fallback
48
+ * @example
49
+ * ```typescript
50
+ * const title = resolveFrontmatterField<string>(
51
+ * ['seoTitle', 'title'],
52
+ * frontmatter,
53
+ * 'Default Title'
54
+ * );
55
+ * ```
56
+ */
57
+ export declare function resolveFrontmatterField<T>(fields: Array<keyof BlogPostFrontmatter>, frontmatter: BlogPostFrontmatter, fallback?: T): T | undefined;
58
+ //# sourceMappingURL=type-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type-guards.d.ts","sourceRoot":"","sources":["../../src/core/type-guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAE9D;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,EAAE,CAE/D;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAOnD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAOxD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,mBAAmB,EAChC,KAAK,EAAE,MAAM,mBAAmB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,mBAAmB,EAChC,KAAK,EAAE,MAAM,mBAAmB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,KAAK,CAAC,MAAM,mBAAmB,CAAC,EACxC,WAAW,EAAE,mBAAmB,EAChC,QAAQ,CAAC,EAAE,CAAC,GACX,CAAC,GAAG,SAAS,CAQf"}