hd-garden 0.3.3 → 0.3.4
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/app/blog/[...path]/page.tsx +2 -2
- package/app/columns/[column]/[...path]/page.tsx +2 -2
- package/app/components/BlogArchive.tsx +1 -1
- package/app/components/GardenContentGuide.tsx +3 -5
- package/app/garden-modules.d.ts +0 -1
- package/app/lib/generated-content.ts +17 -23
- package/app/lib/github-content.ts +12 -7
- package/app/rss.xml/route.ts +1 -3
- package/package.json +1 -1
- package/scripts/build-content.mjs +15 -8
- package/scripts/new-post.mjs +0 -1
|
@@ -48,7 +48,7 @@ export async function generateMetadata({
|
|
|
48
48
|
? new Date(post.updated).toISOString()
|
|
49
49
|
: undefined,
|
|
50
50
|
tags: post.tags,
|
|
51
|
-
images: post.cover ? [{ url: post.cover, alt: post.
|
|
51
|
+
images: post.cover ? [{ url: post.cover, alt: post.title }] : undefined,
|
|
52
52
|
},
|
|
53
53
|
twitter: {
|
|
54
54
|
card: "summary_large_image",
|
|
@@ -92,7 +92,7 @@ export default async function PostPage({
|
|
|
92
92
|
<img
|
|
93
93
|
className="u-photo"
|
|
94
94
|
src={post.cover}
|
|
95
|
-
alt={
|
|
95
|
+
alt={`${post.title} 封面`}
|
|
96
96
|
/>
|
|
97
97
|
</figure>
|
|
98
98
|
)}
|
|
@@ -52,7 +52,7 @@ export async function generateMetadata({
|
|
|
52
52
|
? new Date(entry.updated).toISOString()
|
|
53
53
|
: undefined,
|
|
54
54
|
tags: entry.tags,
|
|
55
|
-
images: entry.cover ? [{ url: entry.cover, alt: entry.
|
|
55
|
+
images: entry.cover ? [{ url: entry.cover, alt: entry.title }] : undefined,
|
|
56
56
|
},
|
|
57
57
|
twitter: {
|
|
58
58
|
card: "summary_large_image",
|
|
@@ -115,7 +115,7 @@ export default async function ColumnEntryPage({
|
|
|
115
115
|
<img
|
|
116
116
|
className="u-photo"
|
|
117
117
|
src={entry.cover}
|
|
118
|
-
alt={
|
|
118
|
+
alt={`${entry.title} 封面`}
|
|
119
119
|
/>
|
|
120
120
|
</figure>
|
|
121
121
|
)}
|
|
@@ -26,13 +26,12 @@ function SourcePanel({ path, source, language }: SourcePanelProps) {
|
|
|
26
26
|
|
|
27
27
|
const postSource = `---
|
|
28
28
|
title: 我的第一篇文章
|
|
29
|
-
slug: first-post
|
|
30
29
|
date: 2026-08-03T09:00:00+08:00
|
|
31
30
|
description: 这篇文章记录我为什么重新开始写博客。
|
|
32
31
|
topic: 写作
|
|
33
32
|
tags: [Markdown, 博客]
|
|
34
33
|
pinned: false
|
|
35
|
-
cover:
|
|
34
|
+
cover: writing/first-post.jpg
|
|
36
35
|
---
|
|
37
36
|
|
|
38
37
|
正文从这里开始。
|
|
@@ -43,7 +42,6 @@ cover: ./assets/cover.jpg
|
|
|
43
42
|
|
|
44
43
|
const thoughtSource = `---
|
|
45
44
|
title: 雨后的散步
|
|
46
|
-
slug: walk-after-rain
|
|
47
45
|
date: 2026-08-03T18:30:00+08:00
|
|
48
46
|
tags: [生活]
|
|
49
47
|
media:
|
|
@@ -121,9 +119,9 @@ const guides: Guide[] = [
|
|
|
121
119
|
"随想适合记录几句话、一个链接或一组多媒体。它同样保存在 Git 仓库中,只需要更少的字段,也可以通过可选管理后台发布。",
|
|
122
120
|
steps: [
|
|
123
121
|
<>
|
|
124
|
-
直接创建 <code>content/thoughts/<
|
|
122
|
+
直接创建 <code>content/thoughts/<文件名>.md</code>,或者在管理后台填写内容。
|
|
125
123
|
</>,
|
|
126
|
-
|
|
124
|
+
<>文件名就是地址。front matter 填写标题和日期;需要暂存时加入草稿状态。</>,
|
|
127
125
|
<>
|
|
128
126
|
在 <code>media</code> 中按顺序添加图片、音频、视频或外部链接。
|
|
129
127
|
</>,
|
package/app/garden-modules.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by
|
|
1
|
+
// Generated by garden sync. Edit files in content/ instead.
|
|
2
2
|
export type MediaItem = {
|
|
3
3
|
type: "image" | "audio" | "video" | "link";
|
|
4
4
|
src: string;
|
|
@@ -33,7 +33,6 @@ export type ContentEntry = {
|
|
|
33
33
|
columnCoverAlt?: string;
|
|
34
34
|
order?: number;
|
|
35
35
|
cover?: string;
|
|
36
|
-
coverAlt?: string;
|
|
37
36
|
mediaType?: "image" | "audio" | "video" | "link" | "text";
|
|
38
37
|
media?: string | MediaItem[];
|
|
39
38
|
mediaAlt?: string;
|
|
@@ -42,11 +41,10 @@ export type ContentEntry = {
|
|
|
42
41
|
linkDescription?: string;
|
|
43
42
|
};
|
|
44
43
|
|
|
45
|
-
export const contentHash = "
|
|
44
|
+
export const contentHash = "86dad25b6dba";
|
|
46
45
|
export const posts = [
|
|
47
46
|
{
|
|
48
47
|
"title": "Markdown 复杂语法实验场",
|
|
49
|
-
"slug": "markdown-lab",
|
|
50
48
|
"description": "用一篇真实文章验证 Typora 常用语法、数学公式、Mermaid、脚注、表格与原生 HTML。",
|
|
51
49
|
"date": "2026-07-25T00:00:00.000Z",
|
|
52
50
|
"topic": "写作系统",
|
|
@@ -59,7 +57,7 @@ export const posts = [
|
|
|
59
57
|
"pinned": false,
|
|
60
58
|
"readingTime": "10 分钟",
|
|
61
59
|
"cover": "https://images.unsplash.com/photo-1761414500824-e280de5a1b37?auto=format&fit=crop&w=1600&q=84",
|
|
62
|
-
"
|
|
60
|
+
"slug": "markdown-lab",
|
|
63
61
|
"path": "markdown-lab",
|
|
64
62
|
"kind": "post",
|
|
65
63
|
"draft": false,
|
|
@@ -68,7 +66,6 @@ export const posts = [
|
|
|
68
66
|
},
|
|
69
67
|
{
|
|
70
68
|
"title": "注意力也是一种界面",
|
|
71
|
-
"slug": "attention-is-an-interface",
|
|
72
69
|
"description": "当工具越来越聪明,真正稀缺的不是功能,而是我们把注意力放在哪里。",
|
|
73
70
|
"date": "2026-07-21T00:00:00.000Z",
|
|
74
71
|
"updated": "2026-07-23T00:00:00.000Z",
|
|
@@ -80,7 +77,7 @@ export const posts = [
|
|
|
80
77
|
"pinned": true,
|
|
81
78
|
"readingTime": "6 分钟",
|
|
82
79
|
"cover": "https://images.unsplash.com/photo-1630234674857-527eb5534837?auto=format&fit=crop&w=1600&q=84",
|
|
83
|
-
"
|
|
80
|
+
"slug": "attention-is-an-interface",
|
|
84
81
|
"path": "attention-is-an-interface",
|
|
85
82
|
"kind": "post",
|
|
86
83
|
"draft": false,
|
|
@@ -89,7 +86,6 @@ export const posts = [
|
|
|
89
86
|
},
|
|
90
87
|
{
|
|
91
88
|
"title": "03 · 带着证据发布",
|
|
92
|
-
"slug": "ship-with-evidence",
|
|
93
89
|
"description": "“我改完了”不是完成,“用户可以确认它好了”才是。",
|
|
94
90
|
"date": "2026-07-18T00:00:00.000Z",
|
|
95
91
|
"topic": "独立开发",
|
|
@@ -98,6 +94,7 @@ export const posts = [
|
|
|
98
94
|
"发布"
|
|
99
95
|
],
|
|
100
96
|
"readingTime": "5 分钟",
|
|
97
|
+
"slug": "ship-with-evidence",
|
|
101
98
|
"path": "building-in-public/ship-with-evidence",
|
|
102
99
|
"kind": "post",
|
|
103
100
|
"draft": false,
|
|
@@ -106,7 +103,6 @@ export const posts = [
|
|
|
106
103
|
},
|
|
107
104
|
{
|
|
108
105
|
"title": "我为什么重新选择本地写作",
|
|
109
|
-
"slug": "local-first-writing",
|
|
110
106
|
"description": "Markdown 文件、可迁移的媒体资源,以及不被平台锁住的长期写作。",
|
|
111
107
|
"date": "2026-07-12T00:00:00.000Z",
|
|
112
108
|
"topic": "写作系统",
|
|
@@ -117,7 +113,7 @@ export const posts = [
|
|
|
117
113
|
"pinned": true,
|
|
118
114
|
"readingTime": "5 分钟",
|
|
119
115
|
"cover": "https://images.unsplash.com/photo-1501525771695-688643efeea4?auto=format&fit=crop&w=1600&q=84",
|
|
120
|
-
"
|
|
116
|
+
"slug": "local-first-writing",
|
|
121
117
|
"path": "local-first-writing",
|
|
122
118
|
"kind": "post",
|
|
123
119
|
"draft": false,
|
|
@@ -126,7 +122,6 @@ export const posts = [
|
|
|
126
122
|
},
|
|
127
123
|
{
|
|
128
124
|
"title": "02 · 让状态变得可靠",
|
|
129
|
-
"slug": "stable-state",
|
|
130
125
|
"description": "状态设计的关键,是明确谁拥有事实。",
|
|
131
126
|
"date": "2026-07-10T00:00:00.000Z",
|
|
132
127
|
"topic": "独立开发",
|
|
@@ -134,6 +129,7 @@ export const posts = [
|
|
|
134
129
|
"状态管理"
|
|
135
130
|
],
|
|
136
131
|
"readingTime": "6 分钟",
|
|
132
|
+
"slug": "stable-state",
|
|
137
133
|
"path": "building-in-public/stable-state",
|
|
138
134
|
"kind": "post",
|
|
139
135
|
"draft": false,
|
|
@@ -142,7 +138,6 @@ export const posts = [
|
|
|
142
138
|
},
|
|
143
139
|
{
|
|
144
140
|
"title": "01 · 从最小闭环开始",
|
|
145
|
-
"slug": "smallest-closed-loop",
|
|
146
141
|
"description": "先让最重要的一条路径真正跑通。",
|
|
147
142
|
"date": "2026-07-04T00:00:00.000Z",
|
|
148
143
|
"topic": "独立开发",
|
|
@@ -151,6 +146,7 @@ export const posts = [
|
|
|
151
146
|
"工程"
|
|
152
147
|
],
|
|
153
148
|
"readingTime": "7 分钟",
|
|
149
|
+
"slug": "smallest-closed-loop",
|
|
154
150
|
"path": "building-in-public/smallest-closed-loop",
|
|
155
151
|
"kind": "post",
|
|
156
152
|
"draft": false,
|
|
@@ -159,7 +155,6 @@ export const posts = [
|
|
|
159
155
|
},
|
|
160
156
|
{
|
|
161
157
|
"title": "做一个 PDF 阅读器之后,我重新理解了「位置」",
|
|
162
|
-
"slug": "building-a-pdf-reader",
|
|
163
158
|
"description": "阅读位置不是页码,而是一段连续体验的锚点。",
|
|
164
159
|
"date": "2026-06-28T00:00:00.000Z",
|
|
165
160
|
"topic": "构建手记",
|
|
@@ -170,7 +165,7 @@ export const posts = [
|
|
|
170
165
|
"pinned": false,
|
|
171
166
|
"readingTime": "8 分钟",
|
|
172
167
|
"cover": "https://images.unsplash.com/photo-1623276527153-fa38c1616b05?auto=format&fit=crop&w=1600&q=84",
|
|
173
|
-
"
|
|
168
|
+
"slug": "building-a-pdf-reader",
|
|
174
169
|
"path": "building-a-pdf-reader",
|
|
175
170
|
"kind": "post",
|
|
176
171
|
"draft": false,
|
|
@@ -179,7 +174,6 @@ export const posts = [
|
|
|
179
174
|
},
|
|
180
175
|
{
|
|
181
176
|
"title": "从逐词生成理解 KV Cache",
|
|
182
|
-
"slug": "understand-kv-cache",
|
|
183
177
|
"description": "不从公式开始,而是从模型下一次到底重复算了什么开始。",
|
|
184
178
|
"date": "2026-06-09T00:00:00.000Z",
|
|
185
179
|
"topic": "AI 学习",
|
|
@@ -190,7 +184,7 @@ export const posts = [
|
|
|
190
184
|
"pinned": false,
|
|
191
185
|
"readingTime": "9 分钟",
|
|
192
186
|
"cover": "https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=1600&q=84",
|
|
193
|
-
"
|
|
187
|
+
"slug": "understand-kv-cache",
|
|
194
188
|
"path": "understand-kv-cache",
|
|
195
189
|
"kind": "post",
|
|
196
190
|
"draft": false,
|
|
@@ -201,7 +195,6 @@ export const posts = [
|
|
|
201
195
|
export const columns = [
|
|
202
196
|
{
|
|
203
197
|
"title": "01 · 从最小闭环开始",
|
|
204
|
-
"slug": "smallest-closed-loop",
|
|
205
198
|
"description": "先让最重要的一条路径真正跑通。",
|
|
206
199
|
"date": "2026-07-04T00:00:00.000Z",
|
|
207
200
|
"topic": "独立开发",
|
|
@@ -210,6 +203,7 @@ export const columns = [
|
|
|
210
203
|
"工程"
|
|
211
204
|
],
|
|
212
205
|
"readingTime": "7 分钟",
|
|
206
|
+
"slug": "smallest-closed-loop",
|
|
213
207
|
"path": "building-in-public/smallest-closed-loop",
|
|
214
208
|
"kind": "column",
|
|
215
209
|
"draft": false,
|
|
@@ -223,7 +217,6 @@ export const columns = [
|
|
|
223
217
|
},
|
|
224
218
|
{
|
|
225
219
|
"title": "02 · 让状态变得可靠",
|
|
226
|
-
"slug": "stable-state",
|
|
227
220
|
"description": "状态设计的关键,是明确谁拥有事实。",
|
|
228
221
|
"date": "2026-07-10T00:00:00.000Z",
|
|
229
222
|
"topic": "独立开发",
|
|
@@ -231,6 +224,7 @@ export const columns = [
|
|
|
231
224
|
"状态管理"
|
|
232
225
|
],
|
|
233
226
|
"readingTime": "6 分钟",
|
|
227
|
+
"slug": "stable-state",
|
|
234
228
|
"path": "building-in-public/stable-state",
|
|
235
229
|
"kind": "column",
|
|
236
230
|
"draft": false,
|
|
@@ -244,7 +238,6 @@ export const columns = [
|
|
|
244
238
|
},
|
|
245
239
|
{
|
|
246
240
|
"title": "03 · 带着证据发布",
|
|
247
|
-
"slug": "ship-with-evidence",
|
|
248
241
|
"description": "“我改完了”不是完成,“用户可以确认它好了”才是。",
|
|
249
242
|
"date": "2026-07-18T00:00:00.000Z",
|
|
250
243
|
"topic": "独立开发",
|
|
@@ -253,6 +246,7 @@ export const columns = [
|
|
|
253
246
|
"发布"
|
|
254
247
|
],
|
|
255
248
|
"readingTime": "5 分钟",
|
|
249
|
+
"slug": "ship-with-evidence",
|
|
256
250
|
"path": "building-in-public/ship-with-evidence",
|
|
257
251
|
"kind": "column",
|
|
258
252
|
"draft": false,
|
|
@@ -268,7 +262,6 @@ export const columns = [
|
|
|
268
262
|
export const thoughts = [
|
|
269
263
|
{
|
|
270
264
|
"title": "雨后的城市",
|
|
271
|
-
"slug": "rainy-night",
|
|
272
265
|
"date": "2026-07-24T14:36:00.000Z",
|
|
273
266
|
"tags": [
|
|
274
267
|
"摄影"
|
|
@@ -307,6 +300,7 @@ export const thoughts = [
|
|
|
307
300
|
"description": "一条和这晚阅读有关、值得保留上下文的链接。"
|
|
308
301
|
}
|
|
309
302
|
],
|
|
303
|
+
"slug": "rainy-night",
|
|
310
304
|
"path": "rainy-night",
|
|
311
305
|
"kind": "thought",
|
|
312
306
|
"draft": false,
|
|
@@ -315,7 +309,6 @@ export const thoughts = [
|
|
|
315
309
|
},
|
|
316
310
|
{
|
|
317
311
|
"title": "今天的声音",
|
|
318
|
-
"slug": "field-recording",
|
|
319
312
|
"date": "2026-07-22T10:10:00.000Z",
|
|
320
313
|
"tags": [
|
|
321
314
|
"声音"
|
|
@@ -328,6 +321,7 @@ export const thoughts = [
|
|
|
328
321
|
"title": "今天的声音 · 循环片段"
|
|
329
322
|
}
|
|
330
323
|
],
|
|
324
|
+
"slug": "field-recording",
|
|
331
325
|
"path": "field-recording",
|
|
332
326
|
"kind": "thought",
|
|
333
327
|
"draft": false,
|
|
@@ -336,7 +330,6 @@ export const thoughts = [
|
|
|
336
330
|
},
|
|
337
331
|
{
|
|
338
332
|
"title": "慢一点看",
|
|
339
|
-
"slug": "slow-motion",
|
|
340
333
|
"date": "2026-07-19T01:20:00.000Z",
|
|
341
334
|
"tags": [
|
|
342
335
|
"影像"
|
|
@@ -349,6 +342,7 @@ export const thoughts = [
|
|
|
349
342
|
"poster": "https://images.unsplash.com/photo-1544655205-dcbeb868c648?auto=format&fit=crop&w=1400&q=82"
|
|
350
343
|
}
|
|
351
344
|
],
|
|
345
|
+
"slug": "slow-motion",
|
|
352
346
|
"path": "slow-motion",
|
|
353
347
|
"kind": "thought",
|
|
354
348
|
"draft": false,
|
|
@@ -357,7 +351,6 @@ export const thoughts = [
|
|
|
357
351
|
},
|
|
358
352
|
{
|
|
359
353
|
"title": "链接不是收藏,是路径",
|
|
360
|
-
"slug": "links-are-paths",
|
|
361
354
|
"date": "2026-07-16T04:05:00.000Z",
|
|
362
355
|
"tags": [
|
|
363
356
|
"阅读"
|
|
@@ -371,6 +364,7 @@ export const thoughts = [
|
|
|
371
364
|
"description": "让不同网站之间的回复、喜欢与引用重新连接起来。"
|
|
372
365
|
}
|
|
373
366
|
],
|
|
367
|
+
"slug": "links-are-paths",
|
|
374
368
|
"path": "links-are-paths",
|
|
375
369
|
"kind": "thought",
|
|
376
370
|
"draft": false,
|
|
@@ -72,17 +72,23 @@ function isMedia(value: unknown): value is MediaItem[] {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function
|
|
75
|
+
function fileStem(filePath: string) {
|
|
76
|
+
return (filePath.split("/").pop() ?? "").replace(/\.md$/u, "");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeThought(source: string, filePath: string, sha?: string): RepositoryThought | null {
|
|
76
80
|
const parsed = matter(source);
|
|
77
81
|
const data = parsed.data;
|
|
78
|
-
|
|
82
|
+
const slug = fileStem(filePath);
|
|
83
|
+
if (typeof data.title !== "string" || typeof data.date !== "string" || !slug) {
|
|
79
84
|
return null;
|
|
80
85
|
}
|
|
86
|
+
if (data.slug !== undefined && data.slug !== slug) return null;
|
|
81
87
|
const media = isMedia(data.media) ? data.media : [];
|
|
82
88
|
const status = data.draft === true ? "draft" : "published";
|
|
83
89
|
return {
|
|
84
|
-
id:
|
|
85
|
-
slug
|
|
90
|
+
id: slug,
|
|
91
|
+
slug,
|
|
86
92
|
title: data.title,
|
|
87
93
|
date: data.date,
|
|
88
94
|
tags: isStringArray(data.tags) ? data.tags : [],
|
|
@@ -90,7 +96,7 @@ function normalizeThought(source: string, path: string, sha?: string): Repositor
|
|
|
90
96
|
media,
|
|
91
97
|
kind: "thought",
|
|
92
98
|
draft: status === "draft",
|
|
93
|
-
sourcePath:
|
|
99
|
+
sourcePath: filePath,
|
|
94
100
|
content: parsed.content.trim(),
|
|
95
101
|
status,
|
|
96
102
|
sha,
|
|
@@ -108,7 +114,6 @@ function serializeThought(thought: {
|
|
|
108
114
|
}) {
|
|
109
115
|
return matter.stringify(thought.content.trim().concat("\n"), {
|
|
110
116
|
title: thought.title,
|
|
111
|
-
slug: thought.slug,
|
|
112
117
|
date: thought.date ?? new Date().toISOString(),
|
|
113
118
|
tags: thought.tags,
|
|
114
119
|
mediaType: thought.media[0]?.type ?? "text",
|
|
@@ -227,7 +232,7 @@ export async function updateRepositoryPostPinned(input: {
|
|
|
227
232
|
const existing = await readRepositoryFile(repositoryPath);
|
|
228
233
|
if (!existing) throw new Error("未找到对应博文。");
|
|
229
234
|
const parsed = matter(existing.source);
|
|
230
|
-
if (
|
|
235
|
+
if (fileStem(repositoryPath) !== input.slug) {
|
|
231
236
|
throw new Error("博文标识不匹配,已拒绝修改。");
|
|
232
237
|
}
|
|
233
238
|
const data = { ...parsed.data } as Record<string, unknown>;
|
package/app/rss.xml/route.ts
CHANGED
|
@@ -37,9 +37,7 @@ export async function GET() {
|
|
|
37
37
|
breaks: true,
|
|
38
38
|
});
|
|
39
39
|
const cover = entry.cover
|
|
40
|
-
? `<p><img src="${escapeXml(entry.cover)}" alt="${escapeXml(
|
|
41
|
-
entry.coverAlt ?? entry.title,
|
|
42
|
-
)}" /></p>`
|
|
40
|
+
? `<p><img src="${escapeXml(entry.cover)}" alt="${escapeXml(entry.title)}" /></p>`
|
|
43
41
|
: "";
|
|
44
42
|
return `
|
|
45
43
|
<item>
|
package/package.json
CHANGED
|
@@ -71,12 +71,13 @@ const isExternalAsset = (value) =>
|
|
|
71
71
|
|
|
72
72
|
const assetCache = new Map();
|
|
73
73
|
|
|
74
|
-
function copyLocalAsset(value, markdownPath, sourcePath) {
|
|
74
|
+
function copyLocalAsset(value, markdownPath, sourcePath, options = {}) {
|
|
75
75
|
if (!value || isExternalAsset(value)) return value;
|
|
76
76
|
|
|
77
|
+
const baseDir = options.baseDir ?? path.dirname(markdownPath);
|
|
77
78
|
const [assetPath, suffix = ""] = value.split(/(?=[?#])/u, 2);
|
|
78
79
|
const decodedPath = decodeURIComponent(assetPath.replace(/^<|>$/g, ""));
|
|
79
|
-
const absoluteSource = path.resolve(
|
|
80
|
+
const absoluteSource = path.resolve(baseDir, decodedPath);
|
|
80
81
|
const relativeSource = path.relative(contentRoot, absoluteSource);
|
|
81
82
|
|
|
82
83
|
if (
|
|
@@ -85,7 +86,7 @@ function copyLocalAsset(value, markdownPath, sourcePath) {
|
|
|
85
86
|
!fs.existsSync(absoluteSource) ||
|
|
86
87
|
!fs.statSync(absoluteSource).isFile()
|
|
87
88
|
) {
|
|
88
|
-
fail(sourcePath, `找不到本地媒体文件 ${value}`);
|
|
89
|
+
fail(sourcePath, options.missingMessage ?? `找不到本地媒体文件 ${value}`);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
if (!assetCache.has(absoluteSource)) {
|
|
@@ -254,9 +255,12 @@ function normalizeMedia(media, markdownPath, sourcePath) {
|
|
|
254
255
|
|
|
255
256
|
function validateAndNormalize(data, content, markdownPath, kind, sourcePath) {
|
|
256
257
|
const title = requireString(data, "title", sourcePath);
|
|
257
|
-
const slug =
|
|
258
|
+
const slug = path.basename(markdownPath, ".md");
|
|
258
259
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(slug)) {
|
|
259
|
-
fail(sourcePath, "
|
|
260
|
+
fail(sourcePath, "文件名只能包含小写字母、数字和连字符");
|
|
261
|
+
}
|
|
262
|
+
if (data.slug !== undefined) {
|
|
263
|
+
fail(sourcePath, "不要写 slug,文件名就是标识");
|
|
260
264
|
}
|
|
261
265
|
|
|
262
266
|
const date = normalizeDate(data.date, "date", sourcePath);
|
|
@@ -286,6 +290,7 @@ function validateAndNormalize(data, content, markdownPath, kind, sourcePath) {
|
|
|
286
290
|
|
|
287
291
|
const publishTime = Date.parse(publishAt ?? date);
|
|
288
292
|
if (!includeDrafts && (data.draft === true || publishTime > now)) return null;
|
|
293
|
+
delete data.coverAlt;
|
|
289
294
|
|
|
290
295
|
return {
|
|
291
296
|
...data,
|
|
@@ -301,7 +306,10 @@ function validateAndNormalize(data, content, markdownPath, kind, sourcePath) {
|
|
|
301
306
|
draft: Boolean(data.draft),
|
|
302
307
|
sourcePath,
|
|
303
308
|
cover: data.cover
|
|
304
|
-
? copyLocalAsset(data.cover, markdownPath, sourcePath
|
|
309
|
+
? copyLocalAsset(data.cover, markdownPath, sourcePath, {
|
|
310
|
+
baseDir: path.join(contentRoot, "covers"),
|
|
311
|
+
missingMessage: `找不到封面 ${data.cover};本地路径相对 content/covers`,
|
|
312
|
+
})
|
|
305
313
|
: undefined,
|
|
306
314
|
columnCover: data.columnCover
|
|
307
315
|
? copyLocalAsset(data.columnCover, markdownPath, sourcePath)
|
|
@@ -399,7 +407,7 @@ function readColumnReferences(posts) {
|
|
|
399
407
|
if (seenColumns.has(slug)) fail(sourcePath, `专栏 slug ${slug} 重复。`);
|
|
400
408
|
seenColumns.add(slug);
|
|
401
409
|
if (!Array.isArray(value.posts) || !value.posts.every((entry) => typeof entry === "string")) {
|
|
402
|
-
fail(sourcePath, "posts
|
|
410
|
+
fail(sourcePath, "posts 必须是博文相对路径数组。");
|
|
403
411
|
}
|
|
404
412
|
const referenced = new Set();
|
|
405
413
|
return value.posts.map((postReference, order) => {
|
|
@@ -487,7 +495,6 @@ export type ContentEntry = {
|
|
|
487
495
|
columnCoverAlt?: string;
|
|
488
496
|
order?: number;
|
|
489
497
|
cover?: string;
|
|
490
|
-
coverAlt?: string;
|
|
491
498
|
mediaType?: "image" | "audio" | "video" | "link" | "text";
|
|
492
499
|
media?: string | MediaItem[];
|
|
493
500
|
mediaAlt?: string;
|