@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +118 -2
  2. package/package.json +70 -5
  3. package/src/components/AlbumCard.astro +73 -0
  4. package/src/components/Analytics.astro +22 -0
  5. package/src/components/BaseLayout.astro +188 -0
  6. package/src/components/Calendar.astro +379 -0
  7. package/src/components/CategoryBar.astro +74 -0
  8. package/src/components/Comment.astro +48 -0
  9. package/src/components/EncryptedContent.astro +42 -0
  10. package/src/components/Footer.astro +31 -0
  11. package/src/components/Header.astro +153 -0
  12. package/src/components/License.astro +29 -0
  13. package/src/components/Live2DWidget.astro +262 -0
  14. package/src/components/MusicPlayer.astro +940 -0
  15. package/src/components/OutdatedCard.astro +53 -0
  16. package/src/components/Pagination.astro +94 -0
  17. package/src/components/PhotoCard.astro +22 -0
  18. package/src/components/PostCard.astro +89 -0
  19. package/src/components/PostNav.astro +40 -0
  20. package/src/components/RecommendedPost.astro +166 -0
  21. package/src/components/SakuraEffect.astro +152 -0
  22. package/src/components/Sidebar.astro +334 -0
  23. package/src/components/Toc.astro +27 -0
  24. package/src/components/analytics/GoogleAnalytics.astro +22 -0
  25. package/src/components/analytics/La51Analytics.astro +52 -0
  26. package/src/components/analytics/MicrosoftClarity.astro +18 -0
  27. package/src/components/analytics/UmamiAnalytics.astro +86 -0
  28. package/src/components/comments/Artalk.astro +51 -0
  29. package/src/components/comments/Disqus.astro +43 -0
  30. package/src/components/comments/Giscus.astro +50 -0
  31. package/src/components/comments/Twikoo.astro +65 -0
  32. package/src/components/comments/Waline.astro +36 -0
  33. package/src/i18n/en.ts +164 -0
  34. package/src/i18n/index.ts +18 -0
  35. package/src/i18n/resolve.ts +49 -0
  36. package/src/i18n/zh-CN.ts +184 -0
  37. package/src/icons.ts +71 -0
  38. package/src/index.ts +411 -0
  39. package/src/plugins/plantuml-encoder.ts +95 -0
  40. package/src/plugins/rehype-diagram.ts +218 -0
  41. package/src/plugins/rehype-ec-vt-rerun.ts +80 -0
  42. package/src/plugins/rehype-email.ts +75 -0
  43. package/src/plugins/rehype-github-card.ts +114 -0
  44. package/src/plugins/rehype.ts +284 -0
  45. package/src/plugins/remark-diagram.ts +78 -0
  46. package/src/plugins/remark-directives.ts +100 -0
  47. package/src/plugins/remark-excerpt.ts +29 -0
  48. package/src/plugins/remark-image-grid.ts +97 -0
  49. package/src/plugins/remark-reading-time.ts +38 -0
  50. package/src/plugins/scripts/diagram-panzoom.js +388 -0
  51. package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
  52. package/src/routes/404.astro +14 -0
  53. package/src/routes/_hydrate.ts +34 -0
  54. package/src/routes/about.astro +25 -0
  55. package/src/routes/api/allPostMeta.json.ts +26 -0
  56. package/src/routes/archive.astro +81 -0
  57. package/src/routes/categories/[category].astro +37 -0
  58. package/src/routes/categories/index.astro +29 -0
  59. package/src/routes/friends.astro +83 -0
  60. package/src/routes/gallery/[album].astro +97 -0
  61. package/src/routes/gallery/index.astro +100 -0
  62. package/src/routes/guestbook.astro +37 -0
  63. package/src/routes/index.astro +47 -0
  64. package/src/routes/og/[...slug].ts +281 -0
  65. package/src/routes/page/[page].astro +53 -0
  66. package/src/routes/posts/[...slug].astro +350 -0
  67. package/src/routes/robots.txt.ts +18 -0
  68. package/src/routes/rss.xml.ts +37 -0
  69. package/src/routes/series/[name].astro +66 -0
  70. package/src/routes/series/index.astro +64 -0
  71. package/src/routes/sponsor.astro +84 -0
  72. package/src/routes/tags/[tag].astro +40 -0
  73. package/src/routes/tags/index.astro +29 -0
  74. package/src/schema.ts +1218 -0
  75. package/src/scripts/app.ts +1041 -0
  76. package/src/scripts/share-poster.ts +468 -0
  77. package/src/styles/global.css +4864 -0
  78. package/src/utils/crypto.ts +46 -0
  79. package/src/utils/gallery.ts +76 -0
  80. package/src/utils/lqip.ts +18 -0
  81. package/src/utils/related.ts +101 -0
  82. package/src/utils.ts +233 -0
  83. package/src/vite.ts +105 -0
