@next-md-blog/core 1.0.1 → 1.0.3

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 (42) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +39 -210
  3. package/dist/components/BlogPostSEO.d.ts +3 -1
  4. package/dist/components/BlogPostSEO.d.ts.map +1 -1
  5. package/dist/components/BlogPostSEO.js +6 -4
  6. package/dist/components/MarkdownContent.d.ts +5 -6
  7. package/dist/components/MarkdownContent.d.ts.map +1 -1
  8. package/dist/components/MarkdownContent.js +8 -4
  9. package/dist/components/markdown/img.d.ts.map +1 -1
  10. package/dist/components/markdown/img.js +4 -1
  11. package/dist/core/config.d.ts +1 -1
  12. package/dist/core/config.js +1 -1
  13. package/dist/core/organization-schema.d.ts +22 -0
  14. package/dist/core/organization-schema.d.ts.map +1 -0
  15. package/dist/core/organization-schema.js +83 -0
  16. package/dist/core/seo-feeds.d.ts +2 -12
  17. package/dist/core/seo-feeds.d.ts.map +1 -1
  18. package/dist/core/seo-feeds.js +4 -34
  19. package/dist/core/seo-metadata.d.ts.map +1 -1
  20. package/dist/core/seo-metadata.js +16 -12
  21. package/dist/core/seo-schema.d.ts +13 -1
  22. package/dist/core/seo-schema.d.ts.map +1 -1
  23. package/dist/core/seo-schema.js +42 -13
  24. package/dist/core/seo-utils.d.ts +19 -7
  25. package/dist/core/seo-utils.d.ts.map +1 -1
  26. package/dist/core/seo-utils.js +65 -7
  27. package/dist/core/seo.d.ts +8 -4
  28. package/dist/core/seo.d.ts.map +1 -1
  29. package/dist/core/seo.js +6 -4
  30. package/dist/core/sitemap-data.d.ts +18 -0
  31. package/dist/core/sitemap-data.d.ts.map +1 -0
  32. package/dist/core/sitemap-data.js +42 -0
  33. package/dist/core/types.d.ts +21 -0
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/core/utils.js +2 -2
  36. package/dist/index.d.ts +4 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -1
  39. package/dist/next.d.ts +19 -0
  40. package/dist/next.d.ts.map +1 -0
  41. package/dist/next.js +44 -0
  42. package/package.json +32 -27
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 next-md-blog contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,242 +1,71 @@
1
1
  # @next-md-blog/core
2
2
 
