@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,36 @@
1
+ /**
2
+ * Default directory name for blog posts
3
+ */
4
+ export declare const POSTS_DIR_NAME = "posts";
5
+ /**
6
+ * Markdown file extension
7
+ */
8
+ export declare const MARKDOWN_EXTENSION = ".md";
9
+ /**
10
+ * MDX file extension
11
+ */
12
+ export declare const MDX_EXTENSION = ".mdx";
13
+ /**
14
+ * Array of supported file extensions
15
+ */
16
+ export declare const SUPPORTED_EXTENSIONS: string[];
17
+ /**
18
+ * Regular expression to match markdown and MDX files
19
+ */
20
+ export declare const MARKDOWN_FILE_REGEX: RegExp;
21
+ /**
22
+ * Default configuration values
23
+ */
24
+ export declare const DEFAULT_SITE_NAME = "My Blog";
25
+ export declare const DEFAULT_BLOG_ROUTE = "blog";
26
+ export declare const DEFAULT_BLOGS_ROUTE = "blogs";
27
+ export declare const DEFAULT_LANG = "en";
28
+ export declare const RSS_POST_LIMIT = 20;
29
+ /**
30
+ * Gets the posts directory path relative to the current working directory
31
+ * @param customPath - Optional custom path to posts directory
32
+ * @param locale - Optional locale code. If provided, appends /{locale} to the posts directory path
33
+ * @returns The full path to the posts directory
34
+ */
35
+ export declare function getPostsDirectory(customPath?: string, locale?: string): string;
36
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/core/constants.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC;;GAEG;AACH,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,aAAa,SAAS,CAAC;AAEpC;;GAEG;AACH,eAAO,MAAM,oBAAoB,UAAsC,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAAgB,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAC3C,eAAO,MAAM,kBAAkB,SAAS,CAAC;AACzC,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,eAAO,MAAM,YAAY,OAAO,CAAC;AACjC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAU9E"}
@@ -0,0 +1,44 @@
1
+ import path from 'path';
2
+ /**
3
+ * Default directory name for blog posts
4
+ */
5
+ export const POSTS_DIR_NAME = 'posts';
6
+ /**
7
+ * Markdown file extension
8
+ */
9
+ export const MARKDOWN_EXTENSION = '.md';
10
+ /**
11
+ * MDX file extension
12
+ */
13
+ export const MDX_EXTENSION = '.mdx';
14
+ /**
15
+ * Array of supported file extensions
16
+ */
17
+ export const SUPPORTED_EXTENSIONS = [MARKDOWN_EXTENSION, MDX_EXTENSION];
18
+ /**
19
+ * Regular expression to match markdown and MDX files
20
+ */
21
+ export const MARKDOWN_FILE_REGEX = /\.(md|mdx)$/;
22
+ /**
23
+ * Default configuration values
24
+ */
25
+ export const DEFAULT_SITE_NAME = 'My Blog';
26
+ export const DEFAULT_BLOG_ROUTE = 'blog';
27
+ export const DEFAULT_BLOGS_ROUTE = 'blogs';
28
+ export const DEFAULT_LANG = 'en';
29
+ export const RSS_POST_LIMIT = 20;
30
+ /**
31
+ * Gets the posts directory path relative to the current working directory
32
+ * @param customPath - Optional custom path to posts directory
33
+ * @param locale - Optional locale code. If provided, appends /{locale} to the posts directory path
34
+ * @returns The full path to the posts directory
35
+ */
36
+ export function getPostsDirectory(customPath, locale) {
37
+ const baseDir = customPath || POSTS_DIR_NAME;
38
+ const postsDir = path.join(process.cwd(), baseDir);
39
+ // If locale is provided, append it to the path (e.g., posts/en/)
40
+ if (locale) {
41
+ return path.join(postsDir, locale);
42
+ }
43
+ return postsDir;
44
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Base error class for next-md-blog library errors
3
+ */
4
+ export declare class MdxBlogError extends Error {
5
+ readonly code: string;
6
+ constructor(message: string, code: string);
7
+ }
8
+ /**
9
+ * Error thrown when a blog post is not found
10
+ */
11
+ export declare class BlogPostNotFoundError extends MdxBlogError {
12
+ constructor(slug: string);
13
+ }
14
+ /**
15
+ * Error thrown when there's an issue reading files
16
+ */
17
+ export declare class FileReadError extends MdxBlogError {
18
+ constructor(filePath: string, originalError?: Error, context?: {
19
+ operation?: string;
20
+ slug?: string;
21
+ });
22
+ /** The file path that failed to be read */
23
+ readonly filePath: string;
24
+ /** The original error that occurred */
25
+ readonly originalError?: Error;
26
+ /** Additional context about the operation */
27
+ readonly context?: {
28
+ operation?: string;
29
+ slug?: string;
30
+ };
31
+ }
32
+ /**
33
+ * Error thrown when there's an issue with directory operations
34
+ */
35
+ export declare class DirectoryError extends MdxBlogError {
36
+ constructor(directoryPath: string, originalError?: Error, context?: {
37
+ operation?: string;
38
+ });
39
+ /** The directory path that failed */
40
+ readonly directoryPath: string;
41
+ /** The original error that occurred */
42
+ readonly originalError?: Error;
43
+ /** Additional context about the operation */
44
+ readonly context?: {
45
+ operation?: string;
46
+ };
47
+ }
48
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;aACQ,IAAI,EAAE,MAAM;gBAA7C,OAAO,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAK1D;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;gBACzC,IAAI,EAAE,MAAM;CAKzB;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,YAAY;gBAE3C,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,KAAK,EACrB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;IAkBjD,2CAA2C;IAC3C,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,uCAAuC;IACvC,SAAgB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtC,6CAA6C;IAC7C,SAAgB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;gBAE5C,aAAa,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,KAAK,EACrB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAkBlC,qCAAqC;IACrC,SAAgB,aAAa,EAAE,MAAM,CAAC;IACtC,uCAAuC;IACvC,SAAgB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtC,6CAA6C;IAC7C,SAAgB,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Base error class for next-md-blog library errors
3
+ */
4
+ export class MdxBlogError extends Error {
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = 'MdxBlogError';
9
+ Object.setPrototypeOf(this, MdxBlogError.prototype);
10
+ }
11
+ }
12
+ /**
13
+ * Error thrown when a blog post is not found
14
+ */
15
+ export class BlogPostNotFoundError extends MdxBlogError {
16
+ constructor(slug) {
17
+ super(`Blog post with slug "${slug}" not found`, 'BLOG_POST_NOT_FOUND');
18
+ this.name = 'BlogPostNotFoundError';
19
+ Object.setPrototypeOf(this, BlogPostNotFoundError.prototype);
20
+ }
21
+ }
22
+ /**
23
+ * Error thrown when there's an issue reading files
24
+ */
25
+ export class FileReadError extends MdxBlogError {
26
+ constructor(filePath, originalError, context) {
27
+ const contextMessage = context?.operation ? ` during ${context.operation}` : '';
28
+ super(`Failed to read file at "${filePath}"${contextMessage}: ${originalError?.message || 'Unknown error'}`, 'FILE_READ_ERROR');
29
+ this.name = 'FileReadError';
30
+ this.filePath = filePath;
31
+ if (originalError !== undefined) {
32
+ this.originalError = originalError;
33
+ }
34
+ if (context !== undefined) {
35
+ this.context = context;
36
+ }
37
+ Object.setPrototypeOf(this, FileReadError.prototype);
38
+ }
39
+ }
40
+ /**
41
+ * Error thrown when there's an issue with directory operations
42
+ */
43
+ export class DirectoryError extends MdxBlogError {
44
+ constructor(directoryPath, originalError, context) {
45
+ const contextMessage = context?.operation ? ` during ${context.operation}` : '';
46
+ super(`Directory operation failed for "${directoryPath}"${contextMessage}: ${originalError?.message || 'Unknown error'}`, 'DIRECTORY_ERROR');
47
+ this.name = 'DirectoryError';
48
+ this.directoryPath = directoryPath;
49
+ if (originalError !== undefined) {
50
+ this.originalError = originalError;
51
+ }
52
+ if (context !== undefined) {
53
+ this.context = context;
54
+ }
55
+ Object.setPrototypeOf(this, DirectoryError.prototype);
56
+ }
57
+ }
@@ -0,0 +1,22 @@
1
+ import type { BlogPost, BlogPostMetadata, GetBlogPostOptions } from './types.js';
2
+ /**
3
+ * Gets a single blog post by slug
4
+ * @param slug - The slug of the blog post (filename without .md or .mdx extension)
5
+ * @param options - Optional configuration
6
+ * @returns The blog post or null if not found
7
+ * @throws {Error} If slug is invalid or file cannot be read
8
+ */
9
+ export declare function getBlogPost(slug: string, options?: GetBlogPostOptions): Promise<BlogPost | null>;
10
+ /**
11
+ * Gets all blog posts from the posts directory
12
+ * @param options - Optional configuration
13
+ * @returns Array of blog post metadata, sorted by date (newest first)
14
+ */
15
+ export declare function getAllBlogPosts(options?: GetBlogPostOptions): Promise<BlogPostMetadata[]>;
16
+ /**
17
+ * Gets all blog post slugs
18
+ * @param options - Optional configuration
19
+ * @returns Array of slug strings
20
+ */
21
+ export declare function getAllBlogPostSlugs(options?: GetBlogPostOptions): Promise<string[]>;
22
+ //# sourceMappingURL=file-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/core/file-utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,kBAAkB,EAAU,MAAM,YAAY,CAAC;AAkHzF;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAuB1B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA0D7B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC,CAEnB"}
@@ -0,0 +1,180 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { getPostsDirectory, SUPPORTED_EXTENSIONS, MARKDOWN_FILE_REGEX } from './constants.js';
5
+ import { validateSlug, validateFrontmatter, validateContent } from './validation.js';
6
+ import { BlogPostNotFoundError, FileReadError, DirectoryError } from './errors.js';
7
+ import { calculateReadingTime, calculateWordCount, normalizeAuthors } from './utils.js';
8
+ import { getConfig } from './config.js';
9
+ /**
10
+ * Safely reads a file and returns its contents
11
+ * @param filePath - Path to the file
12
+ * @returns File contents or null if file doesn't exist
13
+ * @throws {FileReadError} If file exists but cannot be read
14
+ */
15
+ function readFileSafe(filePath) {
16
+ try {
17
+ if (!fs.existsSync(filePath)) {
18
+ return null;
19
+ }
20
+ const stats = fs.statSync(filePath);
21
+ if (!stats.isFile()) {
22
+ return null;
23
+ }
24
+ return fs.readFileSync(filePath, 'utf8');
25
+ }
26
+ catch (error) {
27
+ throw new FileReadError(filePath, error instanceof Error ? error : undefined, { operation: 'readFileSafe' });
28
+ }
29
+ }
30
+ /**
31
+ * Safely reads directory contents
32
+ * @param dirPath - Path to the directory
33
+ * @returns Array of filenames or empty array if directory doesn't exist
34
+ * @throws {DirectoryError} If directory exists but cannot be read
35
+ */
36
+ function readDirectorySafe(dirPath) {
37
+ try {
38
+ if (!fs.existsSync(dirPath)) {
39
+ return [];
40
+ }
41
+ const stats = fs.statSync(dirPath);
42
+ if (!stats.isDirectory()) {
43
+ return [];
44
+ }
45
+ return fs.readdirSync(dirPath);
46
+ }
47
+ catch (error) {
48
+ throw new DirectoryError(dirPath, error instanceof Error ? error : undefined, { operation: 'readDirectorySafe' });
49
+ }
50
+ }
51
+ /**
52
+ * Parses a markdown or MDX file and extracts frontmatter and content
53
+ * @param filePath - Path to the markdown or MDX file
54
+ * @param slug - The slug for the post
55
+ * @param config - Optional blog configuration for author resolution
56
+ * @returns Parsed blog post
57
+ * @throws {FileReadError} If file cannot be read or parsed
58
+ */
59
+ function parseMarkdownFile(filePath, slug, config) {
60
+ const fileContents = readFileSafe(filePath);
61
+ if (fileContents === null) {
62
+ throw new BlogPostNotFoundError(slug);
63
+ }
64
+ try {
65
+ const { data: frontmatter, content } = matter(fileContents);
66
+ validateContent(content);
67
+ const trimmedContent = content.trim();
68
+ const validatedFrontmatter = validateFrontmatter(frontmatter);
69
+ // Automatically calculate reading time and word count
70
+ // Use frontmatter value if provided, otherwise calculate
71
+ const readingTime = validatedFrontmatter.readingTime || calculateReadingTime(trimmedContent);
72
+ const wordCount = calculateWordCount(trimmedContent);
73
+ // Use provided config or fallback to defaults
74
+ const blogConfig = config || getConfig();
75
+ // Normalize authors (resolve from config if available)
76
+ const authors = normalizeAuthors(validatedFrontmatter.author, validatedFrontmatter.authors, blogConfig.authors);
77
+ return {
78
+ slug,
79
+ content: trimmedContent,
80
+ frontmatter: validatedFrontmatter,
81
+ readingTime,
82
+ wordCount,
83
+ authors,
84
+ };
85
+ }
86
+ catch (error) {
87
+ throw new FileReadError(filePath, error instanceof Error ? error : new Error('Failed to parse markdown'), { operation: 'parseMarkdownFile', slug });
88
+ }
89
+ }
90
+ /**
91
+ * Gets a single blog post by slug
92
+ * @param slug - The slug of the blog post (filename without .md or .mdx extension)
93
+ * @param options - Optional configuration
94
+ * @returns The blog post or null if not found
95
+ * @throws {Error} If slug is invalid or file cannot be read
96
+ */
97
+ export async function getBlogPost(slug, options = {}) {
98
+ try {
99
+ validateSlug(slug);
100
+ const postsDir = getPostsDirectory(options.postsDir, options.locale);
101
+ // Try both .md and .mdx extensions
102
+ for (const ext of SUPPORTED_EXTENSIONS) {
103
+ const filePath = path.join(postsDir, `${slug}${ext}`);
104
+ const fileContents = readFileSafe(filePath);
105
+ if (fileContents !== null) {
106
+ return parseMarkdownFile(filePath, slug, options.config);
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ catch (error) {
112
+ if (error instanceof BlogPostNotFoundError || error instanceof FileReadError) {
113
+ throw error;
114
+ }
115
+ // Re-throw validation errors
116
+ throw error;
117
+ }
118
+ }
119
+ /**
120
+ * Gets all blog posts from the posts directory
121
+ * @param options - Optional configuration
122
+ * @returns Array of blog post metadata, sorted by date (newest first)
123
+ */
124
+ export async function getAllBlogPosts(options = {}) {
125
+ try {
126
+ const postsDir = getPostsDirectory(options.postsDir, options.locale);
127
+ const files = readDirectorySafe(postsDir);
128
+ const mdFiles = files.filter((file) => MARKDOWN_FILE_REGEX.test(file));
129
+ const blogConfig = options.config || getConfig();
130
+ const posts = mdFiles
131
+ .map((file) => {
132
+ try {
133
+ // Remove both .md and .mdx extensions
134
+ const slug = file.replace(MARKDOWN_FILE_REGEX, '');
135
+ const filePath = path.join(postsDir, file);
136
+ const fileContents = readFileSafe(filePath);
137
+ if (fileContents === null) {
138
+ return null;
139
+ }
140
+ const { data: frontmatter } = matter(fileContents);
141
+ const validatedFrontmatter = validateFrontmatter(frontmatter);
142
+ const authors = normalizeAuthors(validatedFrontmatter.author, validatedFrontmatter.authors, blogConfig.authors);
143
+ return {
144
+ slug,
145
+ frontmatter: validatedFrontmatter,
146
+ authors,
147
+ };
148
+ }
149
+ catch (error) {
150
+ // Skip files that cannot be parsed
151
+ // In production, consider logging to a structured logger instead of console.warn
152
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
153
+ console.warn(`Skipping file ${file}: ${errorMessage}`);
154
+ return null;
155
+ }
156
+ })
157
+ .filter((post) => post !== null)
158
+ .sort((a, b) => {
159
+ const dateA = a.frontmatter?.date || '';
160
+ const dateB = b.frontmatter?.date || '';
161
+ return dateB.localeCompare(dateA);
162
+ });
163
+ return posts;
164
+ }
165
+ catch (error) {
166
+ // If directory doesn't exist, return empty array
167
+ if (error instanceof DirectoryError) {
168
+ return [];
169
+ }
170
+ throw error;
171
+ }
172
+ }
173
+ /**
174
+ * Gets all blog post slugs
175
+ * @param options - Optional configuration
176
+ * @returns Array of slug strings
177
+ */
178
+ export function getAllBlogPostSlugs(options = {}) {
179
+ return getAllBlogPosts(options).then((posts) => posts.map((post) => post.slug));
180
+ }
@@ -0,0 +1,16 @@
1
+ import type { BlogPost, BlogPostMetadata, Config } from './types.js';
2
+ /**
3
+ * Generates sitemap XML for blog posts
4
+ * @param posts - Array of blog post metadata
5
+ * @param config - SEO configuration
6
+ * @returns Sitemap XML string
7
+ */
8
+ export declare function generateSitemap(posts: BlogPostMetadata[], config?: Config): string;
9
+ /**
10
+ * Generates RSS feed XML for blog posts
11
+ * @param posts - Array of blog posts
12
+ * @param config - SEO configuration
13
+ * @returns RSS XML string
14
+ */
15
+ export declare function generateRSSFeed(posts: BlogPost[], config?: Config): string;
16
+ //# sourceMappingURL=seo-feeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-feeds.d.ts","sourceRoot":"","sources":["../../src/core/seo-feeds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAMrE;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAwBR;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAuDR"}
@@ -0,0 +1,73 @@
1
+ import { getConfig } from './config.js';
2
+ import { resolveFrontmatterField } from './type-guards.js';
3
+ import { DEFAULT_SITE_NAME, RSS_POST_LIMIT } from './constants.js';
4
+ import { resolvePostUrl, escapeXml, getAuthorName } from './seo-utils.js';
5
+ /**
6
+ * Generates sitemap XML for blog posts
7
+ * @param posts - Array of blog post metadata
8
+ * @param config - SEO configuration
9
+ * @returns Sitemap XML string
10
+ */
11
+ export function generateSitemap(posts, config) {
12
+ const blogConfig = config || getConfig();
13
+ const { siteUrl = '' } = blogConfig;
14
+ const urls = posts
15
+ .map((post) => {
16
+ const lastmod = resolveFrontmatterField(['modifiedDate', 'date'], post.frontmatter) || new Date().toISOString().split('T')[0];
17
+ const url = `${siteUrl}/blog/${post.slug}`;
18
+ return ` <url>
19
+ <loc>${escapeXml(url)}</loc>
20
+ <lastmod>${lastmod}</lastmod>
21
+ <changefreq>monthly</changefreq>
22
+ <priority>0.8</priority>
23
+ </url>`;
24
+ })
25
+ .join('\n');
26
+ return `<?xml version="1.0" encoding="UTF-8"?>
27
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
28
+ ${urls}
29
+ </urlset>`;
30
+ }
31
+ /**
32
+ * Generates RSS feed XML for blog posts
33
+ * @param posts - Array of blog posts
34
+ * @param config - SEO configuration
35
+ * @returns RSS XML string
36
+ */
37
+ export function generateRSSFeed(posts, config) {
38
+ const blogConfig = config || getConfig();
39
+ const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, } = blogConfig;
40
+ const items = posts
41
+ .slice(0, RSS_POST_LIMIT) // Limit to most recent posts
42
+ .map((post) => {
43
+ const title = resolveFrontmatterField(['title'], post.frontmatter, post.slug) || post.slug;
44
+ const description = resolveFrontmatterField(['description', 'excerpt'], post.frontmatter, '') || '';
45
+ const authorObj = post.authors[0];
46
+ const author = authorObj ? getAuthorName(authorObj) : (defaultAuthor || '');
47
+ const pubDate = resolveFrontmatterField(['publishedDate', 'date'], post.frontmatter) || new Date().toISOString();
48
+ const url = resolvePostUrl(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl);
49
+ // Format date for RSS (RFC 822)
50
+ const rssDate = new Date(pubDate).toUTCString();
51
+ return ` <item>
52
+ <title>${escapeXml(title)}</title>
53
+ <link>${escapeXml(url)}</link>
54
+ <guid isPermaLink="true">${escapeXml(url)}</guid>
55
+ <description>${escapeXml(description)}</description>
56
+ <author>${escapeXml(author)}</author>
57
+ <pubDate>${rssDate}</pubDate>
58
+ </item>`;
59
+ })
60
+ .join('\n');
61
+ return `<?xml version="1.0" encoding="UTF-8"?>
62
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
63
+ <channel>
64
+ <title>${escapeXml(siteName)}</title>
65
+ <link>${escapeXml(siteUrl)}</link>
66
+ <description>Latest blog posts from ${escapeXml(siteName)}</description>
67
+ <language>en</language>
68
+ <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
69
+ <atom:link href="${escapeXml(siteUrl)}/feed.xml" rel="self" type="application/rss+xml"/>
70
+ ${items}
71
+ </channel>
72
+ </rss>`;
73
+ }
@@ -0,0 +1,17 @@
1
+ import type { BlogPost, BlogPostMetadata, Config } from './types.js';
2
+ import type { Metadata } from 'next';
3
+ /**
4
+ * Generates comprehensive metadata for a blog post
5
+ * @param post - The blog post
6
+ * @param config - SEO configuration
7
+ * @returns Metadata object for Next.js
8
+ */
9
+ export declare function generateBlogPostMetadata(post: BlogPost, config?: Config): Metadata;
10
+ /**
11
+ * Generates metadata for the blog listing page
12
+ * @param posts - Array of blog posts
13
+ * @param config - SEO configuration
14
+ * @returns Metadata object for Next.js
15
+ */
16
+ export declare function generateBlogListMetadata(posts: BlogPostMetadata[], config?: Config): Metadata;
17
+ //# sourceMappingURL=seo-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-metadata.d.ts","sourceRoot":"","sources":["../../src/core/seo-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAcrC;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,CAmMV;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,CAwBV"}