@next-md-blog/core 1.0.2 → 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.
- package/LICENSE +22 -0
- package/README.md +39 -210
- package/dist/components/BlogPostSEO.d.ts +3 -1
- package/dist/components/BlogPostSEO.d.ts.map +1 -1
- package/dist/components/BlogPostSEO.js +6 -4
- package/dist/components/MarkdownContent.d.ts +5 -6
- package/dist/components/MarkdownContent.d.ts.map +1 -1
- package/dist/components/MarkdownContent.js +8 -4
- package/dist/components/markdown/img.d.ts.map +1 -1
- package/dist/components/markdown/img.js +4 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/organization-schema.d.ts +22 -0
- package/dist/core/organization-schema.d.ts.map +1 -0
- package/dist/core/organization-schema.js +83 -0
- package/dist/core/seo-feeds.d.ts +2 -12
- package/dist/core/seo-feeds.d.ts.map +1 -1
- package/dist/core/seo-feeds.js +4 -34
- package/dist/core/seo-metadata.d.ts.map +1 -1
- package/dist/core/seo-metadata.js +16 -12
- package/dist/core/seo-schema.d.ts +13 -1
- package/dist/core/seo-schema.d.ts.map +1 -1
- package/dist/core/seo-schema.js +42 -13
- package/dist/core/seo-utils.d.ts +19 -7
- package/dist/core/seo-utils.d.ts.map +1 -1
- package/dist/core/seo-utils.js +65 -7
- package/dist/core/seo.d.ts +8 -4
- package/dist/core/seo.d.ts.map +1 -1
- package/dist/core/seo.js +6 -4
- package/dist/core/sitemap-data.d.ts +18 -0
- package/dist/core/sitemap-data.d.ts.map +1 -0
- package/dist/core/sitemap-data.js +42 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/next.d.ts +19 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +44 -0
- package/package.json +33 -28
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
npx @next-md-blog/cli
|
|
29
|
-
```
|
|
13
|
+
## Documentation
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
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 [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fnext-md-blog%2Ftemplate) · i18n [](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
- `post` (BlogPost): The blog post object
|
|
180
|
-
- `config` (SEOConfig, optional): SEO configuration
|
|
41
|
+
### `app/sitemap.ts` / `app/robots.ts`
|
|
181
42
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
54
|
+
```ts
|
|
55
|
+
import { getBlogRobots } from '@next-md-blog/core/next';
|
|
56
|
+
import blogConfig from '@/next-md-blog.config';
|
|
222
57
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
</div>
|
|
58
|
+
export default function robots() {
|
|
59
|
+
return getBlogRobots(blogConfig);
|
|
60
|
+
}
|
|
227
61
|
```
|
|
228
62
|
|
|
229
|
-
##
|
|
63
|
+
## Links
|
|
230
64
|
|
|
231
|
-
|
|
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
|
-
##
|
|
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;
|
|
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
|
-
|
|
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
|
|
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?:
|
|
17
|
+
remarkPlugins?: Options['remarkPlugins'];
|
|
19
18
|
/** Optional rehype plugins */
|
|
20
|
-
rehypePlugins?:
|
|
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;
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
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,
|
|
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 ||
|
|
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';
|
package/dist/core/config.d.ts
CHANGED
package/dist/core/config.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/seo-feeds.d.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
import type { BlogPost,
|
|
1
|
+
import type { BlogPost, Config } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Generates
|
|
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,
|
|
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"}
|
package/dist/core/seo-feeds.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { resolvePostUrlWithConfig, escapeXml, getAuthorName, } from './seo-utils.js';
|
|
5
5
|
/**
|
|
6
|
-
* Generates
|
|
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)
|
|
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 =
|
|
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>
|