package/src/schema.ts ADDED
@@ -0,0 +1,1218 @@
1
+ import { z } from "astro/zod";
2
+
3
+ /**
4
+ * Live2D 看板娘(§4.38)单模型。提取为模块级 const:union 单/数组两分支复用。
5
+ * 注意 z.output 中 volume/scale/x/y 因 default 恒为 number。
6
+ */
7
+ const live2dModelSchema = z.object({
8
+ /** model.json 地址(public 路径或外链) */
9
+ path: z.string(),
10
+ /** 模型动作音量 0-1(默认 0 静音) */
11
+ volume: z.number().min(0).max(1).default(0),
12
+ /** 缩放 */
13
+ scale: z.number().default(1),
14
+ /** 水平偏移 -2~2(传给库的 offset[0]) */
15
+ x: z.number().min(-2).max(2).default(0),
16
+ /** 垂直偏移 -2~2(传给库的 offset[1]) */
17
+ y: z.number().min(-2).max(2).default(0),
18
+ });
19
+
20
+ /**
21
+ * 侧栏 widget 共享基字段(§4.51 配置统一):各 widget 分支 extend 复用。
22
+ * 公共配置(开关/显隐/位置)配置在数组项上;组件专属参数统一收进
23
+ * specificConfig 入口(§4.52,对齐参考项目形态):base 默认空对象,
24
+ * 有专属参数的类型分支覆写为专属形状。
25
+ */
26
+ const sidebarWidgetBase = z.object({
27
+ enabled: z.boolean().default(true),
28
+ /** 文章页侧栏是否保留该 widget(false = 仅首页/列表页显示) */
29
+ showOnPost: z.boolean().default(true),
30
+ /** 仅文章页侧栏显示(true = 首页/列表页不出现,如 toc) */
31
+ postOnly: z.boolean().default(false),
32
+ /** 移动端(<768px)隐藏该 widget(§4.49 移动端差异化侧栏) */
33
+ hideOnMobile: z.boolean().default(false),
34
+ /** 渲染分组:sticky 组包进粘性容器跟随滚动(≥768px 生效,§4.50);默认 top 普通流式 */
35
+ position: z.enum(["top", "sticky"]).default("top"),
36
+ /** 组件专属配置入口(§4.52):默认空对象,有专属参数的类型分支覆写为专属形状 */
37
+ specificConfig: z.object({}).default({}),
38
+ });
39
+
40
+ /**
41
+ * 主题配置 schema(astro.config.ts 中 defineTheme({...}) 的入参)
42
+ * 后续新增配置项一律给 default,保证老站点升级后无需改动配置
43
+ */
44
+ export const themeConfigSchema = z.object({
45
+ /**
46
+ * 站点名(§4.58 加默认值):不配置也能通过校验(真零配置可构建),
47
+ * 正式接入时仍应显式配置为自己的站点名
48
+ */
49
+ title: z.string().default("Orchid Blog"),
50
+ /** 站点描述(首页与 meta description 默认值) */
51
+ description: z.string().default(""),
52
+ /**
53
+ * 站点级 SEO keywords(§4.19):注入非文章页 <meta name="keywords">;
54
+ * 文章页由 frontmatter tags 覆盖。留空则不输出该 meta。
55
+ */
56
+ keywords: z.array(z.string()).default([]),
57
+ /** 导航栏 Logo(§4.20):标题左侧图片 URL(public 路径或外链),留空只渲染文字标题 */
58
+ logo: z.string().default(""),
59
+ /** 作者名 */
60
+ author: z.string().default(""),
61
+ /**
62
+ * 强调色(hex),建议带彩度的颜色。
63
+ * 构建期转 HSL 注入 --accent-h/s/l,全部派生色(accent/soft/ink 等)由 HSL 公式计算;
64
+ * 暗色模式下亮度按 html.dark 规则提亮。
65
+ */
66
+ accentColor: z
67
+ .string()
68
+ .regex(/^#[0-9a-fA-F]{6}$/, "accentColor 必须是 6 位 hex 颜色,如 #4a9d8f")
69
+ .default("#4a9d8f"),
70
+ /** 站点语言(<html lang>) */
71
+ lang: z.string().default("zh-CN"),
72
+ /**
73
+ * 主容器页宽(§4.32,px):注入 html 的 --page-width 变量。
74
+ * ≥1280px 屏 .shell 宽度取 min(92vw, 此值),其下全宽;文章页固定 1120px
75
+ * 阅读行宽不跟随;归档/时间线等窄列不受影响。
76
+ */
77
+ pageWidth: z.number().int().min(800).max(1920).default(1600),
78
+ /**
79
+ * 首页 banner(§4.42,hero+壁纸合并单配置):enabled 总开关,false 时图与文案都不渲染。
80
+ * src 配置时渲染 Header 下方下沉横幅(数组构建期随机选一张,零 JS)+ 黑蒙层;
81
+ * quote 配置时渲染 hero 文案——有 banner 叠加其上,无 banner 为列表上方独立区块。
82
+ */
83
+ banner: z
84
+ .object({
85
+ /** 总开关:false 时即使配了 src/quote 也不渲染 */
86
+ enabled: z.boolean().default(true),
87
+ /** 图源(public 路径或外链):单图 URL 或多图数组(构建期随机取一);不配置则无图 */
88
+ src: z.union([z.string(), z.array(z.string())]).optional(),
89
+ /** banner 高度(vh) */
90
+ height: z.number().int().min(10).max(90).default(32),
91
+ /** 黑蒙层不透明度(0-1),深浅模式同色,保证文字可读 */
92
+ dimOpacity: z.number().min(0).max(1).default(0.2),
93
+ /** 图片 object-position(center / center 30% 等) */
94
+ position: z.string().default("center"),
95
+ /** true 仅首页显示,false 全站显示 */
96
+ homeOnly: z.boolean().default(true),
97
+ /** 主句,<em> 标签内文字会以强调色渲染;留空不渲染 hero 文案 */
98
+ quote: z.string().default(""),
99
+ /** 副句 */
100
+ sub: z.string().default(""),
101
+ })
102
+ .default({
103
+ enabled: true,
104
+ height: 32,
105
+ dimOpacity: 0.2,
106
+ position: "center",
107
+ homeOnly: true,
108
+ quote: "",
109
+ sub: "",
110
+ }),
111
+ /**
112
+ * hue 调色面板(M3b-b5):导航栏调色按钮 + 预设色板 + hue 滑杆。
113
+ * 运行时覆写 --accent-h(s/l 保持配置值),持久化 localStorage('qy-hue')。
114
+ * 色板预设为代码级常量,不进 schema。
115
+ */
116
+ huePanel: z
117
+ .object({
118
+ enabled: z.boolean().default(true),
119
+ })
120
+ .default({ enabled: true }),
121
+ /** 文章列表 */
122
+ postList: z
123
+ .object({
124
+ /** 默认卡片形态:cover 图卡 / compact 紧凑行;文章 frontmatter.cardStyle 可按篇覆盖 */
125
+ style: z.enum(["cover", "compact"]).default("cover"),
126
+ /** 列表封面开关(§4.55):false 时列表卡不渲染封面(整体降级纯文字形态) */
127
+ showCover: z.boolean().default(true),
128
+ /** 每页文章数(首页分页) */
129
+ pageSize: z.number().int().min(1).default(10),
130
+ /** 无图文章的兜底封面图(可选;不配置则降级为纯文字卡) */
131
+ fallbackCover: z.string().optional(),
132
+ /**
133
+ * 随机封面图源(§4.33):apis 非空时,frontmatter 显式 image:"api" 的文章
134
+ * 从该列表取封面。URL 支持 {slug} 占位符(替换为文章 id),
135
+ * 并自动追加 ?v=<hash>(Java hashCode(slug)),同 slug 每次构建稳定同图。
136
+ * 客户端加载失败时按列表顺序依次重试,全部失败显示本地 fallbackCover。
137
+ */
138
+ randomCover: z
139
+ .object({
140
+ apis: z.array(z.string()).default([]),
141
+ })
142
+ .default({ apis: [] }),
143
+ /**
144
+ * 列表形态切换(§4.34→§4.55):true 时导航栏右侧出现列表/网格切换按钮(仅首页/列表页渲染),
145
+ * 访客选择持久化 localStorage('qy-postList');false 时访客不可切换,仅使用 defaultLayout。
146
+ */
147
+ allowSwitch: z.boolean().default(false),
148
+ /** 默认列表形态(§4.55):list 上下列表 / grid 网格;访客未手动切换过时的初始形态 */
149
+ defaultLayout: z.enum(["list", "grid"]).default("list"),
150
+ /** 网格模式最小列宽(px),实际列数由容器宽自适应 */
151
+ gridColumnWidth: z.number().int().min(220).max(600).default(320),
152
+ })
153
+ .default({ style: "cover", pageSize: 10 }),
154
+ /** 文章页目录 */
155
+ toc: z
156
+ .object({
157
+ enabled: z.boolean().default(true),
158
+ })
159
+ .default({ enabled: true }),
160
+ /** 分类导航栏(§4.15→§4.23):首页 post-list 与文章页 post-main 顶部渲染(与正文同列宽不全宽),关闭则全站不渲染 */
161
+ categoryBar: z
162
+ .object({
163
+ enabled: z.boolean().default(false),
164
+ })
165
+ .default({ enabled: false }),
166
+ /**
167
+ * 个人资料卡(§4.15):sidebar widgets 中 type:"profile" 时渲染。
168
+ * 头像 + 昵称 + 简介 + 社交链接行;links[].icon 填内置图标名(§4.59,如 "bilibili")
169
+ * 或内联 SVG 字符串(以 "<" 开头,向后兼容)。
170
+ */
171
+ profile: z
172
+ .object({
173
+ /** 头像图 URL(留空渲染昵称首字 fallback) */
174
+ avatar: z.string().default(""),
175
+ name: z.string().default(""),
176
+ bio: z.string().default(""),
177
+ /** 点击头像/昵称跳转(如 /about/),留空则不包链接 */
178
+ url: z.string().default(""),
179
+ links: z
180
+ .array(
181
+ z.object({
182
+ name: z.string(),
183
+ url: z.string(),
184
+ /**
185
+ * 图标(§4.65):内置简名(如 "github"/"qq"/"wechat"/"weibo"/"bilibili"/
186
+ * "xiaohongshu",完整清单见主题 README「社交图标」)、Iconify 全名
187
+ *("集:名",如 "simple-icons:bilibili",站点装对应 @iconify-json/* 即可扩展)
188
+ * 或内联 SVG 字符串(以 "<" 开头,兼容旧写法);
189
+ * 名字未命中时不渲染图标、只渲染文字。构建期内联,无运行时请求
190
+ */
191
+ icon: z.string().default(""),
192
+ /** true 时在图标旁显示名称文字 */
193
+ showName: z.boolean().default(false),
194
+ }),
195
+ )
196
+ .default([]),
197
+ })
198
+ .default({ avatar: "", name: "", bio: "", url: "", links: [] }),
199
+ /**
200
+ * 相关/随机文章推荐(P1-12):文章页 PostNav 后双栏卡。
201
+ * 相关栏构建期评分(标签/标题 Jaccard + 时间新鲜度 + 同分类),候选排除加密文章;
202
+ * 随机栏客户端 fetch /api/allPostMeta.json(加密文章不进随机池)。
203
+ */
204
+ relatedPosts: z
205
+ .object({
206
+ enabled: z.boolean().default(true),
207
+ /** 相关栏数量 */
208
+ count: z.number().int().min(0).max(10).default(5),
209
+ /** 随机栏数量 */
210
+ randomCount: z.number().int().min(0).max(10).default(5),
211
+ })
212
+ .default({ enabled: true, count: 5, randomCount: 5 }),
213
+ /**
214
+ * OG 分享图(P1-11,默认关):构建期为每篇文章生成 1200x630 分享图(/og/<slug>.png)。
215
+ * satori(对象树→SVG)+ sharp(SVG→PNG);Google Fonts 拉取失败时降级纯形状占位图,
216
+ * 构建不中断。网络受限环境可保持关闭或接受占位图。
217
+ */
218
+ og: z
219
+ .object({
220
+ enabled: z.boolean().default(false),
221
+ })
222
+ .default({ enabled: false }),
223
+ /**
224
+ * 分享海报(P1-13):文章页「生成分享海报」按钮,纯浏览器 canvas + qrcode 能力,
225
+ * 构建零开销(qrcode 动态 import 分包,点开才加载)。
226
+ */
227
+ sharePoster: z
228
+ .object({
229
+ enabled: z.boolean().default(true),
230
+ })
231
+ .default({ enabled: true }),
232
+ /**
233
+ * 版权声明(文章级 licenseName/licenseUrl 覆盖名称与链接、license: false 按篇关闭;
234
+ * enabled=false 全站关闭)。prefix/suffix 自定义声明文案,留空用语言包默认
235
+ * (「本文采用 … 许可,转载请注明出处与作者。」)。
236
+ */
237
+ license: z
238
+ .object({
239
+ enabled: z.boolean().default(true),
240
+ name: z.string().default("CC BY-NC-SA 4.0"),
241
+ url: z
242
+ .string()
243
+ .default("https://creativecommons.org/licenses/by-nc-sa/4.0/"),
244
+ /** 声明前缀文案(许可链接之前),留空用语言包默认 */
245
+ prefix: z.string().default(""),
246
+ /** 声明后缀文案(许可链接之后),留空用语言包默认 */
247
+ suffix: z.string().default(""),
248
+ })
249
+ .default({
250
+ enabled: true,
251
+ name: "CC BY-NC-SA 4.0",
252
+ url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
253
+ prefix: "",
254
+ suffix: "",
255
+ }),
256
+ /** 建站日期(YYYY-MM-DD,站点信息卡「已运行」;不配置则不显示该行) */
257
+ since: z.string().optional(),
258
+ /**
259
+ * 顶部导航(§4.16 扩展):item 可带 children 下拉与 external 外链标记;
260
+ * 配了 children 时 href 可省略(父项仅作下拉入口,点击展开/收起)。
261
+ */
262
+ nav: z
263
+ .array(
264
+ z.object({
265
+ label: z.string(),
266
+ href: z.string().url().or(z.string().startsWith("/")).optional(),
267
+ /** true = 外部链接,新窗口打开 */
268
+ external: z.boolean().default(false),
269
+ children: z
270
+ .array(
271
+ z.object({
272
+ label: z.string(),
273
+ href: z.string().url().or(z.string().startsWith("/")),
274
+ external: z.boolean().default(false),
275
+ }),
276
+ )
277
+ .default([]),
278
+ }),
279
+ )
280
+ .default([]),
281
+ /** 页脚自定义 HTML(§4.16→§4.57):备案号等,经 set:html 渲染在版权行上方 */
282
+ footer: z
283
+ .object({
284
+ customHtml: z.string().default(""),
285
+ })
286
+ .default({ customHtml: "" }),
287
+ /**
288
+ * 默认深浅模式(§4.16):仅作用户未选择时的初值,
289
+ * 切换后以 localStorage('qy-mode') 为准。
290
+ */
291
+ colorMode: z.enum(["light", "dark", "system"]).default("system"),
292
+ /**
293
+ * 代码高亮(§4.61):Expressive Code(Astro 生态标准方案)接管代码块渲染,
294
+ * 替代 §4.17 自研 shiki 管线与 §4.37 自研折叠。light/dark 为 EC 支持的 Shiki
295
+ * 主题名(EC 约定 [dark, light] 顺序加载),跟随站点深浅模式切换。
296
+ * fold:行数超过 lineThreshold 自动折叠为 previewLines 行预览;
297
+ * lineNumbers 行号 / languageBadge 语言徽章 / wrap 长行换行为 EC 插件与渲染开关。
298
+ * 注意 default 必须写显式完整字面量:Zod v4 的 .default(x) 在输入缺失时直接取 x、
299
+ * 不做二次 parse,内层字段默认值不会补齐。
300
+ */
301
+ codeTheme: z
302
+ .object({
303
+ light: z.string().default("github-light"),
304
+ dark: z.string().default("github-dark"),
305
+ fold: z
306
+ .object({
307
+ enabled: z.boolean().default(false),
308
+ lineThreshold: z.number().int().min(1).default(15),
309
+ previewLines: z.number().int().min(1).default(8),
310
+ defaultCollapsed: z.boolean().default(true),
311
+ })
312
+ .default({
313
+ enabled: false,
314
+ lineThreshold: 15,
315
+ previewLines: 8,
316
+ defaultCollapsed: true,
317
+ }),
318
+ lineNumbers: z.boolean().default(true),
319
+ languageBadge: z.boolean().default(false),
320
+ wrap: z.boolean().default(false),
321
+ })
322
+ .default({
323
+ light: "github-light",
324
+ dark: "github-dark",
325
+ fold: {
326
+ enabled: false,
327
+ lineThreshold: 15,
328
+ previewLines: 8,
329
+ defaultCollapsed: true,
330
+ },
331
+ lineNumbers: true,
332
+ languageBadge: false,
333
+ wrap: false,
334
+ }),
335
+ /** 独立页开关:关闭后主题不注入对应路由(路由完全不生成) */
336
+ pages: z
337
+ .object({
338
+ about: z.boolean().default(true),
339
+ friends: z.boolean().default(true),
340
+ guestbook: z.boolean().default(true),
341
+ sponsor: z.boolean().default(true),
342
+ gallery: z.boolean().default(true),
343
+ /** 专题(§4.31):frontmatter series 聚合页 /series/ 与 /series/[name]/ */
344
+ series: z.boolean().default(true),
345
+ })
346
+ .default({
347
+ about: true,
348
+ friends: true,
349
+ guestbook: true,
350
+ sponsor: true,
351
+ gallery: true,
352
+ series: true,
353
+ }),
354
+ /**
355
+ * 画廊/相册(M3b-b8)。相册数据纯配置驱动,照片两个来源:
356
+ * 1) site/public/gallery/<id>/ 目录扫描(图片文件按名排序,cover.* 优先做首图)
357
+ * 2) 同目录 urls.txt(每行一个远程图片 URL,# 开头为注释),追加在本地之后
358
+ * 加密相册(password 非空)经 EncryptedContent 构建期加密,slug 用相册 id。
359
+ */
360
+ gallery: z
361
+ .object({
362
+ /** 画廊页标题 */
363
+ title: z.string().default("画廊"),
364
+ /** 画廊页副标题描述 */
365
+ description: z.string().default(""),
366
+ albums: z
367
+ .array(
368
+ z.object({
369
+ /** 相册 id:对应 public/gallery/<id>/ 目录,同时是详情 URL 段与加密 slug */
370
+ id: z.string(),
371
+ name: z.string(),
372
+ description: z.string().default(""),
373
+ date: z.string().default(""),
374
+ location: z.string().default(""),
375
+ tags: z.array(z.string()).default([]),
376
+ /** 封面(public 路径或外链):留空依次取 cover.* / 第一张照片 */
377
+ cover: z.string().default(""),
378
+ /** 非空即加密相册 */
379
+ password: z.string().optional(),
380
+ /** 解锁提示(仅展示,不参与解密) */
381
+ passwordHint: z.string().optional(),
382
+ }),
383
+ )
384
+ .default([]),
385
+ /** 瀑布流最小列宽(px),≥640px 视口生效 */
386
+ columnWidth: z.number().int().min(120).default(240),
387
+ })
388
+ .default({ title: "画廊", description: "", albums: [], columnWidth: 240 }),
389
+ /**
390
+ * 友链数据源(决策 8):同时驱动友链页全量渲染与首页侧栏友链 widget。
391
+ * 头像 image 留空时以 title 首字渲染字头像兜底。
392
+ */
393
+ friends: z
394
+ .object({
395
+ /** 友链页标题 */
396
+ title: z.string().default("友链"),
397
+ /** 友链页副标题描述 */
398
+ description: z.string().default(""),
399
+ /** 页面级排序:weight 权重降序 / random 随机(构建期) */
400
+ sort: z.enum(["weight", "random"]).default("weight"),
401
+ /** 申请友链卡片(title 留空则整卡不渲染) */
402
+ apply: z
403
+ .object({
404
+ title: z.string().default(""),
405
+ description: z.string().default(""),
406
+ /** 申请入口链接(mailto 或表单页) */
407
+ link: z.string().default(""),
408
+ label: z.string().default("申请友链"),
409
+ })
410
+ .default({ title: "", description: "", link: "", label: "申请友链" }),
411
+ list: z
412
+ .array(
413
+ z.object({
414
+ title: z.string(),
415
+ site: z.string().url(),
416
+ /** 头像图(外链或 public 路径;留空用字头像) */
417
+ image: z.string().default(""),
418
+ desc: z.string().default(""),
419
+ tags: z.array(z.string()).default([]),
420
+ /** 权重,越大越靠前(sort=weight 时生效) */
421
+ weight: z.number().default(0),
422
+ enabled: z.boolean().default(true),
423
+ /** 首页/列表页侧栏友链 widget 优先展示(其余按 weight 补足) */
424
+ featured: z.boolean().default(false),
425
+ }),
426
+ )
427
+ .default([]),
428
+ })
429
+ .default({
430
+ title: "友链",
431
+ description: "",
432
+ sort: "weight",
433
+ apply: { title: "", description: "", link: "", label: "申请友链" },
434
+ list: [],
435
+ }),
436
+ /**
437
+ * 侧栏 widget 体系(P2 侧栏改造):
438
+ * - widgets 数组即渲染顺序,可自由增删/排序/开关;类型:
439
+ * announcement / categories / tags / friends / calendar / toc / siteInfo / advertisement / profile
440
+ * - 公共配置(开关/显隐/位置)配置在数组项上;组件专属参数统一收进 item 的
441
+ * specificConfig 入口(§4.52,对齐参考项目):tags.specificConfig.count、
442
+ * friends.specificConfig.count/random、calendar.specificConfig.postTooltip
443
+ * - enabled=false 跳过该 widget;两个方向的显隐(对齐参考模板 showOnPostPage/hideOnNonPostPage):
444
+ * showOnPost=false 仅列表页显示;postOnly=true 仅文章页侧栏显示(如 toc)
445
+ * - hideOnMobile=true 移动端(<768px)隐藏该 widget(§4.49 移动端差异化侧栏,默认 toc/calendar 隐藏)
446
+ * - position="sticky" 渲染进粘性容器,≥768px 跟随滚动(§4.50);默认 "top" 普通流式
447
+ * - onPost=false 时文章详情页不渲染侧栏(TOC 保持正文右侧原位)
448
+ */
449
+ sidebar: z
450
+ .object({
451
+ /** 文章详情页是否启用侧栏 */
452
+ onPost: z.boolean().default(false),
453
+ /** widget 渲染顺序 = 数组顺序;公共配置在数组项上,专属参数收进 specificConfig(§4.52) */
454
+ widgets: z
455
+ .array(
456
+ z.discriminatedUnion("type", [
457
+ sidebarWidgetBase.extend({ type: z.literal("announcement") }),
458
+ sidebarWidgetBase.extend({ type: z.literal("categories") }),
459
+ /**
460
+ * 标签 widget:specificConfig.count 为展示的热门标签数量(0 = 全部展示),
461
+ * 超出部分收进「…更多」入口跳转标签页(§4.48)
462
+ */
463
+ sidebarWidgetBase.extend({
464
+ type: z.literal("tags"),
465
+ specificConfig: z
466
+ .object({ count: z.number().int().min(0).default(12) })
467
+ .default({}),
468
+ }),
469
+ /**
470
+ * 友链 widget:specificConfig.count 展示数量;random=true 构建期随机取样,
471
+ * false 则 featured 优先、其余按 weight 降序取前 count 个
472
+ */
473
+ sidebarWidgetBase.extend({
474
+ type: z.literal("friends"),
475
+ specificConfig: z
476
+ .object({
477
+ count: z.number().int().min(1).default(3),
478
+ random: z.boolean().default(false),
479
+ })
480
+ .default({}),
481
+ }),
482
+ sidebarWidgetBase.extend({
483
+ type: z.literal("calendar"),
484
+ specificConfig: z
485
+ .object({
486
+ /** 日历下方文章列表条目悬停显示全文 title(§4.41);默认关 */
487
+ postTooltip: z.boolean().default(false),
488
+ })
489
+ .default({}),
490
+ }),
491
+ sidebarWidgetBase.extend({ type: z.literal("toc") }),
492
+ sidebarWidgetBase.extend({ type: z.literal("siteInfo") }),
493
+ sidebarWidgetBase.extend({ type: z.literal("advertisement") }),
494
+ sidebarWidgetBase.extend({ type: z.literal("profile") }),
495
+ ]),
496
+ )
497
+ .default([
498
+ { type: "announcement" },
499
+ { type: "categories", position: "sticky" },
500
+ { type: "tags", position: "sticky" },
501
+ { type: "friends" },
502
+ { type: "calendar", hideOnMobile: true, position: "sticky" },
503
+ {
504
+ type: "toc",
505
+ postOnly: true,
506
+ hideOnMobile: true,
507
+ position: "sticky",
508
+ },
509
+ { type: "siteInfo" },
510
+ { type: "advertisement" },
511
+ ]),
512
+ })
513
+ .default({
514
+ onPost: false,
515
+ widgets: [
516
+ { type: "announcement" },
517
+ { type: "categories", position: "sticky" },
518
+ { type: "tags", position: "sticky" },
519
+ { type: "friends" },
520
+ { type: "calendar", hideOnMobile: true, position: "sticky" },
521
+ { type: "toc", postOnly: true, hideOnMobile: true, position: "sticky" },
522
+ { type: "siteInfo" },
523
+ { type: "advertisement" },
524
+ ],
525
+ }),
526
+ /** 打赏页(收款码 image 与跳转 link 至少配一个;均为空渲染占位框) */
527
+ sponsor: z
528
+ .object({
529
+ title: z.string().default("打赏"),
530
+ description: z.string().default(""),
531
+ /** 打赏用途说明(留空不渲染) */
532
+ usage: z.string().default(""),
533
+ showSponsorsList: z.boolean().default(true),
534
+ /** 文章详情页是否渲染「打赏支持」入口(还需 pages.sponsor 开启) */
535
+ showButtonInPost: z.boolean().default(true),
536
+ methods: z
537
+ .array(
538
+ z.object({
539
+ name: z.string(),
540
+ /** 文本/emoji 角标(主题不内置图标依赖) */
541
+ icon: z.string().default(""),
542
+ /** 收款码图片(public 路径或外链) */
543
+ image: z.string().default(""),
544
+ /** 跳转链接(爱发电/ko-fi 等) */
545
+ link: z.string().default(""),
546
+ description: z.string().default(""),
547
+ enabled: z.boolean().default(true),
548
+ }),
549
+ )
550
+ .default([]),
551
+ /** 打赏者名单(date 用 YYYY-MM-DD) */
552
+ sponsors: z
553
+ .array(
554
+ z.object({
555
+ name: z.string(),
556
+ avatar: z.string().default(""),
557
+ amount: z.string().default(""),
558
+ date: z.string().default(""),
559
+ }),
560
+ )
561
+ .default([]),
562
+ })
563
+ .default({
564
+ title: "打赏",
565
+ description: "",
566
+ usage: "",
567
+ showSponsorsList: true,
568
+ showButtonInPost: true,
569
+ methods: [],
570
+ sponsors: [],
571
+ }),
572
+ /**
573
+ * 评论系统(适配层分发,五套可切换:artalk/twikoo/waline/giscus/disqus)。
574
+ * enabled=false 或 provider='none' 时页面完全不渲染评论区;
575
+ * 文章另受 frontmatter comment 字段按篇控制。
576
+ */
577
+ comment: z
578
+ .object({
579
+ enabled: z.boolean().default(false),
580
+ provider: z
581
+ .enum(["none", "artalk", "twikoo", "waline", "giscus", "disqus"])
582
+ .default("none"),
583
+ /** 评论区标题 */
584
+ title: z.string().default("评论"),
585
+ artalk: z
586
+ .object({
587
+ /** Artalk 后端地址 */
588
+ server: z.string().default(""),
589
+ locale: z.string().default("zh-CN"),
590
+ /** 文章访问量统计(Artalk pageview,挂载点 #artalk-pv 由站点自行放置) */
591
+ visitorCount: z.boolean().default(true),
592
+ })
593
+ .default({ server: "", locale: "zh-CN", visitorCount: true }),
594
+ twikoo: z
595
+ .object({
596
+ /** Twikoo 云函数/后端地址(Vercel/Netlify 部署 URL 或腾讯云环境 ID) */
597
+ envId: z.string().default(""),
598
+ /** 前端脚本地址(默认 npmmirror CDN,可换自部署或 jsdelivr) */
599
+ jsUrl: z
600
+ .string()
601
+ .default(
602
+ "https://registry.npmmirror.com/twikoo/1.7.13/files/dist/twikoo.min.js",
603
+ ),
604
+ /** 自定义样式表(留空不加载) */
605
+ cssUrl: z.string().default(""),
606
+ })
607
+ .default({
608
+ envId: "",
609
+ jsUrl:
610
+ "https://registry.npmmirror.com/twikoo/1.7.13/files/dist/twikoo.min.js",
611
+ cssUrl: "",
612
+ }),
613
+ waline: z
614
+ .object({
615
+ /** Waline 服务端地址(如 https://waline.example.com) */
616
+ serverURL: z.string().default(""),
617
+ lang: z.string().default("zh-CN"),
618
+ /** 文章访问量统计(Waline pageview) */
619
+ visitorCount: z.boolean().default(true),
620
+ })
621
+ .default({ serverURL: "", lang: "zh-CN", visitorCount: true }),
622
+ giscus: z
623
+ .object({
624
+ repo: z.string().default(""),
625
+ repoId: z.string().default(""),
626
+ category: z.string().default(""),
627
+ categoryId: z.string().default(""),
628
+ /** giscus.app 生成参数,默认值与官方一致 */
629
+ mapping: z.string().default("pathname"),
630
+ strict: z.string().default("0"),
631
+ reactionsEnabled: z.string().default("1"),
632
+ emitMetadata: z.string().default("0"),
633
+ inputPosition: z.string().default("top"),
634
+ lang: z.string().default("zh-CN"),
635
+ loading: z.string().default("lazy"),
636
+ })
637
+ .default({
638
+ repo: "",
639
+ repoId: "",
640
+ category: "",
641
+ categoryId: "",
642
+ mapping: "pathname",
643
+ strict: "0",
644
+ reactionsEnabled: "1",
645
+ emitMetadata: "0",
646
+ inputPosition: "top",
647
+ lang: "zh-CN",
648
+ loading: "lazy",
649
+ }),
650
+ disqus: z
651
+ .object({
652
+ /** Disqus shortname(站点 shortname.disqus.com) */
653
+ shortname: z.string().default(""),
654
+ })
655
+ .default({ shortname: "" }),
656
+ })
657
+ .default({
658
+ enabled: false,
659
+ provider: "none",
660
+ title: "评论",
661
+ artalk: { server: "", locale: "zh-CN", visitorCount: true },
662
+ twikoo: {
663
+ envId: "",
664
+ jsUrl:
665
+ "https://registry.npmmirror.com/twikoo/1.7.13/files/dist/twikoo.min.js",
666
+ cssUrl: "",
667
+ },
668
+ waline: { serverURL: "", lang: "zh-CN", visitorCount: true },
669
+ giscus: {
670
+ repo: "",
671
+ repoId: "",
672
+ category: "",
673
+ categoryId: "",
674
+ mapping: "pathname",
675
+ strict: "0",
676
+ reactionsEnabled: "1",
677
+ emitMetadata: "0",
678
+ inputPosition: "top",
679
+ lang: "zh-CN",
680
+ loading: "lazy",
681
+ },
682
+ disqus: { shortname: "" },
683
+ }),
684
+ /**
685
+ * 文章过时提醒(orchid-blog showLastModified/outdatedThreshold 对齐)。
686
+ * 距最后更新(updated ?? published)超过 threshold 天时,文章页显示
687
+ * 「最后更新」卡片与过时警告;天数由客户端计算(静态站构建期计算会随时间失效)。
688
+ */
689
+ outdated: z
690
+ .object({
691
+ enabled: z.boolean().default(true),
692
+ /** 过期阈值(天):距最后更新超过该天数才显示卡片 */
693
+ threshold: z.number().int().min(1).default(30),
694
+ })
695
+ .default({ enabled: true, threshold: 30 }),
696
+ /**
697
+ * 侧栏公告(P1-17)。content 留空则整卡不渲染;
698
+ * closable 仅前台隐藏(无持久化,刷新恢复)。
699
+ */
700
+ announcement: z
701
+ .object({
702
+ title: z.string().default("公告"),
703
+ content: z.string().default(""),
704
+ link: z
705
+ .object({
706
+ text: z.string().default(""),
707
+ url: z.string().default(""),
708
+ external: z.boolean().default(false),
709
+ })
710
+ .default({ text: "", url: "", external: false }),
711
+ closable: z.boolean().default(false),
712
+ })
713
+ .default({
714
+ title: "公告",
715
+ content: "",
716
+ link: { text: "", url: "", external: false },
717
+ closable: false,
718
+ }),
719
+ /**
720
+ * 侧栏广告位(P2 保留项,默认关闭)。enabled=false 整卡不渲染;
721
+ * expireDate(YYYY-MM-DD)过期不渲染;displayCount>0 时按 localStorage 计数超限隐藏。
722
+ */
723
+ advertisement: z
724
+ .object({
725
+ enabled: z.boolean().default(false),
726
+ title: z.string().default("广告"),
727
+ /** 广告图(public 路径或外链,留空跳过) */
728
+ image: z.string().default(""),
729
+ /** 点击广告图跳转的链接(留空则图不可点) */
730
+ imageLink: z.string().default(""),
731
+ imageAlt: z.string().default("广告"),
732
+ /** 文本内容(留空跳过) */
733
+ content: z.string().default(""),
734
+ /** 底部链接按钮(text 留空不渲染) */
735
+ link: z
736
+ .object({
737
+ text: z.string().default(""),
738
+ url: z.string().default(""),
739
+ external: z.boolean().default(true),
740
+ })
741
+ .default({ text: "", url: "", external: true }),
742
+ closable: z.boolean().default(true),
743
+ /** 最大展示次数;-1 不限制 */
744
+ displayCount: z.number().int().default(-1),
745
+ /** 过期日期(YYYY-MM-DD),过期不渲染 */
746
+ expireDate: z.string().optional(),
747
+ })
748
+ .default({
749
+ enabled: false,
750
+ title: "广告",
751
+ image: "",
752
+ imageLink: "",
753
+ imageAlt: "广告",
754
+ content: "",
755
+ link: { text: "", url: "", external: true },
756
+ closable: true,
757
+ displayCount: -1,
758
+ }),
759
+ /**
760
+ * 站点统计(P1-16):GA / Clarity / Umami / 51la 四选多。
761
+ * 对应 ID 留空即不注入该统计脚本,全部留空零副作用。
762
+ */
763
+ analytics: z
764
+ .object({
765
+ /** Google Analytics 衡量 ID(G-XXXX) */
766
+ googleAnalyticsId: z.string().default(""),
767
+ /** Microsoft Clarity 项目 ID */
768
+ microsoftClarityId: z.string().default(""),
769
+ umami: z
770
+ .object({
771
+ websiteId: z.string().default(""),
772
+ scriptUrl: z.string().default("https://cloud.umami.is/script.js"),
773
+ /** Umami Live 录制(replays)脚本地址 */
774
+ replaysScriptUrl: z
775
+ .string()
776
+ .default("https://cloud.umami.is/script.replays.js"),
777
+ /** 外链自动标记 data-umami-event 上报 */
778
+ trackOutboundLinks: z.boolean().default(true),
779
+ /** Web Vitals 采集(自建 umami 需 v2.9+) */
780
+ collectWebVitals: z.boolean().default(false),
781
+ replays: z
782
+ .object({
783
+ enabled: z.boolean().default(false),
784
+ sampleRate: z.number().min(0).max(1).default(0.15),
785
+ maskLevel: z.enum(["moderate", "strict"]).default("moderate"),
786
+ /** 单次录制最长毫秒 */
787
+ maxDuration: z.number().int().default(300000),
788
+ /** 录制屏蔽选择器(隐私元素) */
789
+ blockSelector: z.string().default(""),
790
+ })
791
+ .default({
792
+ enabled: false,
793
+ sampleRate: 0.15,
794
+ maskLevel: "moderate",
795
+ maxDuration: 300000,
796
+ blockSelector: "",
797
+ }),
798
+ })
799
+ .default({
800
+ websiteId: "",
801
+ scriptUrl: "https://cloud.umami.is/script.js",
802
+ replaysScriptUrl: "https://cloud.umami.is/script.replays.js",
803
+ trackOutboundLinks: true,
804
+ collectWebVitals: false,
805
+ replays: {
806
+ enabled: false,
807
+ sampleRate: 0.15,
808
+ maskLevel: "moderate",
809
+ maxDuration: 300000,
810
+ blockSelector: "",
811
+ },
812
+ }),
813
+ la51: z
814
+ .object({
815
+ /** 51.la 站点 ID(CK 编码) */
816
+ id: z.string().default(""),
817
+ /** 私有部署 SDK 地址(留空用官方) */
818
+ sdkUrl: z.string().default(""),
819
+ ck: z.string().default(""),
820
+ autoTrack: z.boolean().default(true),
821
+ hashMode: z.boolean().default(false),
822
+ screenRecord: z.boolean().default(true),
823
+ })
824
+ .default({
825
+ id: "",
826
+ sdkUrl: "",
827
+ ck: "",
828
+ autoTrack: true,
829
+ hashMode: false,
830
+ screenRecord: true,
831
+ }),
832
+ })
833
+ .default({
834
+ googleAnalyticsId: "",
835
+ microsoftClarityId: "",
836
+ umami: {
837
+ websiteId: "",
838
+ scriptUrl: "https://cloud.umami.is/script.js",
839
+ replaysScriptUrl: "https://cloud.umami.is/script.replays.js",
840
+ trackOutboundLinks: true,
841
+ collectWebVitals: false,
842
+ replays: {
843
+ enabled: false,
844
+ sampleRate: 0.15,
845
+ maskLevel: "moderate",
846
+ maxDuration: 300000,
847
+ blockSelector: "",
848
+ },
849
+ },
850
+ la51: {
851
+ id: "",
852
+ sdkUrl: "",
853
+ ck: "",
854
+ autoTrack: true,
855
+ hashMode: false,
856
+ screenRecord: true,
857
+ },
858
+ }),
859
+ /* ---------- LaTeX 数学公式(M3b-b4) ---------- */
860
+ latex: z
861
+ .object({
862
+ /** 总开关:false 时 $...$ / $$...$$ 按普通文本渲染,不注入 katex 资源 */
863
+ enabled: z.boolean().default(false),
864
+ })
865
+ .default({ enabled: false }),
866
+ /* ---------- 图表(M3b-b4):Mermaid 构建期双主题静态渲染 + PlantUML 外部服务 ---------- */
867
+ diagram: z
868
+ .object({
869
+ mermaid: z
870
+ .object({
871
+ enabled: z.boolean().default(false),
872
+ /** beautiful-mermaid 主题名(THEMES 键),浅/深各渲染一份,暗色模式切换显隐 */
873
+ lightTheme: z.string().default("github-light"),
874
+ darkTheme: z.string().default("github-dark"),
875
+ })
876
+ .default({
877
+ enabled: false,
878
+ lightTheme: "github-light",
879
+ darkTheme: "github-dark",
880
+ }),
881
+ plantuml: z
882
+ .object({
883
+ enabled: z.boolean().default(false),
884
+ /** PlantUML 渲染服务地址(图片由浏览器访问该服务加载,自建/内网在此替换) */
885
+ server: z.string().default("https://www.plantuml.com/plantuml"),
886
+ /** 浅/深主题名(空 = 尊重图内 !theme 指令;深色留空复用浅色 URL) */
887
+ lightTheme: z.string().default(""),
888
+ darkTheme: z.string().default(""),
889
+ })
890
+ .default({
891
+ enabled: false,
892
+ server: "https://www.plantuml.com/plantuml",
893
+ lightTheme: "",
894
+ darkTheme: "",
895
+ }),
896
+ })
897
+ .default({
898
+ mermaid: {
899
+ enabled: false,
900
+ lightTheme: "github-light",
901
+ darkTheme: "github-dark",
902
+ },
903
+ plantuml: {
904
+ enabled: false,
905
+ server: "https://www.plantuml.com/plantuml",
906
+ lightTheme: "",
907
+ darkTheme: "",
908
+ },
909
+ }),
910
+ /* ---------- 邮箱防爬(M3b-b4):mailto 构建期编码,点击时内联脚本还原 ---------- */
911
+ emailProtection: z
912
+ .object({
913
+ enabled: z.boolean().default(false),
914
+ /** base64:抗爬性更好;rot13:零 atob 依赖 */
915
+ method: z.enum(["base64", "rot13"]).default("base64"),
916
+ })
917
+ .default({ enabled: false, method: "base64" }),
918
+ /**
919
+ * favicon 图标列表(§4.30):数组非空时替换默认 /favicon.svg;
920
+ * theme 指定 "light"/"dark" 时按系统深浅色经 media 切换(可配双图标)。
921
+ */
922
+ favicon: z
923
+ .array(
924
+ z.object({
925
+ src: z.string(),
926
+ type: z.string().optional(),
927
+ sizes: z.string().optional(),
928
+ /** "light"/"dark":按 prefers-color-scheme 生效 */
929
+ theme: z.enum(["light", "dark"]).optional(),
930
+ }),
931
+ )
932
+ .default([]),
933
+ /**
934
+ * 构建期日期时区(IANA 名,如 "Asia/Shanghai",§4.30):
935
+ * formatDate 等输出与构建机时区解耦。留空跟随构建环境。
936
+ */
937
+ timezone: z.string().optional(),
938
+ /**
939
+ * 归档页按年折叠(§4.30 对齐参考站 foldArticle):开启时最新一年默认展开,其余折叠。
940
+ */
941
+ archive: z
942
+ .object({
943
+ fold: z.boolean().default(false),
944
+ })
945
+ .default({ fold: false }),
946
+ /**
947
+ * 樱花飘落特效(§4.30):全站 canvas 粒子(程序化花瓣,零贴图资产),
948
+ * prefers-reduced-motion 自动停用,页签隐藏暂停。
949
+ */
950
+ sakura: z
951
+ .object({
952
+ enabled: z.boolean().default(false),
953
+ /** 同屏花瓣数 */
954
+ density: z.number().int().min(1).max(200).default(24),
955
+ })
956
+ .default({ enabled: false, density: 24 }),
957
+ /**
958
+ * 音乐播放器(§4.36):local 模式,自研播放内核(audio + LRC 解析),
959
+ * 根节点 transition:persist 跨页不断播。meting 在线模式预留后续扩展。
960
+ */
961
+ music: z
962
+ .object({
963
+ enabled: z.boolean().default(false),
964
+ /** 悬浮位置:left 左下 / right 右下 */
965
+ position: z.enum(["left", "right"]).default("left"),
966
+ /** 初始音量 0-1(访客调整后记忆于 localStorage) */
967
+ volume: z.number().min(0).max(1).default(0.7),
968
+ /** 循环模式:list 列表循环 / one 单曲循环 / random 随机 */
969
+ playMode: z.enum(["list", "one", "random"]).default("list"),
970
+ /** 是否显示歌词开关按钮 */
971
+ showLyrics: z.boolean().default(true),
972
+ /** 本地播放列表 */
973
+ playlist: z
974
+ .array(
975
+ z.object({
976
+ /** 曲名 */
977
+ name: z.string(),
978
+ artist: z.string().default(""),
979
+ /** 音频地址(public 路径或外链) */
980
+ url: z.string(),
981
+ /** 封面图(可选,缺省渲染唱片图标) */
982
+ cover: z.string().optional(),
983
+ /** 歌词:.lrc 文件路径或内联 LRC 文本(可选,缺省无歌词) */
984
+ lrc: z.string().optional(),
985
+ }),
986
+ )
987
+ .default([]),
988
+ })
989
+ .default({
990
+ enabled: false,
991
+ position: "left",
992
+ volume: 0.7,
993
+ playMode: "list",
994
+ showLyrics: true,
995
+ playlist: [],
996
+ }),
997
+ /**
998
+ * Live2D 看板娘(§4.38):l2d-widget 悬浮模型 + 菜单 + 气泡说话。
999
+ * 根节点 transition:persist 跨页常驻;enabled 且配置了 model 才渲染。
1000
+ * zIndex 默认 240(低于返回顶部 250 / 音乐悬浮球 255,避免角落按钮被挡)。
1001
+ */
1002
+ live2d: z
1003
+ .object({
1004
+ enabled: z.boolean().default(false),
1005
+ /** 模型:单对象或数组(数组时可经菜单 action=switchModel 轮换) */
1006
+ model: z
1007
+ .union([live2dModelSchema, z.array(live2dModelSchema).min(1)])
1008
+ .optional(),
1009
+ /** 悬浮位置 */
1010
+ position: z.enum(["bottom-left", "bottom-right"]).default("bottom-left"),
1011
+ /** 画布尺寸(px):单一数字(正方形边长)或 {width,height} */
1012
+ size: z
1013
+ .union([
1014
+ z.number().int().min(100).max(600),
1015
+ z.object({
1016
+ width: z.number().int().min(100).max(600),
1017
+ height: z.number().int().min(100).max(600),
1018
+ }),
1019
+ ])
1020
+ .default(280),
1021
+ /** 菜单/气泡等 UI 主题色(CSS 颜色或 CSS 变量),留空用库默认 */
1022
+ primaryColor: z.string().default(""),
1023
+ /** 入场/退场动画时长(ms) */
1024
+ transitionDuration: z.number().int().min(0).optional(),
1025
+ /** 入场/退场动画类型 */
1026
+ transitionType: z.enum(["slide", "fade"]).optional(),
1027
+ /** 悬浮菜单(覆盖默认三键:主页/回顶部/休眠);action 内置
1028
+ * home/scrollToTop/sleep/switchModel,或 "link" + url 跳外链 */
1029
+ menus: z
1030
+ .object({
1031
+ items: z
1032
+ .array(
1033
+ z.object({
1034
+ /** 图标(iconify 名称,如 mdi:home) */
1035
+ icon: z.string().default(""),
1036
+ label: z.string(),
1037
+ action: z.string(),
1038
+ /** action=link 时的跳转地址 */
1039
+ url: z.string().optional(),
1040
+ }),
1041
+ )
1042
+ .default([]),
1043
+ align: z.enum(["left", "right"]).default("right"),
1044
+ })
1045
+ .optional(),
1046
+ /** 气泡说话:欢迎语与循环提示;整体不配置用库默认,enable:false 显式关闭 */
1047
+ tips: z
1048
+ .object({
1049
+ enable: z.boolean().default(true),
1050
+ welcomeMessage: z.array(z.string()).default([]),
1051
+ messages: z.array(z.string()).default([]),
1052
+ /** 单条气泡显示时长(ms) */
1053
+ duration: z.number().int().min(500).default(3000),
1054
+ /** 轮换间隔(ms) */
1055
+ interval: z.number().int().min(500).default(6000),
1056
+ /** 气泡相对模型的像素偏移 */
1057
+ offset: z
1058
+ .object({ x: z.number().default(0), y: z.number().default(0) })
1059
+ .default({ x: 0, y: 0 }),
1060
+ /** 打字机效果参数(透传库) */
1061
+ typing: z
1062
+ .object({
1063
+ param: z.string().optional(),
1064
+ speed: z.number().int().optional(),
1065
+ minValue: z.number().int().optional(),
1066
+ maxValue: z.number().int().optional(),
1067
+ })
1068
+ .optional(),
1069
+ })
1070
+ .optional(),
1071
+ /** 移动端按断点隐藏(隐藏态不加载模型,转桌面端自动拉起) */
1072
+ responsive: z
1073
+ .object({
1074
+ hideOnMobile: z.boolean().default(true),
1075
+ mobileBreakpoint: z.number().int().min(320).default(768),
1076
+ })
1077
+ .default({ hideOnMobile: true, mobileBreakpoint: 768 }),
1078
+ /** 容器层级 */
1079
+ zIndex: z.number().int().min(1).default(240),
1080
+ })
1081
+ // Zod v4 default 不二次 parse,必须写显式完整字面量
1082
+ .default({
1083
+ enabled: false,
1084
+ position: "bottom-left",
1085
+ size: 280,
1086
+ primaryColor: "",
1087
+ responsive: { hideOnMobile: true, mobileBreakpoint: 768 },
1088
+ zIndex: 240,
1089
+ }),
1090
+ /**
1091
+ * 字体(§4.66,Astro Fonts API 标准方案):声明式接入字体,主题负责把
1092
+ * 本段映射进 astro 顶层 fonts 配置(provider 字符串经 fontProviders 工厂
1093
+ * 转为 provider 对象)并在 <head> 渲染 <Font> 组件产出 @font-face 与变量。
1094
+ * role 把字体变量映射到主题三角色(--font-body/--font-display/--font-mono)。
1095
+ * 留空数组不注入不渲染,零开销。
1096
+ */
1097
+ fonts: z
1098
+ .array(
1099
+ z.object({
1100
+ /** 字体族名(provider 侧标识,如 "Inter";local 为自定义名) */
1101
+ name: z.string(),
1102
+ /** CSS 变量名(如 "--font-inter"):Font 组件据此产出变量与 @font-face */
1103
+ cssVariable: z
1104
+ .string()
1105
+ .regex(/^--[a-zA-Z0-9-]+$/, "必须以 -- 开头且仅含字母/数字/连字符"),
1106
+ /** provider:六种标准来源 */
1107
+ provider: z.enum([
1108
+ "google",
1109
+ "fontsource",
1110
+ "local",
1111
+ "bunny",
1112
+ "fontshare",
1113
+ "npm",
1114
+ ]),
1115
+ /** 字重(默认 [400];Astro 只下载列出的字重档) */
1116
+ weights: z
1117
+ .array(z.number().int().min(1).max(1000))
1118
+ .min(1)
1119
+ .default([400]),
1120
+ /** 字形(默认 normal + italic) */
1121
+ styles: z
1122
+ .array(z.enum(["normal", "italic", "oblique"]))
1123
+ .min(1)
1124
+ .default(["normal", "italic"]),
1125
+ /** 语言子集(按子集拆分字体文件,默认 latin) */
1126
+ subsets: z.array(z.string()).min(1).default(["latin"]),
1127
+ /** 回退字体族(Astro 据此产出优化的 fallback @font-face) */
1128
+ fallbacks: z.array(z.string()).default(["sans-serif"]),
1129
+ /** font-display 行为,不传用 Astro 默认 */
1130
+ display: z
1131
+ .enum(["auto", "swap", "block", "metadata", "fallback"])
1132
+ .optional(),
1133
+ /** 是否 <link rel=preload> 预载(主题层字段,正文首屏字体建议开启) */
1134
+ preload: z.boolean().default(false),
1135
+ /**
1136
+ * 角色映射(主题层字段):body 正文 / display 标题 / mono 代码。
1137
+ * 配置后 <head> 产出 html:root 高优先级规则,把对应角色变量
1138
+ * 重定向为 var(本字体变量), 原角色回退栈(中文等未覆盖字形自然回落)。
1139
+ */
1140
+ role: z.enum(["body", "display", "mono"]).optional(),
1141
+ /** provider=local 专属:本地字体文件声明 */
1142
+ options: z
1143
+ .object({
1144
+ variants: z
1145
+ .array(
1146
+ z.object({
1147
+ /** 字体文件路径(woff2 等,相对站点项目根或包内路径) */
1148
+ src: z.string(),
1149
+ /** 该文件的字重(如 "400") */
1150
+ weight: z.string().optional(),
1151
+ /** 该文件的字形(normal/italic) */
1152
+ style: z.string().optional(),
1153
+ }),
1154
+ )
1155
+ .min(1)
1156
+ .optional(),
1157
+ })
1158
+ .optional(),
1159
+ }),
1160
+ )
1161
+ .default([]),
1162
+ });
1163
+
1164
+ export type ThemeConfig = z.input<typeof themeConfigSchema>;
1165
+ export type ResolvedThemeConfig = z.output<typeof themeConfigSchema>;
1166
+
1167
+ /**
1168
+ * 内容 schema 工厂:文章集合 frontmatter。
1169
+ * Astro 要求 collections 定义在站点项目内,站点通过
1170
+ * schema: definePostSchema({ ...自定义字段 })
1171
+ * 在兼容主题字段的基础上扩展自己的 frontmatter。
1172
+ * password/passwordHint 为加密文章兼容字段(功能在后续里程碑实现)。
1173
+ * lang/author/sourceLink/licenseName/licenseUrl/comment 为 orchid-blog frontmatter 全量对齐
1174
+ * (旧文章实际使用为 0,预留以保证迁移与未来写作无损)。
1175
+ * cardStyle:文章列表卡片形态按篇覆盖(决策 7)。
1176
+ */
1177
+ export function definePostSchema(extensions: z.ZodRawShape = {}) {
1178
+ return z
1179
+ .object({
1180
+ title: z.string(),
1181
+ description: z.string().default(""),
1182
+ published: z.coerce.date(),
1183
+ updated: z.coerce.date().optional(),
1184
+ draft: z.boolean().default(false),
1185
+ pinned: z.boolean().default(false),
1186
+ image: z.string().optional(),
1187
+ tags: z.array(z.string()).default([]),
1188
+ category: z.string().optional(),
1189
+ password: z.string().optional(),
1190
+ passwordHint: z.string().optional(),
1191
+ /** 语言(BCP-47,如 zh-CN;空 = 跟随站点) */
1192
+ lang: z.string().optional(),
1193
+ /** 作者名(空 = 跟随站点配置) */
1194
+ author: z.string().optional(),
1195
+ /** 原文链接(转载文章) */
1196
+ sourceLink: z.string().optional(),
1197
+ /** 文章级许可证(空 = 跟随站点全局配置) */
1198
+ licenseName: z.string().optional(),
1199
+ licenseUrl: z.string().optional(),
1200
+ /** 本篇是否展示版权声明(默认开;全站另受 license.enabled 控制) */
1201
+ license: z.boolean().default(true),
1202
+ /** 本篇是否展示评论(默认开) */
1203
+ comment: z.boolean().default(true),
1204
+ /** 列表卡片形态按篇覆盖:cover 图卡 / compact 紧凑行 / text 纯文字 */
1205
+ cardStyle: z.enum(["cover", "compact", "text"]).optional(),
1206
+ /** 专题名(§4.31):同专题文章聚合到 /series/<name>/,文章页出专题导航 */
1207
+ series: z.string().optional(),
1208
+ /** 专题内顺序(小在前);未配置则按发布时间升序 */
1209
+ seriesOrder: z.number().optional(),
1210
+ /**
1211
+ * 专题内分组(§4.75):专题页与文章页专题目录按组分块展示
1212
+ * (未分组文章块在最前,各分组按组内首篇顺序依次排列,全局序号连续)。
1213
+ * 组名直接来自本字段文本,无 i18n 参与。
1214
+ */
1215
+ seriesGroup: z.string().optional(),
1216
+ })
1217
+ .extend(extensions);
1218
+ }