3
- A powerful React library for parsing and displaying markdown and MDX blog posts in Next.js projects. Write your blog posts in markdown or MDX format and easily display them with dynamic routes, complete SEO optimization, and beautiful Open Graph images.
3
+ **Next.js App Router–first** helpers for **Markdown / MDX** posts on disk: **`MarkdownContent`**, **`generateBlogPostMetadata`**, **JSON-LD** (including richer **Organization** publisher data), **RSS**, and SEO via **[metadata file conventions](https://nextjs.org/docs/app/api-reference/file-conventions/metadata)** use **`app/sitemap.ts`** / **`app/robots.ts`** / optional **`feed.xml`** with **`@next-md-blog/core/next`** (`getBlogSitemap`, `getBlogRobots`, `createRssFeedResponse`).
4
4
 
5
- ## ✨ Features
6
-
7
- - 📝 **Markdown & MDX Support** - Full markdown and MDX support with GitHub Flavored Markdown (GFM) support
8
- - 🎨 **Server Components** - Built for Next.js App Router with server-side rendering
9
- - 🔍 **SEO Optimized** - Automatic metadata generation with Open Graph and Twitter Cards
10
- - 🖼️ **OG Images** - Built-in OG image generation component
11
- - ⚡ **Type Safe** - Full TypeScript support with comprehensive types
12
- - 🎯 **Flexible** - Works with both App Router and Pages Router
13
- - 🛡️ **Robust** - Input validation, error handling, and clean code principles
14
-
15
- ## 📦 Installation
5
+ ## Install
16
6
 
17
7
  ```bash
18
8
  npm install @next-md-blog/core
19
9
  ```
20
10
 
21
- ## 🚀 Quick Start
22
-
23
- ### 1. Initialize with CLI (Recommended)
24
-
25
- The easiest way to get started is using the CLI:
11
+ Peers: `next@^16`, `react@^19`, `react-dom@^19`.
26
12
 
27
- ```bash
28
- npx @next-md-blog/cli
29
- ```
13
+ ## Documentation
30
14
 
31
- This will automatically:
32
- - Create blog routes and pages
33
- - Install required dependencies
34
- - Configure Tailwind CSS typography
35
- - ✅ Set up SEO configuration
15
+ - **Published docs:** [https://www.next-md-blog.com](https://www.next-md-blog.com)
16
+ - **Live demos:** [demo.next-md-blog.com](https://demo.next-md-blog.com) (single locale) · [demo.i18n.next-md-blog.com](https://demo.i18n.next-md-blog.com) (i18n)
17
+ - **Vercel:** single locale [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnext-md-blog%2Ftemplate) · i18n [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnext-md-blog%2Ftemplate-i18n)
18
+ - **Source & issues:** [github.com/next-md-blog/next-md-blog](https://github.com/next-md-blog/next-md-blog)
36
19
 
37
- ### 2. Manual Setup
20
+ To build the docs locally, clone the monorepo, run `pnpm install && pnpm dev:docs`, and open [http://localhost:5101](http://localhost:5101).
38
21
 
39
- If you prefer manual setup:
40
-
41
- 1. **Install the package:**
42
- ```bash
43
- npm install @next-md-blog/core @tailwindcss/typography
44
- ```
22
+ Entry points in the repo: [Home / overview](https://github.com/next-md-blog/next-md-blog/blob/main/docs/content/index.mdx), [API reference](https://github.com/next-md-blog/next-md-blog/blob/main/docs/content/api-reference.mdx).
45
23
 
46
- 2. **Create a config file:**
47
- ```tsx
48
- // next-md-blog.config.ts
49
- import { createConfig } from '@next-md-blog/core';
50
-
51
- export default createConfig({
52
- siteName: 'My Blog',
53
- siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com',
54
- defaultAuthor: 'Your Name',
55
- twitterHandle: '@yourhandle',
56
- defaultLang: 'en',
57
- });
58
- ```
24
+ ## Quick usage
59
25
 
60
- 3. **Create blog routes:**
61
26
  ```tsx
62
- // app/blog/[slug]/page.tsx
63
- import { getBlogPost, getAllBlogPosts, generateBlogPostMetadata } from '@next-md-blog/core';
64
- import { MarkdownContent } from '@next-md-blog/core';
65
- import { notFound } from 'next/navigation';
66
- import type { Metadata } from 'next';
27
+ import {
28
+ getBlogPost,
29
+ getAllBlogPosts,
30
+ MarkdownContent,
31
+ createConfig,
32
+ generateBlogPostMetadata,
33
+ } from '@next-md-blog/core';
67
34
  import blogConfig from '@/next-md-blog.config';
68
35
 
69
- export async function generateStaticParams() {
70
- const posts = await getAllBlogPosts({ config: blogConfig });
71
- return posts.map((post) => ({
72
- slug: post.slug,
73
- }));
74
- }
75
-
76
- export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
77
- const { slug } = await params;
78
- const post = await getBlogPost(slug, { config: blogConfig });
79
-
80
- if (!post) {
81
- return { title: 'Post Not Found' };
82
- }
83
-
84
- return generateBlogPostMetadata(post, blogConfig);
85
- }
86
-
87
- export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
88
- const { slug } = await params;
89
- const post = await getBlogPost(slug, { config: blogConfig });
90
-
91
- if (!post) {
92
- notFound();
93
- }
94
-
95
- return (
96
- <div className="prose prose-lg dark:prose-invert max-w-none">
97
- <MarkdownContent content={post.content} />
98
- </div>
99
- );
100
- }
101
- ```
102
-
103
- ## 📚 API Reference
104
-
105
- ### Components
106
-
107
- #### `MarkdownContent`
108
-
109
- A React Server Component that renders markdown content as HTML.
110
-
111
- **Props:**
112
- - `content` (string, required): The markdown content to render
113
- - `className` (string, optional): CSS class name for the container
114
- - `components` (MarkdownComponents, optional): Custom components to override default markdown rendering
115
- - `remarkPlugins` (any[], optional): Custom remark plugins to extend markdown parsing
116
- - `rehypePlugins` (any[], optional): Custom rehype plugins to extend HTML processing
117
-
118
- **Example:**
119
- ```tsx
120
- <MarkdownContent content="# Hello World" className="prose prose-lg" />
121
- ```
122
-
123
- #### `BlogPostSEO`
124
-
125
- A component that generates JSON-LD structured data for SEO.
126
-
127
- **Props:**
128
- - `post` (BlogPost, required): The blog post object
129
- - `config` (SEOConfig, required): SEO configuration
130
-
131
- **Example:**
132
- ```tsx
133
- <BlogPostSEO post={post} config={blogConfig} />
36
+ const post = await getBlogPost('hello', { config: blogConfig });
134
37
  ```
135
38
 
136
- #### `OgImage`
137
-
138
- A component for generating Open Graph images (used with `@vercel/og`).
139
-
140
- **Props:**
141
- - `title` (string, required): Title text
142
- - `description` (string, optional): Description/subtitle text
143
- - `siteName` (string, optional): Site name
144
- - `backgroundColor` (string, optional): Background color (default: `#1a1a1a`)
145
- - `textColor` (string, optional): Text color (default: `#ffffff`)
146
-
147
- ### Functions
148
-
149
- #### `getBlogPost(slug: string, options?: GetBlogPostOptions): Promise<BlogPost | null>`
150
-
151
- Retrieves a single blog post by its slug.
152
-
153
- **Parameters:**
154
- - `slug` (string): The slug of the blog post (filename without .md or .mdx extension)
155
- - `options` (optional): Configuration object
156
- - `postsDir` (string, optional): Custom path to posts directory
157
- - `config` (Config, optional): Blog configuration
158
-
159
- **Returns:**
160
- - `Promise<BlogPost | null>`: The blog post object or null if not found
161
-
162
- #### `getAllBlogPosts(options?: GetBlogPostOptions): Promise<BlogPostMetadata[]>`
163
-
164
- Retrieves all blog posts from the posts folder.
165
-
166
- **Parameters:**
167
- - `options` (optional): Configuration object
168
- - `postsDir` (string, optional): Custom path to posts directory
169
- - `config` (Config, optional): Blog configuration
170
-
171
- **Returns:**
172
- - `Promise<BlogPostMetadata[]>`: Array of blog post metadata, sorted by date (newest first)
173
-
174
- #### `generateBlogPostMetadata(post: BlogPost, config?: SEOConfig): Metadata`
175
-
176
- Generates comprehensive SEO metadata for a blog post.
39
+ Scaffold routes, **`sitemap.ts`**, **`robots.ts`**, and config with **`npx @next-md-blog/cli`**.
177
40
 
178
- **Parameters:**
179
- - `post` (BlogPost): The blog post object
180
- - `config` (SEOConfig, optional): SEO configuration
41
+ ### `app/sitemap.ts` / `app/robots.ts`
181
42
 
182
- **Returns:**
183
- - `Metadata`: Next.js metadata object
184
-
185
- #### `generateBlogListMetadata(posts: BlogPostMetadata[], config?: SEOConfig): Metadata`
186
-
187
- Generates SEO metadata for the blog listing page.
188
-
189
- ### Types
190
-
191
- #### `BlogPost`
192
-
193
- ```typescript
194
- interface BlogPost {
195
- slug: string;
196
- content: string;
197
- frontmatter: BlogPostFrontmatter;
198
- authors: Author[];
199
- readingTime: number;
200
- wordCount: number;
201
- }
202
- ```
43
+ ```ts
44
+ import { getAllBlogPosts } from '@next-md-blog/core';
45
+ import { getBlogSitemap, getBlogRobots } from '@next-md-blog/core/next';
46
+ import blogConfig from '@/next-md-blog.config';
203
47
 
204
- #### `BlogPostFrontmatter`
205
-
206
- ```typescript
207
- interface BlogPostFrontmatter {
208
- title?: string;
209
- date?: string;
210
- description?: string;
211
- author?: string;
212
- tags?: string[];
213
- ogImage?: string;
214
- image?: string;
215
- [key: string]: unknown;
48
+ export default async function sitemap() {
49
+ const posts = await getAllBlogPosts({ config: blogConfig });
50
+ return getBlogSitemap(posts, blogConfig);
216
51
  }
217
52
  ```
218
53
 
219
- ## 🎨 Styling
220
-
221
- The library is designed to work with Tailwind CSS v4 and the `@tailwindcss/typography` plugin. Use the `prose` classes for beautiful markdown styling:
54
+ ```ts
55
+ import { getBlogRobots } from '@next-md-blog/core/next';
56
+ import blogConfig from '@/next-md-blog.config';
222
57
 
223
- ```tsx
224
- <div className="prose prose-lg dark:prose-invert max-w-none">
225
- <MarkdownContent content={post.content} />
226
- </div>
58
+ export default function robots() {
59
+ return getBlogRobots(blogConfig);
60
+ }
227
61
  ```
228
62
 
229
- ## 📖 Documentation
63
+ ## Links
230
64
 
231
- For complete documentation, examples, and advanced usage, visit the [main repository](https://github.com/florianamette/next-mdx-blog).
65
+ - [npm](https://www.npmjs.com/package/@next-md-blog/core)
66
+ - [Issues](https://github.com/next-md-blog/next-md-blog/issues)
67
+ - Source: [`packages/core`](https://github.com/next-md-blog/next-md-blog/tree/main/packages/core)
232
68
 
233
- ## 📝 License
69
+ ## License
234
70
 
235
71
  MIT
236
-
237
- ## 🔗 Links
238
-
239
- - [GitHub Repository](https://github.com/florianamette/next-mdx-blog)
240
- - [CLI Package](https://www.npmjs.com/package/@next-md-blog/cli)
241
- - [Issues](https://github.com/florianamette/next-mdx-blog/issues)
242
-
@@ -14,6 +14,8 @@ export interface BlogPostSEOProps {
14
14
  }>;
15
15
  /** Whether to include breadcrumbs schema (default: true) */
16
16
  includeBreadcrumbs?: boolean;
17
+ /** Single `@graph` script (Organization + article + breadcrumbs by reference) */
18
+ asGraph?: boolean;
17
19
  }
18
20
  /**
19
21
  * Component that generates and injects JSON-LD structured data for a blog post
@@ -24,5 +26,5 @@ export interface BlogPostSEOProps {
24
26
  * <BlogPostSEO post={post} />
25
27
  * ```
26
28
  */
27
- export declare function BlogPostSEO({ post, config, breadcrumbs, includeBreadcrumbs, }: BlogPostSEOProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function BlogPostSEO({ post, config, breadcrumbs, includeBreadcrumbs, asGraph, }: BlogPostSEOProps): import("react/jsx-runtime.js").JSX.Element;
28
30
  //# sourceMappingURL=BlogPostSEO.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BlogPostSEO.d.ts","sourceRoot":"","sources":["../../src/components/BlogPostSEO.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAIzD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oBAAoB;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,MAAM,EACN,WAAW,EACX,kBAAyB,GAC1B,EAAE,gBAAgB,2CA2BlB"}
1
+ {"version":3,"file":"BlogPostSEO.d.ts","sourceRoot":"","sources":["../../src/components/BlogPostSEO.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAQzD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oBAAoB;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,MAAM,EACN,WAAW,EACX,kBAAyB,EACzB,OAAe,GAChB,EAAE,gBAAgB,8CAsClB"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { generateBlogPostSchema, generateBreadcrumbsSchema } from '../core/seo.js';
2
+ import { generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, } from '../core/seo.js';
3
3
  import { getConfig } from '../core/config.js';
4
4
  /**
5
5
  * Component that generates and injects JSON-LD structured data for a blog post
@@ -10,11 +10,13 @@ import { getConfig } from '../core/config.js';
10
10
  * <BlogPostSEO post={post} />
11
11
  * ```
12
12
  */
13
- export function BlogPostSEO({ post, config, breadcrumbs, includeBreadcrumbs = true, }) {
13
+ export function BlogPostSEO({ post, config, breadcrumbs, includeBreadcrumbs = true, asGraph = false, }) {
14
14
  const blogConfig = config || getConfig();
15
- // Generate article schema
15
+ if (asGraph) {
16
+ const graphSchema = generateBlogPostSchemaGraph(post, blogConfig, breadcrumbs, includeBreadcrumbs);
17
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(graphSchema) } }));
18
+ }
16
19
  const articleSchema = generateBlogPostSchema(post, blogConfig);
17
- // Generate breadcrumbs schema if enabled
18
20
  const breadcrumbsSchema = includeBreadcrumbs
19
21
  ? generateBreadcrumbsSchema(post, blogConfig, breadcrumbs)
20
22
  : null;
@@ -1,9 +1,8 @@
1
- import type { Components } from 'react-markdown';
1
+ import type { Components, Options } from 'react-markdown';
2
2
  /**
3
3
  * Component types that can be overridden
4
4
  */
5
- export interface MarkdownComponents extends Partial<Components> {
6
- }
5
+ export type MarkdownComponents = Partial<Components>;
7
6
  /**
8
7
  * Props for the MarkdownContent component
9
8
  */
@@ -15,9 +14,9 @@ export interface MarkdownContentProps {
15
14
  /** Optional custom components to override default markdown rendering */
16
15
  components?: MarkdownComponents;
17
16
  /** Optional remark plugins */
18
- remarkPlugins?: any[];
17
+ remarkPlugins?: Options['remarkPlugins'];
19
18
  /** Optional rehype plugins */
20
- rehypePlugins?: any[];
19
+ rehypePlugins?: Options['rehypePlugins'];
21
20
  }
22
21
  /**
23
22
  * React Server Component that renders markdown content as React elements
@@ -44,5 +43,5 @@ export interface MarkdownContentProps {
44
43
  * />
45
44
  * ```
46
45
  */
47
- export declare function MarkdownContent({ content, className, components, remarkPlugins, rehypePlugins, }: MarkdownContentProps): import("react/jsx-runtime").JSX.Element;
46
+ export declare function MarkdownContent({ content, className, components, remarkPlugins, rehypePlugins, }: MarkdownContentProps): import("react/jsx-runtime.js").JSX.Element;
48
47
  //# sourceMappingURL=MarkdownContent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownContent.d.ts","sourceRoot":"","sources":["../../src/components/MarkdownContent.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,OAAO,CAAC,UAAU,CAAC;CAAG;AAElE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,8BAA8B;IAC9B,aAAa,CAAC,EAAE,GAAG,EAAE,CAAC;IACtB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,GAAG,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,SAAc,EACd,UAAU,EACV,aAAkB,EAClB,aAAkB,GACnB,EAAE,oBAAoB,2CA0BtB"}
1
+ {"version":3,"file":"MarkdownContent.d.ts","sourceRoot":"","sources":["../../src/components/MarkdownContent.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAU1D;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,8BAA8B;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACzC,8BAA8B;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,SAAc,EACd,UAAU,EACV,aAAa,EACb,aAAa,GACd,EAAE,oBAAoB,8CAyBtB"}
@@ -3,6 +3,11 @@ import ReactMarkdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import remarkEmoji from 'remark-emoji';
5
5
  import { defaultMarkdownComponents } from './markdown/defaults.js';
6
+ function toPluggableArray(list) {
7
+ if (list == null)
8
+ return [];
9
+ return Array.isArray(list) ? list : [list];
10
+ }
6
11
  /**
7
12
  * React Server Component that renders markdown content as React elements
8
13
  * Uses react-markdown under the hood with support for custom components.
@@ -28,7 +33,7 @@ import { defaultMarkdownComponents } from './markdown/defaults.js';
28
33
  * />
29
34
  * ```
30
35
  */
31
- export function MarkdownContent({ content, className = '', components, remarkPlugins = [], rehypePlugins = [], }) {
36
+ export function MarkdownContent({ content, className = '', components, remarkPlugins, rehypePlugins, }) {
32
37
  if (!content || typeof content !== 'string') {
33
38
  throw new Error('Invalid content: must be a non-empty string');
34
39
  }
@@ -38,7 +43,6 @@ export function MarkdownContent({ content, className = '', components, remarkPlu
38
43
  ...defaultMarkdownComponents,
39
44
  ...components,
40
45
  };
41
- // Default remark plugins
42
- const defaultRemarkPlugins = [remarkGfm, remarkEmoji, ...remarkPlugins];
43
- return (_jsx("article", { className: className ? `prose prose-lg max-w-none dark:prose-invert ${className}` : 'prose prose-lg max-w-none dark:prose-invert', children: _jsx(ReactMarkdown, { remarkPlugins: defaultRemarkPlugins, rehypePlugins: rehypePlugins, components: mergedComponents, children: content }) }));
46
+ const defaultRemarkPlugins = [remarkGfm, remarkEmoji, ...toPluggableArray(remarkPlugins)];
47
+ return (_jsx("article", { className: className ? `prose prose-lg max-w-none dark:prose-invert ${className}` : 'prose prose-lg max-w-none dark:prose-invert', children: _jsx(ReactMarkdown, { remarkPlugins: defaultRemarkPlugins, rehypePlugins: toPluggableArray(rehypePlugins), components: mergedComponents, children: content }) }));
44
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"img.d.ts","sourceRoot":"","sources":["../../../src/components/markdown/img.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,QAAS,SAAQ,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC;CAAG;AAE9E,QAAA,MAAM,GAAG,mFAeR,CAAC;AAGF,eAAe,GAAG,CAAC"}
1
+ {"version":3,"file":"img.d.ts","sourceRoot":"","sources":["../../../src/components/markdown/img.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,QAAS,SAAQ,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC;CAAG;AAE9E,QAAA,MAAM,GAAG,mFAmBR,CAAC;AAGF,eAAe,GAAG,CAAC"}
@@ -2,7 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  const Img = React.forwardRef(({ className, alt, src, ...props }, ref) => {
4
4
  // Generate a fallback alt text from the image filename if alt is not provided
5
- const altText = alt || (src ? `Image: ${src.split('/').pop()?.split('?')[0] || 'image'}` : 'Image');
5
+ const altText = alt ||
6
+ (typeof src === 'string' && src
7
+ ? `Image: ${src.split('/').pop()?.split('?')[0] || 'image'}`
8
+ : 'Image');
6
9
  return (_jsx("img", { ref: ref, alt: altText, src: src, className: className, ...props }));
7
10
  });
8
11
  Img.displayName = 'Img';
@@ -5,7 +5,7 @@ import type { Config } from './types.js';
5
5
  *
6
6
  * @example
7
7
  * ```ts
8
- * import { createConfig } from '@florianamette/next-md-blog';
8
+ * import { createConfig } from '@next-md-blog/core';
9
9
  *
10
10
  * export default createConfig({
11
11
  * siteName: 'My Blog',
@@ -14,7 +14,7 @@ const DEFAULT_CONFIG = {
14
14
  *
15
15
  * @example
16
16
  * ```ts
17
- * import { createConfig } from '@florianamette/next-md-blog';
17
+ * import { createConfig } from '@next-md-blog/core';
18
18
  *
19
19
  * export default createConfig({
20
20
  * siteName: 'My Blog',
@@ -0,0 +1,22 @@
1
+ import type { Config } from './types.js';
2
+ /**
3
+ * Normalizes siteUrl to an origin with no trailing slash (empty string if invalid).
4
+ */
5
+ export declare function normalizeSiteOrigin(siteUrl: string | undefined): string;
6
+ /**
7
+ * Stable @id for the site Organization node (fragment on origin).
8
+ */
9
+ export declare function resolveOrganizationId(config?: Config): string | undefined;
10
+ /**
11
+ * JSON-LD Organization node for publisher / standalone script.
12
+ */
13
+ export declare function buildOrganizationNode(config?: Config): Record<string, unknown> | undefined;
14
+ /**
15
+ * Standalone Organization JSON-LD for layout or @graph.
16
+ */
17
+ export declare function generateOrganizationSchema(config?: Config): Record<string, unknown> | undefined;
18
+ /** Publisher object for BlogPosting (no @context). */
19
+ export declare function buildPublisherEmbedded(config?: Config): Record<string, unknown> | undefined;
20
+ /** Organization node for @graph (no @context). */
21
+ export declare function buildOrganizationGraphNode(config?: Config): Record<string, unknown> | undefined;
22
+ //# sourceMappingURL=organization-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"organization-schema.d.ts","sourceRoot":"","sources":["../../src/core/organization-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAIzC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAQvE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAoC1F;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE/F;AAED,sDAAsD;AACtD,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAM3F;AAED,kDAAkD;AAClD,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE/F"}
@@ -0,0 +1,83 @@
1
+ import { DEFAULT_SITE_NAME } from './constants.js';
2
+ import { getConfig } from './config.js';
3
+ /**
4
+ * Normalizes siteUrl to an origin with no trailing slash (empty string if invalid).
5
+ */
6
+ export function normalizeSiteOrigin(siteUrl) {
7
+ if (!siteUrl?.trim())
8
+ return '';
9
+ try {
10
+ const u = new URL(siteUrl);
11
+ return `${u.protocol}//${u.host}`;
12
+ }
13
+ catch {
14
+ return siteUrl.replace(/\/$/, '');
15
+ }
16
+ }
17
+ /**
18
+ * Stable @id for the site Organization node (fragment on origin).
19
+ */
20
+ export function resolveOrganizationId(config) {
21
+ const blogConfig = config || getConfig();
22
+ const org = blogConfig.organization;
23
+ if (org?.id?.trim())
24
+ return org.id.trim();
25
+ const origin = normalizeSiteOrigin(blogConfig.siteUrl);
26
+ if (!origin)
27
+ return undefined;
28
+ return `${origin}/#organization`;
29
+ }
30
+ /**
31
+ * JSON-LD Organization node for publisher / standalone script.
32
+ */
33
+ export function buildOrganizationNode(config) {
34
+ const blogConfig = config || getConfig();
35
+ const { siteName = DEFAULT_SITE_NAME, siteUrl = '', organization: org, } = blogConfig;
36
+ if (!siteName)
37
+ return undefined;
38
+ const origin = normalizeSiteOrigin(siteUrl);
39
+ const id = resolveOrganizationId(blogConfig);
40
+ const node = {
41
+ '@context': 'https://schema.org',
42
+ '@type': 'Organization',
43
+ name: siteName,
44
+ };
45
+ if (id)
46
+ node['@id'] = id;
47
+ if (origin || siteUrl) {
48
+ node.url = origin || siteUrl.replace(/\/$/, '');
49
+ }
50
+ if (org?.legalName)
51
+ node.legalName = org.legalName;
52
+ if (org?.description)
53
+ node.description = org.description;
54
+ if (org?.logo) {
55
+ node.logo = {
56
+ '@type': 'ImageObject',
57
+ url: org.logo,
58
+ };
59
+ }
60
+ if (org?.sameAs && org.sameAs.length > 0) {
61
+ node.sameAs = org.sameAs.length === 1 ? org.sameAs[0] : org.sameAs;
62
+ }
63
+ return node;
64
+ }
65
+ /**
66
+ * Standalone Organization JSON-LD for layout or @graph.
67
+ */
68
+ export function generateOrganizationSchema(config) {
69
+ return buildOrganizationNode(config);
70
+ }
71
+ /** Publisher object for BlogPosting (no @context). */
72
+ export function buildPublisherEmbedded(config) {
73
+ const node = buildOrganizationNode(config);
74
+ if (!node)
75
+ return undefined;
76
+ const rest = { ...node };
77
+ delete rest['@context'];
78
+ return rest;
79
+ }
80
+ /** Organization node for @graph (no @context). */
81
+ export function buildOrganizationGraphNode(config) {
82
+ return buildPublisherEmbedded(config);
83
+ }
@@ -1,16 +1,6 @@
1
- import type { BlogPost, BlogPostMetadata, Config } from './types.js';
1
+ import type { BlogPost, Config } from './types.js';
2
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
3
+ * Generates RSS feed XML for blog posts (used by `createRssFeedResponse` in `@next-md-blog/core/next`).
14
4
  */
15
5
  export declare function generateRSSFeed(posts: BlogPost[], config?: Config): string;
16
6
  //# sourceMappingURL=seo-feeds.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"seo-feeds.d.ts","sourceRoot":"","sources":["../../src/core/seo-feeds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAUnD;;GAEG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAuDR"}
@@ -1,52 +1,22 @@
1
1
  import { getConfig } from './config.js';
2
2
  import { resolveFrontmatterField } from './type-guards.js';
3
3
  import { DEFAULT_SITE_NAME, RSS_POST_LIMIT } from './constants.js';
4
- import { resolvePostUrl, escapeXml, getAuthorName } from './seo-utils.js';
4
+ import { resolvePostUrlWithConfig, escapeXml, getAuthorName, } from './seo-utils.js';
5
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
6
+ * Generates RSS feed XML for blog posts (used by `createRssFeedResponse` in `@next-md-blog/core/next`).
36
7
  */
37
8
  export function generateRSSFeed(posts, config) {
38
9
  const blogConfig = config || getConfig();
39
10
  const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, } = blogConfig;
40
11
  const items = posts
41
- .slice(0, RSS_POST_LIMIT) // Limit to most recent posts
12
+ .slice(0, RSS_POST_LIMIT)
42
13
  .map((post) => {
43
14
  const title = resolveFrontmatterField(['title'], post.frontmatter, post.slug) || post.slug;
44
15
  const description = resolveFrontmatterField(['description', 'excerpt'], post.frontmatter, '') || '';
45
16
  const authorObj = post.authors[0];
46
17
  const author = authorObj ? getAuthorName(authorObj) : (defaultAuthor || '');
47
18
  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)
19
+ const url = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, blogConfig);
50
20
  const rssDate = new Date(pubDate).toUTCString();
51
21
  return ` <item>
52
22
  <title>${escapeXml(title)}</title>