@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,83 @@
1
+ /**
2
+ * Type guard to check if a value is a non-empty string
3
+ */
4
+ export function isString(value) {
5
+ return typeof value === 'string' && value.length > 0;
6
+ }
7
+ /**
8
+ * Type guard to check if a value is a string array
9
+ */
10
+ export function isStringArray(value) {
11
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
12
+ }
13
+ /**
14
+ * Type guard to check if a value is a number
15
+ */
16
+ export function isNumber(value) {
17
+ return typeof value === 'number' && !isNaN(value);
18
+ }
19
+ /**
20
+ * Type guard to check if a value is an Author object
21
+ */
22
+ export function isAuthorObject(value) {
23
+ return (typeof value === 'object' &&
24
+ value !== null &&
25
+ 'name' in value &&
26
+ typeof value.name === 'string');
27
+ }
28
+ /**
29
+ * Type guard to check if a value is an Author
30
+ */
31
+ export function isAuthor(value) {
32
+ return (typeof value === 'object' &&
33
+ value !== null &&
34
+ 'name' in value &&
35
+ typeof value.name === 'string');
36
+ }
37
+ /**
38
+ * Gets a string field from frontmatter with fallback
39
+ * @param frontmatter - Blog post frontmatter object
40
+ * @param field - Field name to extract
41
+ * @param fallback - Optional fallback value if field is missing or invalid
42
+ * @returns String value or undefined
43
+ */
44
+ export function getStringField(frontmatter, field, fallback) {
45
+ const value = frontmatter[field];
46
+ return isString(value) ? value : fallback;
47
+ }
48
+ /**
49
+ * Gets a number field from frontmatter with fallback
50
+ * @param frontmatter - Blog post frontmatter object
51
+ * @param field - Field name to extract
52
+ * @param fallback - Optional fallback value if field is missing or invalid
53
+ * @returns Number value or undefined
54
+ */
55
+ export function getNumberField(frontmatter, field, fallback) {
56
+ const value = frontmatter[field];
57
+ return isNumber(value) ? value : fallback;
58
+ }
59
+ /**
60
+ * Resolves a frontmatter field from multiple possible field names
61
+ * Checks fields in order and returns the first valid value found
62
+ * @param fields - Array of field names to check (in priority order)
63
+ * @param frontmatter - Blog post frontmatter object
64
+ * @param fallback - Optional fallback value if no fields are found
65
+ * @returns First valid field value or fallback
66
+ * @example
67
+ * ```typescript
68
+ * const title = resolveFrontmatterField<string>(
69
+ * ['seoTitle', 'title'],
70
+ * frontmatter,
71
+ * 'Default Title'
72
+ * );
73
+ * ```
74
+ */
75
+ export function resolveFrontmatterField(fields, frontmatter, fallback) {
76
+ for (const field of fields) {
77
+ const value = frontmatter[field];
78
+ if (value !== undefined && value !== null && value !== '') {
79
+ return value;
80
+ }
81
+ }
82
+ return fallback;
83
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Blog configuration
3
+ */
4
+ export interface Config {
5
+ /** Site name */
6
+ siteName?: string;
7
+ /** Default site URL */
8
+ siteUrl?: string;
9
+ /** Default author name */
10
+ defaultAuthor?: string;
11
+ /** Array of author objects with detailed information */
12
+ authors?: Author[];
13
+ /** Twitter handle */
14
+ twitterHandle?: string;
15
+ /** Default OG image URL */
16
+ defaultOgImage?: string;
17
+ /** Default language code */
18
+ defaultLang?: string;
19
+ /** Alternate language URLs for hreflang (e.g., { 'en': 'https://example.com/blog/post', 'fr': 'https://example.com/fr/blog/post' }) */
20
+ alternateLanguages?: Record<string, string>;
21
+ }
22
+ /**
23
+ * Author information
24
+ */
25
+ export interface Author {
26
+ name: string;
27
+ email?: string;
28
+ bio?: string;
29
+ avatar?: string;
30
+ twitter?: string;
31
+ github?: string;
32
+ url?: string;
33
+ }
34
+ /**
35
+ * Frontmatter metadata for blog posts
36
+ */
37
+ export interface BlogPostFrontmatter {
38
+ title?: string;
39
+ date?: string;
40
+ description?: string;
41
+ /** Author can be a string, an object with name property (Quarto format), or an array of either */
42
+ author?: string | {
43
+ name: string;
44
+ [key: string]: unknown;
45
+ } | (string | {
46
+ name: string;
47
+ [key: string]: unknown;
48
+ })[];
49
+ /** Authors can be an array of strings or objects with name property */
50
+ authors?: (string | {
51
+ name: string;
52
+ [key: string]: unknown;
53
+ })[];
54
+ tags?: string[];
55
+ /** Custom OG image URL */
56
+ ogImage?: string;
57
+ /** Featured image URL (fallback for OG image) */
58
+ image?: string;
59
+ /** Robots meta directive (e.g., 'noindex, nofollow' or 'index, follow') */
60
+ robots?: string;
61
+ /** Whether to exclude from search engine indexing */
62
+ noindex?: boolean;
63
+ /** Whether to exclude from following links */
64
+ nofollow?: boolean;
65
+ /** Reading time in minutes (auto-calculated if not provided) */
66
+ readingTime?: number;
67
+ [key: string]: unknown;
68
+ }
69
+ /**
70
+ * Complete blog post with content and metadata
71
+ */
72
+ export interface BlogPost {
73
+ /** The slug identifier for the post (filename without extension) */
74
+ slug: string;
75
+ /** The markdown content of the post */
76
+ content: string;
77
+ /** Frontmatter metadata parsed from the markdown file */
78
+ frontmatter: BlogPostFrontmatter;
79
+ /** Reading time in minutes (auto-calculated) */
80
+ readingTime: number;
81
+ /** Word count (auto-calculated) */
82
+ wordCount: number;
83
+ /** Normalized authors array (string or Author objects) */
84
+ authors: (string | Author)[];
85
+ }
86
+ /**
87
+ * Blog post metadata without content (for listings)
88
+ */
89
+ export interface BlogPostMetadata {
90
+ /** The slug identifier for the post */
91
+ slug: string;
92
+ /** Frontmatter metadata parsed from the markdown file */
93
+ frontmatter: BlogPostFrontmatter;
94
+ /** Normalized authors array (string or Author objects) */
95
+ authors: (string | Author)[];
96
+ }
97
+ /**
98
+ * Options for reading blog posts
99
+ */
100
+ export interface GetBlogPostOptions {
101
+ /** Custom path to posts directory */
102
+ postsDir?: string;
103
+ /** Locale code for multi-language support (e.g., 'en', 'fr') */
104
+ locale?: string;
105
+ /** Blog configuration for author resolution */
106
+ config?: Config;
107
+ }
108
+ /**
109
+ * Result of file system operations
110
+ */
111
+ export interface FileSystemResult<T> {
112
+ success: boolean;
113
+ data?: T;
114
+ error?: Error;
115
+ }
116
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import type { Author } from './types.js';
2
+ /**
3
+ * Calculates reading time in minutes from markdown content
4
+ * Assumes average reading speed of 200 words per minute
5
+ * @param content - The markdown content
6
+ * @returns Reading time in minutes (rounded up)
7
+ */
8
+ export declare function calculateReadingTime(content: string): number;
9
+ /**
10
+ * Calculates word count from markdown content
11
+ * @param content - The markdown content
12
+ * @returns Word count
13
+ */
14
+ export declare function calculateWordCount(content: string): number;
15
+ /**
16
+ * Resolves author names to full Author objects from config
17
+ * Matches by full name only (case-insensitive)
18
+ * @param authorName - Author name to resolve
19
+ * @param configAuthors - Array of authors from config
20
+ * @returns Author object if found, otherwise returns the name as string
21
+ */
22
+ export declare function resolveAuthorFromConfig(authorName: string, configAuthors?: Author[]): string | Author;
23
+ /**
24
+ * Normalizes authors to an array of strings or Author objects
25
+ * Supports:
26
+ * - author: "John Doe" (string)
27
+ * - author: { name: "John Doe", affiliation: "..." } (single object with name property - Quarto format)
28
+ * - author: ["John Doe", "Jane Smith"] (array of strings)
29
+ * - author: [{ name: "John Doe" }, { name: "Jane Smith" }] (array of objects with name)
30
+ * - authors: ["John Doe", "Jane Smith"] (array of strings)
31
+ * - authors: [{ name: "John Doe" }, { name: "Jane Smith" }] (array of objects with name)
32
+ * If configAuthors is provided, resolves author names to full Author objects by matching full name
33
+ * Falls back to string names if authors array is not configured (backward compatible)
34
+ * @param author - Author field from frontmatter (string, object with name, string[], or array of objects with name)
35
+ * @param authors - Authors field from frontmatter (string[] or array of objects with name)
36
+ * @param configAuthors - Optional array of authors from config to resolve against
37
+ * @returns Normalized array of author names or Author objects
38
+ */
39
+ export declare function normalizeAuthors(author?: string | {
40
+ name: string;
41
+ [key: string]: unknown;
42
+ } | (string | {
43
+ name: string;
44
+ [key: string]: unknown;
45
+ })[], authors?: (string | {
46
+ name: string;
47
+ [key: string]: unknown;
48
+ })[], configAuthors?: Author[]): (string | Author)[];
49
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AA+DzC;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqB5D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAU1D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,GAAG,MAAM,CAYjB;AAsBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,CAAC,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC,EAAE,EAClH,OAAO,CAAC,EAAE,CAAC,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC,EAAE,EAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CA0CrB"}
@@ -0,0 +1,175 @@
1
+ // Regex patterns for markdown stripping (compiled once at module level)
2
+ const CODE_BLOCK_REGEX = /```[\s\S]*?```/g;
3
+ const INLINE_CODE_REGEX = /`[^`]*`/g;
4
+ const LINK_REGEX = /\[([^\]]*)\]\([^\)]*\)/g;
5
+ const IMAGE_REGEX = /!\[([^\]]*)\]\([^\)]*\)/g;
6
+ const HTML_TAG_REGEX = /<[^>]*>/g;
7
+ const HEADER_REGEX = /#+\s+/g;
8
+ const HORIZONTAL_RULE_REGEX = /---+/g;
9
+ const LIST_MARKER_REGEX = /^[\s]*[-*+]\s+/gm;
10
+ const BLOCKQUOTE_REGEX = /^>\s+/gm;
11
+ const WHITESPACE_REGEX = /\s+/g;
12
+ // Constants
13
+ const WORDS_PER_MINUTE = 200;
14
+ const MIN_READING_TIME = 1;
15
+ /**
16
+ * Strips markdown syntax from content to get plain text
17
+ * @param content - The markdown content
18
+ * @param options - Options for what to strip
19
+ * @returns Plain text with markdown syntax removed
20
+ */
21
+ function stripMarkdownSyntax(content, options = {}) {
22
+ let plainText = content
23
+ // Remove code blocks
24
+ .replace(CODE_BLOCK_REGEX, '')
25
+ // Remove inline code
26
+ .replace(INLINE_CODE_REGEX, '')
27
+ // Remove links but keep text
28
+ .replace(LINK_REGEX, '$1')
29
+ // Remove images
30
+ .replace(IMAGE_REGEX, '')
31
+ // Remove HTML tags
32
+ .replace(HTML_TAG_REGEX, '');
33
+ // Conditionally remove additional markdown elements
34
+ if (options.includeHeaders) {
35
+ plainText = plainText.replace(HEADER_REGEX, '');
36
+ }
37
+ if (options.includeHorizontalRules) {
38
+ plainText = plainText.replace(HORIZONTAL_RULE_REGEX, '');
39
+ }
40
+ if (options.includeLists) {
41
+ plainText = plainText.replace(LIST_MARKER_REGEX, '');
42
+ }
43
+ if (options.includeBlockquotes) {
44
+ plainText = plainText.replace(BLOCKQUOTE_REGEX, '');
45
+ }
46
+ // Remove extra whitespace
47
+ return plainText.replace(WHITESPACE_REGEX, ' ').trim();
48
+ }
49
+ /**
50
+ * Calculates reading time in minutes from markdown content
51
+ * Assumes average reading speed of 200 words per minute
52
+ * @param content - The markdown content
53
+ * @returns Reading time in minutes (rounded up)
54
+ */
55
+ export function calculateReadingTime(content) {
56
+ if (!content || typeof content !== 'string') {
57
+ return 0;
58
+ }
59
+ // Strip all markdown syntax including headers, lists, blockquotes, etc.
60
+ const plainText = stripMarkdownSyntax(content, {
61
+ includeHeaders: true,
62
+ includeLists: true,
63
+ includeBlockquotes: true,
64
+ includeHorizontalRules: true,
65
+ });
66
+ // Count words (split by whitespace)
67
+ const wordCount = plainText.split(/\s+/).filter((word) => word.length > 0).length;
68
+ // Calculate reading time
69
+ const readingTime = Math.ceil(wordCount / WORDS_PER_MINUTE);
70
+ // Minimum reading time is 1 minute
71
+ return Math.max(MIN_READING_TIME, readingTime);
72
+ }
73
+ /**
74
+ * Calculates word count from markdown content
75
+ * @param content - The markdown content
76
+ * @returns Word count
77
+ */
78
+ export function calculateWordCount(content) {
79
+ if (!content || typeof content !== 'string') {
80
+ return 0;
81
+ }
82
+ // Strip basic markdown syntax (no headers, lists, etc. for word count)
83
+ const plainText = stripMarkdownSyntax(content);
84
+ // Count words (split by whitespace)
85
+ return plainText.split(/\s+/).filter((word) => word.length > 0).length;
86
+ }
87
+ /**
88
+ * Resolves author names to full Author objects from config
89
+ * Matches by full name only (case-insensitive)
90
+ * @param authorName - Author name to resolve
91
+ * @param configAuthors - Array of authors from config
92
+ * @returns Author object if found, otherwise returns the name as string
93
+ */
94
+ export function resolveAuthorFromConfig(authorName, configAuthors) {
95
+ // If no config authors, return name as-is (backward compatibility)
96
+ if (!configAuthors || configAuthors.length === 0) {
97
+ return authorName;
98
+ }
99
+ // Match by full name (case-insensitive)
100
+ const found = configAuthors.find((a) => a.name.toLowerCase() === authorName.toLowerCase());
101
+ return found || authorName;
102
+ }
103
+ /**
104
+ * Extracts author name from various formats (string, object with name property, etc.)
105
+ * @param item - Author item that could be a string or object
106
+ * @returns Author name as string, or undefined if invalid
107
+ */
108
+ function extractAuthorName(item) {
109
+ if (typeof item === 'string') {
110
+ return item.trim() || undefined;
111
+ }
112
+ if (typeof item === 'object' && item !== null) {
113
+ // Handle objects with name property: { name: "John Doe" }
114
+ if ('name' in item && typeof item.name === 'string') {
115
+ return item.name.trim() || undefined;
116
+ }
117
+ }
118
+ return undefined;
119
+ }
120
+ /**
121
+ * Normalizes authors to an array of strings or Author objects
122
+ * Supports:
123
+ * - author: "John Doe" (string)
124
+ * - author: { name: "John Doe", affiliation: "..." } (single object with name property - Quarto format)
125
+ * - author: ["John Doe", "Jane Smith"] (array of strings)
126
+ * - author: [{ name: "John Doe" }, { name: "Jane Smith" }] (array of objects with name)
127
+ * - authors: ["John Doe", "Jane Smith"] (array of strings)
128
+ * - authors: [{ name: "John Doe" }, { name: "Jane Smith" }] (array of objects with name)
129
+ * If configAuthors is provided, resolves author names to full Author objects by matching full name
130
+ * Falls back to string names if authors array is not configured (backward compatible)
131
+ * @param author - Author field from frontmatter (string, object with name, string[], or array of objects with name)
132
+ * @param authors - Authors field from frontmatter (string[] or array of objects with name)
133
+ * @param configAuthors - Optional array of authors from config to resolve against
134
+ * @returns Normalized array of author names or Author objects
135
+ */
136
+ export function normalizeAuthors(author, authors, configAuthors) {
137
+ const authorList = [];
138
+ // First, collect from authors field
139
+ if (Array.isArray(authors) && authors.length > 0) {
140
+ authors.forEach((a) => {
141
+ const name = extractAuthorName(a);
142
+ if (name) {
143
+ authorList.push(name);
144
+ }
145
+ });
146
+ }
147
+ // Then, collect from author field
148
+ if (author) {
149
+ if (Array.isArray(author)) {
150
+ author.forEach((a) => {
151
+ const name = extractAuthorName(a);
152
+ if (name) {
153
+ authorList.push(name);
154
+ }
155
+ });
156
+ }
157
+ else if (typeof author === 'string' && author.trim().length > 0) {
158
+ authorList.push(author.trim());
159
+ }
160
+ else if (typeof author === 'object' && author !== null && 'name' in author) {
161
+ // Handle single object: { name: "John Doe" }
162
+ const name = extractAuthorName(author);
163
+ if (name) {
164
+ authorList.push(name);
165
+ }
166
+ }
167
+ }
168
+ // Remove duplicates while preserving order
169
+ const uniqueAuthors = authorList.filter((a, i, arr) => arr.indexOf(a) === i);
170
+ // Resolve authors from config if available
171
+ if (configAuthors && configAuthors.length > 0) {
172
+ return uniqueAuthors.map((name) => resolveAuthorFromConfig(name, configAuthors));
173
+ }
174
+ return uniqueAuthors;
175
+ }
@@ -0,0 +1,22 @@
1
+ import type { BlogPostFrontmatter } from './types';
2
+ /**
3
+ * Validates that a slug is safe and valid
4
+ * @param slug - The slug to validate
5
+ * @returns True if valid, throws error if invalid
6
+ * @throws {Error} If slug is invalid
7
+ */
8
+ export declare function validateSlug(slug: string): boolean;
9
+ /**
10
+ * Validates frontmatter data
11
+ * @param frontmatter - The frontmatter to validate
12
+ * @returns Validated frontmatter
13
+ */
14
+ export declare function validateFrontmatter(frontmatter: unknown): BlogPostFrontmatter;
15
+ /**
16
+ * Validates markdown content
17
+ * @param content - The content to validate
18
+ * @returns True if valid
19
+ * @throws {Error} If content is invalid
20
+ */
21
+ export declare function validateContent(content: string): boolean;
22
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/core/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAoBlD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,OAAO,GAAG,mBAAmB,CAW7E;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAKxD"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Validates that a slug is safe and valid
3
+ * @param slug - The slug to validate
4
+ * @returns True if valid, throws error if invalid
5
+ * @throws {Error} If slug is invalid
6
+ */
7
+ export function validateSlug(slug) {
8
+ if (!slug || typeof slug !== 'string') {
9
+ throw new Error('Slug must be a non-empty string');
10
+ }
11
+ if (slug.trim().length === 0) {
12
+ throw new Error('Slug cannot be empty or whitespace');
13
+ }
14
+ // Prevent directory traversal and invalid characters
15
+ if (slug.includes('..') || slug.includes('/') || slug.includes('\\')) {
16
+ throw new Error('Slug cannot contain path separators or directory traversal sequences');
17
+ }
18
+ // Prevent null bytes
19
+ if (slug.includes('\0')) {
20
+ throw new Error('Slug cannot contain null bytes');
21
+ }
22
+ return true;
23
+ }
24
+ /**
25
+ * Validates frontmatter data
26
+ * @param frontmatter - The frontmatter to validate
27
+ * @returns Validated frontmatter
28
+ */
29
+ export function validateFrontmatter(frontmatter) {
30
+ if (!frontmatter || typeof frontmatter !== 'object') {
31
+ return {};
32
+ }
33
+ // Ensure it's a plain object
34
+ if (Array.isArray(frontmatter)) {
35
+ return {};
36
+ }
37
+ return frontmatter;
38
+ }
39
+ /**
40
+ * Validates markdown content
41
+ * @param content - The content to validate
42
+ * @returns True if valid
43
+ * @throws {Error} If content is invalid
44
+ */
45
+ export function validateContent(content) {
46
+ if (typeof content !== 'string') {
47
+ throw new Error('Content must be a string');
48
+ }
49
+ return true;
50
+ }
@@ -0,0 +1,15 @@
1
+ export { MarkdownContent } from './components/MarkdownContent.js';
2
+ export type { MarkdownContentProps, MarkdownComponents } from './components/MarkdownContent.js';
3
+ export { defaultMarkdownComponents } from './components/markdown/defaults.js';
4
+ export { OgImage } from './components/OgImage.js';
5
+ export type { OgImageProps } from './components/OgImage.js';
6
+ export { BlogPostSEO } from './components/BlogPostSEO.js';
7
+ export type { BlogPostSEOProps } from './components/BlogPostSEO.js';
8
+ export { getBlogPost, getAllBlogPosts, getAllBlogPostSlugs } from './core/file-utils.js';
9
+ export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema, generateSitemap, generateRSSFeed, } from './core/seo.js';
10
+ export { createConfig, loadConfig, getConfig } from './core/config.js';
11
+ export type { BlogPost, BlogPostMetadata, BlogPostFrontmatter, GetBlogPostOptions, Author, Config, } from './core/types.js';
12
+ export { MdxBlogError, BlogPostNotFoundError, FileReadError, DirectoryError, } from './core/errors.js';
13
+ export { POSTS_DIR_NAME, getPostsDirectory } from './core/constants.js';
14
+ export { calculateReadingTime, calculateWordCount, normalizeAuthors } from './core/utils.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,eAAe,GAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGvE,YAAY,EACV,QAAQ,EACR,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,MAAM,EACN,MAAM,GACP,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxE,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ // Components
2
+ export { MarkdownContent } from './components/MarkdownContent.js';
3
+ export { defaultMarkdownComponents } from './components/markdown/defaults.js';
4
+ export { OgImage } from './components/OgImage.js';
5
+ export { BlogPostSEO } from './components/BlogPostSEO.js';
6
+ // Utilities
7
+ export { getBlogPost, getAllBlogPosts, getAllBlogPostSlugs } from './core/file-utils.js';
8
+ // SEO
9
+ export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema, generateSitemap, generateRSSFeed, } from './core/seo.js';
10
+ // Config
11
+ export { createConfig, loadConfig, getConfig } from './core/config.js';
12
+ // Errors
13
+ export { MdxBlogError, BlogPostNotFoundError, FileReadError, DirectoryError, } from './core/errors.js';
14
+ // Constants
15
+ export { POSTS_DIR_NAME, getPostsDirectory } from './core/constants.js';
16
+ // Utilities
17
+ export { calculateReadingTime, calculateWordCount, normalizeAuthors } from './core/utils.js';
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@next-md-blog/core",
3
+ "version": "1.0.0",
4
+ "description": "A React library for parsing and displaying markdown blog posts in Next.js",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "module": "dist/index.js",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsc --watch",
23
+ "prepare": "npm run build",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "prepublishOnly": "npm run build && npm run test"
28
+ },
29
+ "keywords": [
30
+ "react",
31
+ "nextjs",
32
+ "markdown",
33
+ "blog",
34
+ "mdx",
35
+ "content"
36
+ ],
37
+ "author": "florianamette",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/florianamette/next-mdx-blog.git",
42
+ "directory": "packages/core"
43
+ },
44
+ "homepage": "https://github.com/florianamette/next-mdx-blog#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/florianamette/next-mdx-blog/issues"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "dependencies": {
52
+ "@tailwindcss/typography": "^0.5.19",
53
+ "c12": "^2.0.4",
54
+ "gray-matter": "^4.0.3",
55
+ "react-markdown": "^10.1.0",
56
+ "rehype-react": "^7.0.0",
57
+ "remark": "^15.0.1",
58
+ "remark-emoji": "^5.0.2",
59
+ "remark-gfm": "^4.0.0",
60
+ "remark-rehype": "^11.1.1",
61
+ "tailwindcss": "^4.0.0",
62
+ "unist-util-visit": "^5.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@semantic-release/changelog": "^6.0.3",
66
+ "@semantic-release/git": "^10.0.1",
67
+ "@types/node": "^20.0.0",
68
+ "@types/react": "^18.0.0",
69
+ "@types/react-dom": "^18.0.0",
70
+ "@vitest/coverage-v8": "^4.0.8",
71
+ "semantic-release": "^23.0.0",
72
+ "typescript": "^5.0.0",
73
+ "vitest": "^4.0.8"
74
+ },
75
+ "peerDependencies": {
76
+ "next": "^16.0.1",
77
+ "react": "^19.2.0",
78
+ "react-dom": "^19.2.0"
79
+ }
80
+ }