next-posts 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.
- package/LICENSE +21 -0
- package/README-zh.md +334 -0
- package/README.md +337 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1751 -0
- package/index.ts +65 -0
- package/package.json +34 -0
- package/tests/fixtures/custom-posts/custom.md +6 -0
- package/tests/fixtures/posts/hello-world.md +6 -0
- package/tests/fixtures/posts/second-post.md +9 -0
- package/tests/index.test.ts +252 -0
- package/tsconfig.json +14 -0
- package/tsdown.config.ts +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 yd-tw
|
|
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.
|
package/README-zh.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# next-posts
|
|
2
|
+
|
|
3
|
+
> 載入文章並將 YAML 解析到 Next.js 中!
|
|
4
|
+
|
|
5
|
+
一個輕量、零依賴的 TypeScript 函式庫,用於在 Next.js 中建立靜態部落格等文章頁面。它會解析包含 YAML frontmatter 的 Markdown 檔案,並提供簡潔的工具函式,方便搭配 SSG(Static Site Generation)使用。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 功能特色
|
|
10
|
+
|
|
11
|
+
- 零執行期依賴 —— 僅使用 Node.js 內建模組
|
|
12
|
+
- 完整 TypeScript 支援(支援泛型 metadata)
|
|
13
|
+
- 支援任意目錄結構
|
|
14
|
+
- 同時支援 `slug` 與 `slug.md` 輸入格式
|
|
15
|
+
- 相容 Next.js App Router 與 Pages Router
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 安裝
|
|
20
|
+
|
|
21
|
+
使用 npm 或你喜歡的套件管理工具。
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install next-posts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 快速開始
|
|
30
|
+
|
|
31
|
+
在專案根目錄建立 `posts/` 資料夾,並放入 Markdown 檔案:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
my-next-app/
|
|
35
|
+
└── posts/
|
|
36
|
+
├── hello-world.md
|
|
37
|
+
└── getting-started.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
每個檔案應包含 YAML frontmatter 區塊與文章內容:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
---
|
|
44
|
+
title: Hello World
|
|
45
|
+
date: 2024-01-01
|
|
46
|
+
tags:
|
|
47
|
+
- blog
|
|
48
|
+
- intro
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
Welcome to my blog!
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
接著在 Next.js 中使用:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { getAllPosts, getPostBySlug, getAllPostParams } from "next-posts";
|
|
58
|
+
|
|
59
|
+
interface PostMeta {
|
|
60
|
+
title: string;
|
|
61
|
+
date: string;
|
|
62
|
+
tags?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 取得所有文章
|
|
66
|
+
const posts = getAllPosts<PostMeta>();
|
|
67
|
+
|
|
68
|
+
// 取得單篇文章
|
|
69
|
+
const post = getPostBySlug<PostMeta>("hello-world");
|
|
70
|
+
|
|
71
|
+
// 產生靜態路徑(Next.js)
|
|
72
|
+
const paths = getAllPostParams();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 模板
|
|
76
|
+
|
|
77
|
+
你也可以從模板開始:
|
|
78
|
+
|
|
79
|
+
- [next-profile-template](https://github.com/yd-tw/next-profile-template)
|
|
80
|
+
- [kuang-ti-web](https://github.com/yd-tw/kuang-ti-web)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
### `getAllPosts<T>(directory?)`
|
|
87
|
+
|
|
88
|
+
回傳指定目錄中的所有文章,包含解析後的 frontmatter 與內容。
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const posts = getAllPosts<PostMeta>();
|
|
92
|
+
// [
|
|
93
|
+
// { slug: 'hello-world', metadata: { title: '...' }, content: '...' },
|
|
94
|
+
// ...
|
|
95
|
+
// ]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| 參數 | 型別 | 預設值 | 說明 |
|
|
99
|
+
| ----------- | -------- | ---------- | ----------------------------- |
|
|
100
|
+
| `directory` | `string` | `"posts/"` | 相對於 `process.cwd()` 的路徑 |
|
|
101
|
+
|
|
102
|
+
**回傳型別:**
|
|
103
|
+
`Array<{ slug: string; metadata: T; content: string }>`
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `getPostBySlug<T>(slug, directory?)`
|
|
108
|
+
|
|
109
|
+
依據 slug 取得單篇文章。支援帶或不帶 `.md` 副檔名。
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const post = getPostBySlug<PostMeta>("hello-world");
|
|
113
|
+
// {
|
|
114
|
+
// slug: 'hello-world',
|
|
115
|
+
// metadata: { title: 'Hello World', ... },
|
|
116
|
+
// content: '...'
|
|
117
|
+
// }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| 參數 | 型別 | 預設值 | 說明 |
|
|
121
|
+
| ----------- | -------- | ---------- | ----------------------------- |
|
|
122
|
+
| `slug` | `string` | — | 檔名(可含或不含 `.md`) |
|
|
123
|
+
| `directory` | `string` | `"posts/"` | 相對於 `process.cwd()` 的路徑 |
|
|
124
|
+
|
|
125
|
+
**回傳型別:**
|
|
126
|
+
`{ slug: string; metadata: T; content: string }`
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### `getAllPostSlugs(directory?)`
|
|
131
|
+
|
|
132
|
+
回傳指定目錄中的原始檔名(包含 `.md` 副檔名)。
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const slugs = getAllPostSlugs();
|
|
136
|
+
// ['hello-world.md', 'getting-started.md']
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| 參數 | 型別 | 預設值 | 說明 |
|
|
140
|
+
| ----------- | -------- | ---------- | ----------------------------- |
|
|
141
|
+
| `directory` | `string` | `"posts/"` | 相對於 `process.cwd()` 的路徑 |
|
|
142
|
+
|
|
143
|
+
**回傳型別:**
|
|
144
|
+
`string[]`
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### `getAllPostParams(directory?)`
|
|
149
|
+
|
|
150
|
+
回傳適用於 Next.js `generateStaticParams` 或 `getStaticPaths` 的 slug 參數物件,會自動移除 `.md` 副檔名。
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const params = getAllPostParams();
|
|
154
|
+
// [{ slug: 'hello-world' }, { slug: 'getting-started' }]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| 參數 | 型別 | 預設值 | 說明 |
|
|
158
|
+
| ----------- | -------- | ---------- | ----------------------------- |
|
|
159
|
+
| `directory` | `string` | `"posts/"` | 相對於 `process.cwd()` 的路徑 |
|
|
160
|
+
|
|
161
|
+
**回傳型別:**
|
|
162
|
+
`Array<{ slug: string }>`
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### `parseFrontmatter(input)`
|
|
167
|
+
|
|
168
|
+
解析原始 Markdown 字串並提取 YAML frontmatter。適用於非從檔案系統讀取的 Markdown,或需要較底層控制時使用。
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { parseFrontmatter } from "next-posts";
|
|
172
|
+
|
|
173
|
+
const raw = `---
|
|
174
|
+
title: Hello World
|
|
175
|
+
date: 2024-01-01
|
|
176
|
+
tags:
|
|
177
|
+
- blog
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
Welcome to my blog!`;
|
|
181
|
+
|
|
182
|
+
const { data, content } = parseFrontmatter(raw);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- `data`:解析後的 YAML frontmatter 物件,若無有效 frontmatter 則為 `{}`。
|
|
186
|
+
- `content`:移除 frontmatter 後的 Markdown 內容。
|
|
187
|
+
|
|
188
|
+
| 參數 | 型別 | 說明 |
|
|
189
|
+
| ------- | -------- | -------------------------------- |
|
|
190
|
+
| `input` | `string` | 含有 frontmatter 的原始 Markdown |
|
|
191
|
+
|
|
192
|
+
**回傳型別:**
|
|
193
|
+
`{ data: Record<string, unknown>; content: string }`
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 搭配 Next.js 使用
|
|
198
|
+
|
|
199
|
+
### App Router
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
// app/blog/[slug]/page.tsx
|
|
203
|
+
import { getAllPostParams, getPostBySlug } from "next-posts";
|
|
204
|
+
|
|
205
|
+
interface PostMeta {
|
|
206
|
+
title: string;
|
|
207
|
+
date: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function generateStaticParams() {
|
|
211
|
+
return getAllPostParams();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default async function Page({
|
|
215
|
+
params,
|
|
216
|
+
}: {
|
|
217
|
+
params: Promise<{ slug: string }>;
|
|
218
|
+
}) {
|
|
219
|
+
const { slug } = await params;
|
|
220
|
+
const { metadata, content } = getPostBySlug<PostMeta>(slug);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<article>
|
|
224
|
+
<h1>{metadata.title}</h1>
|
|
225
|
+
<p>{metadata.date}</p>
|
|
226
|
+
{/* 推薦使用 ReactMarkdown,但你可以使用任何喜愛的 */}
|
|
227
|
+
<ReactMarkdown>{post.content}</ReactMarkdown>
|
|
228
|
+
</article>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### Pages Router
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// pages/blog/[slug].tsx
|
|
239
|
+
import { GetStaticPaths, GetStaticProps } from "next";
|
|
240
|
+
import { getAllPostParams, getPostBySlug } from "next-posts";
|
|
241
|
+
|
|
242
|
+
interface PostMeta {
|
|
243
|
+
title: string;
|
|
244
|
+
date: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const getStaticPaths: GetStaticPaths = () => ({
|
|
248
|
+
paths: getAllPostParams().map(({ slug }) => ({ params: { slug } })),
|
|
249
|
+
fallback: false,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
export const getStaticProps: GetStaticProps = ({ params }) => {
|
|
253
|
+
const post = getPostBySlug<PostMeta>(params!.slug as string);
|
|
254
|
+
return { props: { post } };
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 自訂文章目錄
|
|
261
|
+
|
|
262
|
+
所有函式皆支援傳入自訂目錄:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const posts = getAllPosts("content/articles/");
|
|
266
|
+
const post = getPostBySlug("my-article", "content/articles/");
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 從 v0.x 版本遷移
|
|
272
|
+
|
|
273
|
+
遷移過程很容易,因為大部分的 API 都保持兼容。
|
|
274
|
+
|
|
275
|
+
1. 變更套件名稱:
|
|
276
|
+
|
|
277
|
+
`next-posts` 過去稱為 `next-staticblog` ,你可以直接替換套件名稱並指向最新版本。舊的套件將棄用並不再進行維護。
|
|
278
|
+
|
|
279
|
+
2. 明確的型別:
|
|
280
|
+
|
|
281
|
+
在 `v0.x` 版本中使用 `any` 來定義 `metadata`,
|
|
282
|
+
在 `v1.x` 版本中改為使用 `unknown` 來提升安全性。
|
|
283
|
+
請閱讀文檔瞭解如何傳入泛型,擁有安全的型別。
|
|
284
|
+
|
|
285
|
+
除此之外套件移除了 `gray-matter` 並改用 `@std/yaml` 進行解析。雖然變更通過了所有的測試,但仍然可能在部分非標準用法上有細微差異。
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 常見問題
|
|
290
|
+
|
|
291
|
+
### no such file or directory
|
|
292
|
+
|
|
293
|
+
我們推薦你使用 SSG 建置,因為這樣才能確保最高效率。因此若你在動態路由中使用 `next-posts` 將有可能導致錯誤。
|
|
294
|
+
幸運的是通常你只需要使用 `getAllPostParams()` 並結合 `generateStaticParams()` 就能使用 `SSG`。
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
Error: ENOENT: no such file or directory, scandir '/var/task/posts/news/zh'
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
但若你堅持使用動態路由,則需要在 next.config.ts 添加設置以確保文章能夠被包含。
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// next.config.ts
|
|
304
|
+
outputFileTracingIncludes: {
|
|
305
|
+
"/**": ["./posts/**/*.{md,mdx}"],
|
|
306
|
+
},
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 貢獻
|
|
312
|
+
|
|
313
|
+
你可以透過回報問題或提交新功能完善這個套件!
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# 安裝依賴
|
|
317
|
+
npm install
|
|
318
|
+
|
|
319
|
+
# 建置
|
|
320
|
+
npm run build
|
|
321
|
+
|
|
322
|
+
# 執行測試
|
|
323
|
+
npm test
|
|
324
|
+
|
|
325
|
+
# 格式化程式碼
|
|
326
|
+
npm run format
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 授權
|
|
332
|
+
|
|
333
|
+
MIT © yd-tw
|
|
334
|
+
[https://github.com/yd-tw/next-posts](https://github.com/yd-tw/next-posts)
|
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# next-posts
|
|
2
|
+
|
|
3
|
+
> Load posts and parse YAML into Next.js!
|
|
4
|
+
|
|
5
|
+
A lightweight, zero-dependency TypeScript library for building static blog posts and content pages in Next.js. It parses Markdown files with YAML frontmatter and provides simple utility functions designed for use with SSG (Static Site Generation).
|
|
6
|
+
|
|
7
|
+
[中文](/README-zh.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Zero runtime dependencies — only uses built-in Node.js modules
|
|
14
|
+
- Full TypeScript support (generic metadata support)
|
|
15
|
+
- Supports arbitrary directory structures
|
|
16
|
+
- Accepts both `slug` and `slug.md` formats
|
|
17
|
+
- Compatible with Next.js App Router and Pages Router
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Install using npm or your preferred package manager:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install next-posts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
Create a `posts/` directory in your project root and add Markdown files:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
my-next-app/
|
|
37
|
+
└── posts/
|
|
38
|
+
├── hello-world.md
|
|
39
|
+
└── getting-started.md
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each file should contain a YAML frontmatter block and content:
|
|
43
|
+
|
|
44
|
+
```markdown
|
|
45
|
+
---
|
|
46
|
+
title: Hello World
|
|
47
|
+
date: 2024-01-01
|
|
48
|
+
tags:
|
|
49
|
+
- blog
|
|
50
|
+
- intro
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
Welcome to my blog!
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then use it in your Next.js project:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { getAllPosts, getPostBySlug, getAllPostParams } from "next-posts";
|
|
60
|
+
|
|
61
|
+
interface PostMeta {
|
|
62
|
+
title: string;
|
|
63
|
+
date: string;
|
|
64
|
+
tags?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get all posts
|
|
68
|
+
const posts = getAllPosts<PostMeta>();
|
|
69
|
+
|
|
70
|
+
// Get a single post
|
|
71
|
+
const post = getPostBySlug<PostMeta>("hello-world");
|
|
72
|
+
|
|
73
|
+
// Generate static paths (Next.js)
|
|
74
|
+
const paths = getAllPostParams();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Template
|
|
78
|
+
|
|
79
|
+
You can also start with a template:
|
|
80
|
+
|
|
81
|
+
- [next-profile-template](https://github.com/yd-tw/next-profile-template)
|
|
82
|
+
- [kuang-ti-web](https://github.com/yd-tw/kuang-ti-web)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### `getAllPosts<T>(directory?)`
|
|
89
|
+
|
|
90
|
+
Returns all posts from the specified directory, including parsed frontmatter and content.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const posts = getAllPosts<PostMeta>();
|
|
94
|
+
// [
|
|
95
|
+
// { slug: 'hello-world', metadata: { title: '...' }, content: '...' },
|
|
96
|
+
// ...
|
|
97
|
+
// ]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
| Parameter | Type | Default | Description |
|
|
101
|
+
| ----------- | -------- | ---------- | -------------------------------- |
|
|
102
|
+
| `directory` | `string` | `"posts/"` | Path relative to `process.cwd()` |
|
|
103
|
+
|
|
104
|
+
**Return type:**
|
|
105
|
+
`Array<{ slug: string; metadata: T; content: string }>`
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### `getPostBySlug<T>(slug, directory?)`
|
|
110
|
+
|
|
111
|
+
Returns a single post by slug. Supports both with and without the `.md` extension.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const post = getPostBySlug<PostMeta>("hello-world");
|
|
115
|
+
// {
|
|
116
|
+
// slug: 'hello-world',
|
|
117
|
+
// metadata: { title: 'Hello World', ... },
|
|
118
|
+
// content: '...'
|
|
119
|
+
// }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Parameter | Type | Default | Description |
|
|
123
|
+
| ----------- | -------- | ---------- | -------------------------------- |
|
|
124
|
+
| `slug` | `string` | — | Filename (with or without `.md`) |
|
|
125
|
+
| `directory` | `string` | `"posts/"` | Path relative to `process.cwd()` |
|
|
126
|
+
|
|
127
|
+
**Return type:**
|
|
128
|
+
`{ slug: string; metadata: T; content: string }`
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `getAllPostSlugs(directory?)`
|
|
133
|
+
|
|
134
|
+
Returns all raw filenames (including the `.md` extension) from the specified directory.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const slugs = getAllPostSlugs();
|
|
138
|
+
// ['hello-world.md', 'getting-started.md']
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Parameter | Type | Default | Description |
|
|
142
|
+
| ----------- | -------- | ---------- | -------------------------------- |
|
|
143
|
+
| `directory` | `string` | `"posts/"` | Path relative to `process.cwd()` |
|
|
144
|
+
|
|
145
|
+
**Return type:**
|
|
146
|
+
`string[]`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `getAllPostParams(directory?)`
|
|
151
|
+
|
|
152
|
+
Returns slug parameter objects suitable for Next.js `generateStaticParams` or `getStaticPaths`. Automatically removes the `.md` extension.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const params = getAllPostParams();
|
|
156
|
+
// [{ slug: 'hello-world' }, { slug: 'getting-started' }]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Parameter | Type | Default | Description |
|
|
160
|
+
| ----------- | -------- | ---------- | -------------------------------- |
|
|
161
|
+
| `directory` | `string` | `"posts/"` | Path relative to `process.cwd()` |
|
|
162
|
+
|
|
163
|
+
**Return type:**
|
|
164
|
+
`Array<{ slug: string }>`
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### `parseFrontmatter(input)`
|
|
169
|
+
|
|
170
|
+
Parses a raw Markdown string and extracts YAML frontmatter. Useful when Markdown is not loaded from the filesystem or when lower-level control is needed.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { parseFrontmatter } from "next-posts";
|
|
174
|
+
|
|
175
|
+
const raw = `---
|
|
176
|
+
title: Hello World
|
|
177
|
+
date: 2024-01-01
|
|
178
|
+
tags:
|
|
179
|
+
- blog
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
Welcome to my blog!`;
|
|
183
|
+
|
|
184
|
+
const { data, content } = parseFrontmatter(raw);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- `data`: Parsed YAML frontmatter object. Returns `{}` if no valid frontmatter exists.
|
|
188
|
+
- `content`: Markdown content with the frontmatter removed.
|
|
189
|
+
|
|
190
|
+
| Parameter | Type | Description |
|
|
191
|
+
| --------- | -------- | ------------------------------------------ |
|
|
192
|
+
| `input` | `string` | Raw Markdown string containing frontmatter |
|
|
193
|
+
|
|
194
|
+
**Return type:**
|
|
195
|
+
`{ data: Record<string, unknown>; content: string }`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Using with Next.js
|
|
200
|
+
|
|
201
|
+
### App Router
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
// app/blog/[slug]/page.tsx
|
|
205
|
+
import { getAllPostParams, getPostBySlug } from "next-posts";
|
|
206
|
+
|
|
207
|
+
interface PostMeta {
|
|
208
|
+
title: string;
|
|
209
|
+
date: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function generateStaticParams() {
|
|
213
|
+
return getAllPostParams();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default async function Page({
|
|
217
|
+
params,
|
|
218
|
+
}: {
|
|
219
|
+
params: Promise<{ slug: string }>;
|
|
220
|
+
}) {
|
|
221
|
+
const { slug } = await params;
|
|
222
|
+
const { metadata, content } = getPostBySlug<PostMeta>(slug);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<article>
|
|
226
|
+
<h1>{metadata.title}</h1>
|
|
227
|
+
<p>{metadata.date}</p>
|
|
228
|
+
{/* ReactMarkdown is recommended, but you can use any renderer you prefer */}
|
|
229
|
+
<ReactMarkdown>{content}</ReactMarkdown>
|
|
230
|
+
</article>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Pages Router
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// pages/blog/[slug].tsx
|
|
241
|
+
import { GetStaticPaths, GetStaticProps } from "next";
|
|
242
|
+
import { getAllPostParams, getPostBySlug } from "next-posts";
|
|
243
|
+
|
|
244
|
+
interface PostMeta {
|
|
245
|
+
title: string;
|
|
246
|
+
date: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const getStaticPaths: GetStaticPaths = () => ({
|
|
250
|
+
paths: getAllPostParams().map(({ slug }) => ({ params: { slug } })),
|
|
251
|
+
fallback: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export const getStaticProps: GetStaticProps = ({ params }) => {
|
|
255
|
+
const post = getPostBySlug<PostMeta>(params!.slug as string);
|
|
256
|
+
return { props: { post } };
|
|
257
|
+
};
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Custom Post Directory
|
|
263
|
+
|
|
264
|
+
All functions accept a custom directory:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const posts = getAllPosts("content/articles/");
|
|
268
|
+
const post = getPostBySlug("my-article", "content/articles/");
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Migrating from v0.x
|
|
274
|
+
|
|
275
|
+
Migration is straightforward since most APIs remain compatible.
|
|
276
|
+
|
|
277
|
+
1. Package rename:
|
|
278
|
+
|
|
279
|
+
`next-posts` was previously called `next-staticblog`. Simply replace the package name and install the latest version. The old package is deprecated and no longer maintained.
|
|
280
|
+
|
|
281
|
+
2. Explicit typing:
|
|
282
|
+
|
|
283
|
+
In `v0.x`, `metadata` was typed as `any`.
|
|
284
|
+
In `v1.x`, it is typed as `unknown` to improve type safety.
|
|
285
|
+
Refer to the documentation on how to pass generics for safe typing.
|
|
286
|
+
|
|
287
|
+
Additionally, the package removed `gray-matter` and now uses `@std/yaml` for parsing. While all tests pass, there may be minor differences in some non-standard use cases.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## FAQ
|
|
292
|
+
|
|
293
|
+
### no such file or directory
|
|
294
|
+
|
|
295
|
+
SSG is strongly recommended for maximum efficiency. Using `next-posts` inside dynamic routes may cause errors.
|
|
296
|
+
|
|
297
|
+
Typically, you only need to combine `getAllPostParams()` with `generateStaticParams()` to enable SSG.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
Error: ENOENT: no such file or directory, scandir '/var/task/posts/news/zh'
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
If you insist on using dynamic routes, add the following configuration to `next.config.ts` to ensure Markdown files are included:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// next.config.ts
|
|
307
|
+
outputFileTracingIncludes: {
|
|
308
|
+
"/**": ["./posts/**/*.{md,mdx}"],
|
|
309
|
+
},
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Contributing
|
|
315
|
+
|
|
316
|
+
You can improve this package by reporting issues or submitting new features!
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Install dependencies
|
|
320
|
+
npm install
|
|
321
|
+
|
|
322
|
+
# Build
|
|
323
|
+
npm run build
|
|
324
|
+
|
|
325
|
+
# Run tests
|
|
326
|
+
npm test
|
|
327
|
+
|
|
328
|
+
# Format code
|
|
329
|
+
npm run format
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
MIT © yd-tw
|
|
337
|
+
[https://github.com/yd-tw/next-posts](https://github.com/yd-tw/next-posts)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function parseFrontmatter(input: string): {
|
|
2
|
+
data: Record<string, unknown>;
|
|
3
|
+
content: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function getAllPostSlugs(directory?: string): string[];
|
|
6
|
+
export declare function getAllPosts<T extends object = Record<string, unknown>>(directory?: string): {
|
|
7
|
+
slug: string;
|
|
8
|
+
metadata: T;
|
|
9
|
+
content: string;
|
|
10
|
+
}[];
|
|
11
|
+
export declare function getAllPostParams(directory?: string): {
|
|
12
|
+
slug: string;
|
|
13
|
+
}[];
|
|
14
|
+
export declare function getPostBySlug<T extends object = Record<string, unknown>>(slug: string, directory?: string): {
|
|
15
|
+
slug: string;
|
|
16
|
+
metadata: T;
|
|
17
|
+
content: string;
|
|
18
|
+
};
|