@windcoder/astro-theme-orchid 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  ## 快速上手
14
14
 
15
+ > 新站点推荐直接从 [astro-theme-orchid-starter](https://github.com/iwinder/astro-theme-orchid-starter) 模板生成(Use this template),配置与内容骨架开箱即用;以下为在已有 Astro 站点上接入主题的步骤。
16
+
15
17
  ### 1. 安装
16
18
 
17
19
  ```sh
@@ -28,11 +30,11 @@ import { defineConfig } from "astro/config";
28
30
 
29
31
  export default defineConfig({
30
32
  site: "https://blog.example.com",
31
- integrations: [orchidTheme({ title: "我的博客" /* 其余配置见下文 */ })],
33
+ integrations: [orchidTheme({ title: "我的博客" /* 其余配置见 demo 站指南系列 */ })],
32
34
  });
33
35
  ```
34
36
 
35
- 配置在 dev/build 启动即校验(Zod schema),错误会直接报出来。所有配置项都有默认值,只有 `title` 必填。
37
+ 配置在 dev/build 启动即校验(Zod schema),错误会直接报出来。所有配置项都有默认值,无必填项(`title` 缺省为 `"Orchid Blog"`,正式站点应显式配置)。
36
38
 
37
39
  ### 3. 内容集合
38
40
 
@@ -53,31 +55,9 @@ export const collections = { posts };
53
55
 
54
56
  #### 可选:启用 MDX
55
57
 
56
- MDX 是站点能力选择:是否写 `.mdx` 文章由站点决定。主题渲染管线与其天然兼容——
57
- `@astrojs/mdx` 默认继承站点 `markdown` 配置,主题注册的自定义 unified processor
58
- (KaTeX / Mermaid / PlantUML / directives / Expressive Code)原样作用于 `.mdx`,无需任何额外接线。
59
-
60
- ```sh
61
- pnpm add @astrojs/mdx
62
- ```
58
+ MDX 是站点能力选择:是否写 `.mdx` 文章由站点决定。主题渲染管线与其天然兼容——`@astrojs/mdx` 默认继承站点 `markdown` 配置,主题注册的自定义 unified processor(KaTeX / Mermaid / PlantUML / directives / Expressive Code)原样作用于 `.mdx`。
63
59
 
64
- ```ts
65
- // astro.config.ts
66
- import mdx from "@astrojs/mdx";
67
-
68
- export default defineConfig({
69
- integrations: [mdx(), orchidTheme(themeConfig)],
70
- });
71
- ```
72
-
73
- 集合 loader 同步扩展为 `"**/*.{md,mdx}"`。正文可直接 `import` 并渲染 Astro 组件
74
- (组件是站点直接依赖,如 `astro-icon` 需在站点 `package.json` 声明)。
75
-
76
- > 已知上游坑(§4.67):`hast-util-to-estree@3.1.3` 依赖的 `style-to-js@1.0.0`
77
- > 纯 CJS(`exports.default` 形态),ESM 默认导入得到对象而非函数,MDX 编译带
78
- > style 属性的节点(EC/katex 输出)报 `styleToJs is not a function`。在
79
- > `pnpm-workspace.yaml` 的 `overrides` 加 `style-to-js: ^1.1.16` 即可
80
- > (1.1.x 起修正 `module.exports` 互操作)。
60
+ 启用三步:`pnpm add @astrojs/mdx`、在 `integrations` 数组里注册(与主题顺序无关)、集合 loader 扩为 `"**/*.{md,mdx}"`。组件写作、`.md`/`.mdx` 选型,以及已知的 `style-to-js` 依赖坑(含一行 overrides 根治配方),见 demo 站《MDX 指南》一文。
81
61
 
82
62
  ### 4. 站点结构约定
83
63
 
@@ -92,188 +72,17 @@ site/
92
72
  └── public/ # 静态资源(音乐/壁纸/画廊/模型等)
93
73
  ```
94
74
 
95
- 独立页 `friends.md` / `guestbook.md` 的 frontmatter 只需 `title`;页面数据(友链列表、打赏码等)来自主题配置(见下文 `friends` / `sponsor`)。
96
-
97
- ## 配置参考
98
-
99
- > **同步约定**:主题新增或修改配置项时,本表同步更新(同一次提交内)。字段语义以 [`src/schema.ts`](./src/schema.ts) 注释为准。
100
-
101
- ### 基础信息
102
-
103
- | 配置项 | 类型 | 默认值 | 说明 |
104
- |---|---|---|---|
105
- | `title` | `string` | (必填) | 站点名 |
106
- | `description` | `string` | `""` | 站点描述(首页与 meta description 默认值) |
107
- | `keywords` | `string[]` | `[]` | 站点级 SEO keywords;文章页由 frontmatter `tags` 覆盖,留空不输出 meta |
108
- | `logo` | `string` | `""` | 导航栏 Logo 图片 URL(public 路径或外链),留空只渲染文字标题 |
109
- | `author` | `string` | `""` | 作者名 |
110
- | `accentColor` | `string` | `"#4a9d8f"` | 强调色(6 位 hex)。构建期转 HSL,全部派生色由公式计算 |
111
- | `lang` | `string` | `"zh-CN"` | 站点语言(`<html lang>`),同时驱动 UI 文案(支持 `zh-CN` / `en`) |
112
- | `pageWidth` | `number` | `1120` | 主容器页宽(800-1920 px),驱动主壳与文章页双列布局 |
113
- | `timezone` | `string` | — | 构建期日期时区(IANA 名,如 `"Asia/Shanghai"`),留空跟随构建环境 |
114
- | `since` | `string` | — | 建站日期 `YYYY-MM-DD`(站点信息卡「已运行」),不配置不显示 |
115
- | `colorMode` | `"light" \| "dark" \| "system"` | `"system"` | 默认深浅模式(仅初值,用户切换后以 localStorage 为准) |
116
- | `favicon` | `{src, type?, sizes?, theme?}[]` | `[]` | 数组非空时替换默认 favicon;`theme: "light"/"dark"` 按系统深浅切换 |
117
- | `footer.customHtml` | `string` | `""` | 页脚自定义 HTML(备案号等),渲染在页脚最底部 |
118
-
119
- ### 导航与背景
120
-
121
- | 配置项 | 类型 | 默认值 | 说明 |
122
- |---|---|---|---|
123
- | `nav` | `{label, href?, external?, children?}[]` | `[]` | 顶部导航。`children` 非空时父项为下拉入口;`external: true` 新窗口打开 |
124
- | `banner.enabled` | `boolean` | `true` | 首页 banner 总开关(hero 文案 + 背景图合并单配置,§4.42);`false` 时两者都不渲染 |
125
- | `banner.src` | `string \| string[]` | — | banner 图源。数组时构建期随机选一张(零 JS);不配置则无图(quote 有值时 hero 文案渲染为列表上方独立区块) |
126
- | `banner.height` | `number` | `32` | banner 高度(10-90 vh) |
127
- | `banner.dimOpacity` | `number` | `0.2` | 黑蒙层不透明度(0-1) |
128
- | `banner.position` | `string` | `"center"` | 图片 object-position |
129
- | `banner.homeOnly` | `boolean` | `true` | `true` 仅首页显示 |
130
- | `banner.quote` / `banner.sub` | `string` | `""` | hero 主句/副句;quote 支持 `<em>` 强调色,留空不渲染。有 banner 图时叠加显示其上,否则为列表上方独立区块 |
131
- | `huePanel.enabled` | `boolean` | `true` | 导航栏 hue 调色面板(预设色板 + 滑杆,运行时覆写 `--accent-h`,持久化 localStorage) |
132
-
133
- ### 文章列表与侧栏
134
-
135
- | 配置项 | 类型 | 默认值 | 说明 |
136
- |---|---|---|---|
137
- | `postList.style` | `"cover" \| "compact"` | `"cover"` | 默认卡片形态;frontmatter `cardStyle` 可按篇覆盖、可混排 |
138
- | `postList.showCover` | `boolean` | `true` | 列表封面开关(§4.55);`false` 时列表卡不渲染封面(整体降级纯文字形态) |
139
- | `postList.pageSize` | `number` | `10` | 每页文章数 |
140
- | `postList.fallbackCover` | `string` | — | 无图文章兜底封面;不配置则降级纯文字卡 |
141
- | `postList.randomCover.apis` | `string[]` | `[]` | 随机封面图源(URL 支持 `{slug}` 占位符);frontmatter `image: "api"` 的文章从此取图,客户端失败自动重试降级 |
142
- | `postList.defaultLayout` | `"list" \| "grid"` | `"list"` | 默认列表形态(§4.55):访客未手动切换过时的初始形态;移动端小屏不生效(恒为单列上下布局) |
143
- | `postList.allowSwitch` | `boolean` | `false` | 导航栏右上的列表/网格切换按钮(§4.55 移至 Header,仅首页/列表页显示;访客选择持久化 localStorage) |
144
- | `postList.gridColumnWidth` | `number` | `320` | 网格模式最小列宽(220-600 px) |
145
- | `categoryBar.enabled` | `boolean` | `false` | 分类导航栏(首页列表与文章页正文顶部) |
146
- | `toc.enabled` | `boolean` | `true` | 文章页目录 |
147
- | `archive.fold` | `boolean` | `false` | 归档页按年折叠(最新一年默认展开) |
148
- | `outdated.enabled` / `outdated.threshold` | `boolean` / `number` | `true` / `30` | 文章过时提醒与阈值(天,按最后更新时间计算) |
149
- | `sidebar.onPost` | `boolean` | `false` | 文章详情页是否启用侧栏 |
150
- | `sidebar.widgets` | `{type, enabled?, showOnPost?, postOnly?, hideOnMobile?, position?, specificConfig?}[]` | 公告/分类/标签/友链/日历/TOC/站点信息/广告 | widget 顺序即渲染顺序,公共配置(开关/显隐/位置)配在数组项上,专属参数统一收进 `specificConfig` 入口(§4.52)。类型:`announcement` `categories` `tags` `friends` `calendar` `toc` `siteInfo` `advertisement` `profile`;`showOnPost: false` 仅列表页显示,`postOnly: true` 仅文章页显示,`hideOnMobile: true` 移动端隐藏,`position: "sticky"` ≥768px 跟随滚动 |
151
- | `sidebar.widgets` tags 项 `specificConfig.count` | `number` | `12` | 展示的热门标签数量(0 = 全部展示),超出收进「…更多」入口 |
152
- | `sidebar.widgets` friends 项 `specificConfig.count` / `specificConfig.random` | `number` / `boolean` | `3` / `false` | 友链展示数量;`random: true` 构建期随机取样 |
153
- | `sidebar.widgets` calendar 项 `specificConfig.postTooltip` | `boolean` | `false` | `true` 时日历下方文章列表条目悬停显示全文 title |
154
- | `announcement` | `{title, content, link{text,url,external}, closable}` | content 空 | 侧栏公告卡;`content` 留空整卡不渲染 |
155
- | `advertisement` | `{enabled, title, image, imageLink, imageAlt, content, link{text,url,external}, closable, displayCount, expireDate?}` | `enabled: false` | 侧栏广告位;`displayCount` 按 localStorage 计数限次(-1 不限),`expireDate` 过期不渲染 |
156
- | `profile` | `{avatar, name, bio, url, links[{name,url,icon,showName}]}` | 全空 | 侧栏个人资料卡(`sidebar.widgets` 含 `profile` 时渲染);`icon` 填内置图标名或内联 SVG,见「社交图标」 |
157
-
158
- #### 社交图标
159
-
160
- `profile.links[].icon` 三种写法:**内置简名**(推荐,构建期映射到 Iconify 标准集合内联渲染,无运行时请求与 JS 开销)、**Iconify 全名**(`"集:名"`,如 `"simple-icons:bilibili"`;站点补装任意 `@iconify-json/*` 集合包即可使用,无需改主题)或**内联 SVG 字符串**(以 `<` 开头,向后兼容)。名字 trim + 转小写后解析,未命中跳过图标只渲染文字,不报错。品牌图标 `fill: currentColor`、线性图标 `stroke: currentColor`,颜色随文字继承。
161
-
162
- 基于 [astro-icon](https://docs.astro.build/en/guides/integrations-guide/icon/) + Iconify 生态(§4.65 标准化方案),内置简名 20 枚:
163
-
164
- - 线性描边(lucide,ISC):`github` `email` `rss`
165
- - 品牌填充(simple-icons,CC0):`qq` `wechat`(别名 `weixin`) `weibo` `bilibili` `xiaohongshu` `x`(别名 `twitter`) `telegram` `youtube` `instagram` `linkedin` `discord` `steam` `zhihu` `gitee` `juejin` `csdn` `douyin`(simple-icons 未收录抖音官方形,暂以内联 SVG 兜底)
166
-
167
- 图文示例见 demo 站「主题使用指南」专题的《社交图标配置》一文。
168
-
169
- ### 写作增强
170
-
171
- | 配置项 | 类型 | 默认值 | 说明 |
172
- |---|---|---|---|
173
- | `codeTheme.light` / `codeTheme.dark` | `string` | `"github-light"` / `"github-dark"` | Expressive Code 双主题(Shiki 主题名),按站点深浅模式切换 |
174
- | `codeTheme.fold` | `{enabled, lineThreshold, previewLines, defaultCollapsed}` | `false` / `15` / `8` / `true` | 长代码自动折叠(pluginCollapsible):行数超过 `lineThreshold` 折叠为 `previewLines` 行预览;meta 标 `collapse` 的块恒可手动折叠;math/mermaid/plantuml 块不参与 |
175
- | `codeTheme.lineNumbers` | `boolean` | `true` | 代码块显示行号(EC pluginLineNumbers,shell 会话块自动关闭) |
176
- | `codeTheme.languageBadge` | `boolean` | `false` | 代码块右上角显示语言徽标(EC pluginLanguageBadge) |
177
- | `codeTheme.wrap` | `boolean` | `false` | 长行自动换行(false 时横向滚动) |
178
- | `latex.enabled` | `boolean` | `false` | `$...$` / `$$...$$` 数学公式(KaTeX,含 mhchem 化学宏) |
179
- | `diagram.mermaid` | `{enabled, lightTheme, darkTheme}` | `false` / `"github-light"` / `"github-dark"` | Mermaid 构建期双主题静态渲染(` ```mermaid ` 代码块) |
180
- | `diagram.plantuml` | `{enabled, server, lightTheme, darkTheme}` | `false` / 官方服务 / `""` / `""` | PlantUML(` ```plantuml ` 代码块),图片由浏览器访问渲染服务加载 |
181
- | `emailProtection` | `{enabled, method}` | `false` / `"base64"` | mailto 链接构建期编码防爬(`base64` / `rot13`),点击时还原 |
182
- | `og.enabled` | `boolean` | `false` | 构建期生成每篇文章 OG 分享图(1200x630);Google Fonts 拉取失败降级占位图,不中断构建 |
183
- | `sharePoster.enabled` | `boolean` | `true` | 文章页「生成分享海报」按钮(纯浏览器 canvas,按需加载) |
184
- | `relatedPosts` | `{enabled, count, randomCount}` | `true` / `5` / `5` | 文章页相关/随机推荐(相关栏构建期评分;加密文章不进两个池) |
185
- | `license` | `{enabled, name, url, prefix, suffix}` | `true` / `"CC BY-NC-SA 4.0"` / CC 链接 / `""` / `""` | 版权声明;frontmatter `licenseName/licenseUrl/license` 可按篇覆盖 |
186
-
187
- ### 独立页与数据
188
-
189
- | 配置项 | 类型 | 默认值 | 说明 |
190
- |---|---|---|---|
191
- | `pages` | `{about, friends, guestbook, sponsor, gallery, series}` | 全 `true` | 独立页路由开关,`false` 则路由完全不生成 |
192
- | `gallery` | `{title, description, albums[], columnWidth}` | 「画廊」/ `[]` / `240` | 相册数据纯配置驱动。`albums[]`: `{id, name, description?, date?, location?, tags?, cover?, password?, passwordHint?}`;照片来源 `public/gallery/<id>/` 目录扫描 + 同目录 `urls.txt` 远程列表;`password` 非空即加密相册(构建期加密) |
193
- | `friends` | `{title, description, sort, apply, list[]}` | 「友链」/ weight 排序 | `list[]`: `{title, site, image?, desc?, tags?, weight?, enabled?, featured?}`;同时驱动友链页与侧栏 widget;头像留空渲染字头像 |
194
- | `friends.apply` | `{title, description, link, label}` | title 空 | 申请友链卡片,title 留空整卡不渲染 |
195
- | `sponsor` | `{title, description, usage, showSponsorsList, showButtonInPost, methods[], sponsors[]}` | 「打赏」 | `methods[]`: `{name, icon?, image?, link?, description?, enabled?}`(收款码与跳转链接至少配一个);`sponsors[]`: `{name, avatar?, amount?, date?}` 打赏者名单 |
196
- | `comment` | `{enabled, provider, title, artalk, twikoo, waline, giscus, disqus}` | `false` / `"none"` / 「评论」 | 五套评论按 `provider` 分发;`enabled: false` 或 `provider: "none"` 页面不渲染评论区。各套子配置:`artalk{server, locale, visitorCount}`、`twikoo{envId, jsUrl, cssUrl}`、`waline{serverURL, lang, visitorCount}`、`giscus{repo, repoId, category, categoryId, mapping, strict, reactionsEnabled, emitMetadata, inputPosition, lang, loading}`、`disqus{shortname}`;对应字段留空即不启用该套 |
197
- | `analytics` | `{googleAnalyticsId, microsoftClarityId, umami, la51}` | 全空 | 四选多,ID 留空即不注入。`umami{websiteId, scriptUrl, replaysScriptUrl, trackOutboundLinks, collectWebVitals, replays{...}}`;`la51{id, sdkUrl, ck, autoTrack, hashMode, screenRecord}` |
198
-
199
- ### 悬浮组件与特效
200
-
201
- | 配置项 | 类型 | 默认值 | 说明 |
202
- |---|---|---|---|
203
- | `sakura` | `{enabled, density}` | `false` / `24` | 樱花飘落(全站 canvas 粒子,零贴图);`prefers-reduced-motion` 自动停用 |
204
- | `music` | `{enabled, position, volume, playMode, showLyrics, playlist[]}` | `false` / `"left"` / `0.7` / `"list"` / `true` / `[]` | 音乐播放器(local 模式,`transition:persist` 跨页不断播)。`playlist[]`: `{name, artist?, url, cover?, lrc?}`;`lrc` 支持 `.lrc` 文件路径或内联歌词文本;`playMode`: `list`/`one`/`random`;音量访客调整后记忆。`enabled` 且歌单非空才渲染 |
205
- | `fonts` | `{name, cssVariable, provider, weights?, styles?, subsets?, fallbacks?, display?, preload?, role?, options?}[]` | `[]` | 字体(Astro Fonts API 标准方案,§4.66):声明式接入,构建期产出 @font-face 与变量,`<head>` 渲染 `<Font>` 组件;`role` 映射主题三角色变量。详见下方「fonts 详细配置」 |
206
- | `live2d` | 见下 | `enabled: false` | Live2D 看板娘(基于 [l2d-widget](https://l2d-widget.hacxy.cn)),`transition:persist` 跨页常驻 |
207
-
208
- #### live2d 详细配置
209
-
210
- | 子项 | 类型 | 默认值 | 说明 |
211
- |---|---|---|---|
212
- | `enabled` | `boolean` | `false` | 总开关 |
213
- | `model` | `{path, volume?, scale?, x?, y?}` 或其数组 | — | 模型配置,数组可多模型切换(默认菜单自动出现「切换模型」)。`path` 为模型 json 的 public 路径或外链;`volume` 动作音量 0-1(默认 0 静音);`scale` 缩放;`x`/`y` 偏移 -2~2 |
214
- | `position` | `"bottom-left" \| "bottom-right"` | `"bottom-left"` | 显示位置。与音乐播放器(左下)/ 返回顶部(右下)同角时会重叠,注意 `zIndex` 层级取舍 |
215
- | `size` | `number` 或 `{width, height}` | `280` | 画布尺寸(px),数字为正方形边长 |
216
- | `primaryColor` | `string` | `""` | 菜单/气泡等 UI 主题色,留空用库默认 |
217
- | `transitionDuration` / `transitionType` | `number` / `"slide" \| "fade"` | — | 入场/退场动画时长(ms)与类型 |
218
- | `menus.items` | `{icon?, label, action, url?}[]` | 主页/回到顶部/休眠(+切换模型) | 自定义菜单(覆盖默认)。`action` 内置动作:`home` `scrollToTop` `sleep` `switchModel`,或 `"link"` + `url` 跳转外链 |
219
- | `menus.align` | `"left" \| "right"` | `"right"` | 菜单对齐 |
220
- | `tips` | `{enable, welcomeMessage[], messages[], duration, interval, offset{x,y}, typing?}` | — | 提示气泡:欢迎语与循环提示文案(站点个性化内容,自行配置);`duration` 显示时长 / `interval` 轮换间隔(ms);不配置用库默认,`enable: false` 关闭 |
221
- | `responsive` | `{hideOnMobile, mobileBreakpoint}` | `true` / `768` | 移动端按断点隐藏 |
222
- | `zIndex` | `number` | `240` | 容器层级(返回顶部 250 / 音乐播放器 255,默认放其下避免按钮被挡) |
223
-
224
- #### fonts 详细配置
225
-
226
- | 子项 | 类型 | 默认值 | 说明 |
227
- |---|---|---|---|
228
- | `name` | `string` | (必填) | 字体族名(provider 侧标识,如 `"Inter"`;`local` 为自定义名) |
229
- | `cssVariable` | `string` | (必填) | CSS 变量名(`--` 开头,如 `"--font-inter"`),Font 组件据此产出变量 |
230
- | `provider` | `"google" \| "fontsource" \| "local" \| "bunny" \| "fontshare" \| "npm"` | (必填) | 字体来源。`google`/`fontsource`/`bunny`/`fontshare` 构建期联网拉取;`local` 配合 `options.variants` 声明本地文件;`npm` 从已装字体包读取 |
231
- | `weights` | `number[]` | `[400]` | 字重档位(Astro 只下载列出的字重) |
232
- | `styles` | `("normal" \| "italic" \| "oblique")[]` | `["normal", "italic"]` | 字形 |
233
- | `subsets` | `string[]` | `["latin"]` | 语言子集 |
234
- | `fallbacks` | `string[]` | `["sans-serif"]` | 回退字体族(Astro 自动产出 size-adjust 优化 fallback @font-face,减少布局抖动) |
235
- | `display` | `"auto" \| "swap" \| "block" \| "metadata" \| "fallback"` | Astro 默认 | font-display 行为 |
236
- | `preload` | `boolean` | `false` | `<link rel=preload>` 首屏预载字体文件(正文字体建议开启) |
237
- | `role` | `"body" \| "display" \| "mono"` | — | 角色映射(主题层字段):`<head>` 产出 `html:root` 高优先级规则,把 `--font-body` / `--font-display` / `--font-mono` 重定向为 `var(<cssVariable>), <原角色回退栈>`——字体未覆盖的字形(如 Inter 只有 latin,中文)自然回落原栈 |
238
- | `options.variants` | `{src, weight?, style?}[]` | — | `provider: "local"` 专属:本地字体文件声明(相对站点项目根) |
239
-
240
- 示例(demo 站即此配置):
241
-
242
- ```ts
243
- // site/config/appearanceConfig.ts
244
- export const fonts: ThemeConfig["fonts"] = [
245
- {
246
- name: "Inter",
247
- cssVariable: "--font-inter",
248
- provider: "fontsource",
249
- weights: [400, 700],
250
- preload: true,
251
- role: "body",
252
- },
253
- ];
254
- ```
75
+ 独立页 `friends.md` / `guestbook.md` 的 frontmatter 只需 `title`;页面数据(友链列表、打赏码等)来自主题配置,见 demo 站《独立页面与数据》一文。
255
76
 
256
- 与字体子集化管线(§4.5)的边界:`site/subset-fonts.json` 面向 `public/` 下自托管字体文件(构建后子集化 + URL 重写);`fonts` 走 Astro Fonts API,产物在 `dist/_astro/fonts/`、由 `<Font>` 直接产出引用。两条管线互不相交,可并存。
77
+ ## 配置文档
257
78
 
258
- ## 文章 frontmatter
79
+ 主题配置项全部有默认值、无必填项,dev/build 启动即经 Zod schema 校验,配错直接报出。字段语义以 [`src/schema.ts`](./src/schema.ts) 注释为准。
259
80
 
260
- `definePostSchema` 全量字段(均可选字段见默认值):
81
+ 完整配置说明与文章 frontmatter 全字段表统一放在 demo 站「主题使用指南」系列文章(源文件在本仓库 `site/src/content/posts/`),按主题分组:
261
82
 
262
- | 字段 | 类型 | 默认值 | 说明 |
263
- |---|---|---|---|
264
- | `title` | `string` | (必填) | 标题 |
265
- | `description` | `string` | `""` | 摘要描述(构建期另有自动摘要插件兜底) |
266
- | `published` / `updated` | `date` | 必填 / — | 发布/更新时间 |
267
- | `draft` / `pinned` | `boolean` | `false` | 草稿 / 置顶 |
268
- | `image` | `string` | — | 封面图;`"api"` 走 `postList.randomCover.apis` 随机图 |
269
- | `tags` / `category` | `string[]` / `string` | `[]` / — | 标签 / 分类 |
270
- | `password` / `passwordHint` | `string` | — | 非空即加密文章(构建期加密,密码校验在客户端) |
271
- | `cardStyle` | `"cover" \| "compact" \| "text"` | — | 列表卡片形态按篇覆盖 |
272
- | `series` / `seriesOrder` | `string` / `number` | — | 专题名与专题内顺序(聚合到 `/series/<name>/`) |
273
- | `lang` / `author` | `string` | — | 语言 / 作者(空跟随站点) |
274
- | `sourceLink` | `string` | — | 原文链接(转载) |
275
- | `licenseName` / `licenseUrl` / `license` | `string` / `string` / `boolean` | — / — / `true` | 文章级版权覆盖,`license: false` 按篇关闭声明 |
276
- | `comment` | `boolean` | `true` | 本篇是否展示评论 |
83
+ - 基础:《站点基础配置》|《导航栏》|《首页横幅》|《侧边栏与小组件》|《个人资料卡》|《版权声明》|《文章列表与归档》
84
+ - 功能:《文章页功能》|《内容渲染》|《樱花特效》|《音乐播放器》|《Live2D 看板娘》|《字体》|《评论系统》|《公告》|《站点统计》|《广告位》|《友链》|《赞助》|《画廊》|《独立页面》
85
+ - 写作:《写作指南》(frontmatter 全字段与增强语法)|《MDX 指南》(MDX 启用与踩坑修复)
277
86
 
278
87
  ## 定制与覆盖
279
88
 
@@ -286,15 +95,15 @@ export const fonts: ThemeConfig["fonts"] = [
286
95
 
287
96
  ## 文档同步约定
288
97
 
289
- - 新增/修改**配置项**:同一次提交内更新本 README「配置参考」对应行,并在 `docs/design.md` 记录功能节(§ 编号)。
290
- - 新增**frontmatter 字段**:同步更新「文章 frontmatter」表。
98
+ - 新增/修改**配置项**:同一次提交内更新 demo 站对应指南文章(`site/src/content/posts/`),并在 `docs/design.md` 记录功能节(§ 编号)。
99
+ - 新增**frontmatter 字段**:同步更新 demo 站《写作指南》字段表。
291
100
  - 行为变更不涉及配置面时,至少在 `docs/design.md` 留痕。
292
101
 
293
102
  ## 本仓库开发
294
103
 
295
104
  ```text
296
105
  packages/theme 主题包(exports 直指 src,无构建步骤)
297
- site 中性 demo 站点(config/ 按模块拆分配置,是配置参考的最佳示例)
106
+ site 中性 demo 站点(config/ 按模块拆分配置,是指南文章与配置示例的载体)
298
107
  docs/design.md 设计与实现记录(§ 编号推进,含踩坑与验证口径)
299
108
  ```
300
109
 
package/package.json CHANGED
@@ -1,71 +1,73 @@
1
1
  {
2
- "name": "@windcoder/astro-theme-orchid",
3
- "version": "0.1.0",
4
- "description": "清新简约的 Astro 博客主题,以 Integration 形式提供,支持版本化升级与组件覆盖",
5
- "type": "module",
6
- "keywords": [
7
- "astro",
8
- "astro-integration",
9
- "blog",
10
- "theme",
11
- "tailwindcss"
12
- ],
13
- "license": "MIT",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/iwinder/qy-astro-theme-orchid.git",
17
- "directory": "packages/theme"
18
- },
19
- "homepage": "https://github.com/iwinder/qy-astro-theme-orchid/tree/main/packages/theme#readme",
20
- "bugs": "https://github.com/iwinder/qy-astro-theme-orchid/issues",
21
- "publishConfig": {
22
- "access": "public",
23
- "registry": "https://registry.npmjs.org/"
24
- },
25
- "exports": {
26
- ".": "./src/index.ts",
27
- "./schema": "./src/schema.ts",
28
- "./routes/*": "./src/routes/*",
29
- "./components/*": "./src/components/*",
30
- "./plugins/*": "./src/plugins/*",
31
- "./styles/*": "./src/styles/*"
32
- },
33
- "files": [
34
- "src"
35
- ],
36
- "peerDependencies": {
37
- "@iconify-json/lucide": "^1.2.0",
38
- "@iconify-json/simple-icons": "^1.2.0",
39
- "astro": "^7.0.0"
40
- },
41
- "dependencies": {
42
- "@astrojs/markdown-remark": "^7.3.2",
43
- "@astrojs/rss": "^4.0.19",
44
- "@astrojs/sitemap": "^3.7.3",
45
- "@expressive-code/plugin-collapsible-sections": "^0.44.2",
46
- "@expressive-code/plugin-line-numbers": "^0.44.2",
47
- "@tailwindcss/vite": "^4.3.2",
48
- "astro-icon": "^1.2.0",
49
- "beautiful-mermaid": "^1.1.3",
50
- "expressive-code-collapsible": "^0.1.0",
51
- "expressive-code-language-badge": "^1.1.0",
52
- "hastscript": "^9.0.1",
53
- "katex": "^0.19.0",
54
- "l2d-widget": "^0.1.2",
55
- "pako": "^3.0.2",
56
- "photoswipe": "^5.4.4",
57
- "qrcode": "^1.5.4",
58
- "rehype-expressive-code": "^0.44.2",
59
- "rehype-katex": "^7.0.1",
60
- "remark-directive": "^4.0.0",
61
- "remark-math": "^6.0.0",
62
- "satori": "^0.47.1",
63
- "sharp": "^0.35.5",
64
- "tailwindcss": "^4.3.2"
65
- },
66
- "devDependencies": {
67
- "@types/pako": "^2.0.3",
68
- "@types/qrcode": "^1.5.6",
69
- "subset-font": "^2.5.0"
70
- }
71
- }
2
+ "name": "@windcoder/astro-theme-orchid",
3
+ "version": "0.2.1",
4
+ "description": "清新简约的 Astro 博客主题,以 Integration 形式提供,支持版本化升级与组件覆盖",
5
+ "type": "module",
6
+ "keywords": [
7
+ "astro",
8
+ "astro-integration",
9
+ "blog",
10
+ "theme",
11
+ "tailwindcss"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/iwinder/qy-astro-theme-orchid.git",
17
+ "directory": "packages/theme"
18
+ },
19
+ "homepage": "https://github.com/iwinder/qy-astro-theme-orchid/tree/main/packages/theme#readme",
20
+ "bugs": "https://github.com/iwinder/qy-astro-theme-orchid/issues",
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "exports": {
26
+ ".": "./src/index.ts",
27
+ "./schema": "./src/schema.ts",
28
+ "./routes/*": "./src/routes/*",
29
+ "./components/*": "./src/components/*",
30
+ "./plugins/*": "./src/plugins/*",
31
+ "./styles/*": "./src/styles/*"
32
+ },
33
+ "files": [
34
+ "src",
35
+ "scripts",
36
+ "!scripts/*.test.mjs"
37
+ ],
38
+ "peerDependencies": {
39
+ "@iconify-json/lucide": "^1.2.0",
40
+ "@iconify-json/simple-icons": "^1.2.0",
41
+ "astro": "^7.0.0"
42
+ },
43
+ "dependencies": {
44
+ "@astrojs/markdown-remark": "^7.3.2",
45
+ "@astrojs/rss": "^4.0.19",
46
+ "@astrojs/sitemap": "^3.7.3",
47
+ "@expressive-code/plugin-collapsible-sections": "^0.44.2",
48
+ "@expressive-code/plugin-line-numbers": "^0.44.2",
49
+ "@tailwindcss/vite": "^4.3.2",
50
+ "astro-icon": "^1.2.0",
51
+ "beautiful-mermaid": "^1.1.3",
52
+ "expressive-code-collapsible": "^0.1.0",
53
+ "expressive-code-language-badge": "^1.1.0",
54
+ "hastscript": "^9.0.1",
55
+ "katex": "^0.19.0",
56
+ "l2d-widget": "^0.1.2",
57
+ "pako": "^3.0.2",
58
+ "photoswipe": "^5.4.4",
59
+ "qrcode": "^1.5.4",
60
+ "rehype-expressive-code": "^0.44.2",
61
+ "rehype-katex": "^7.0.1",
62
+ "remark-directive": "^4.0.0",
63
+ "remark-math": "^6.0.0",
64
+ "satori": "^0.47.1",
65
+ "sharp": "^0.35.5",
66
+ "subset-font": "^2.5.0",
67
+ "tailwindcss": "^4.3.2"
68
+ },
69
+ "devDependencies": {
70
+ "@types/pako": "^2.0.3",
71
+ "@types/qrcode": "^1.5.6"
72
+ }
73
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LQIP 生成脚本(M3b-b9,P1-14):扫描站点 public 下的图片,每张采样
4
+ * 2×2 像素取三色,产出 18 字符 compact hex(三段 rgb hex 拼接),
5
+ * 整体写入站点根 lqips.json(键 → 色,全量重建,增量收益有限故不做)。
6
+ *
7
+ * 运行:pnpm --filter site lqip(cwd 必须是 site 根)
8
+ * 消费链路:lqips.json → themeLqipsPlugin(vite.ts)→ virtual:theme/lqips
9
+ * → utils/lqip.ts getLqipStyle() → 构建期内联渐变(零客户端请求)。
10
+ *
11
+ * 键约定:`public:<posix 相对路径>`。主题图片一律 public 路径或外链
12
+ * (不用 astro:assets),故无需 src: 键;外链/无数据在消费端自然降级纯色。
13
+ * sharp 经本脚本文件位置解析到 theme 包依赖,与 cwd 无关。
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import sharp from "sharp";
19
+
20
+ const cwd = process.cwd();
21
+ const publicDir = path.join(cwd, "public");
22
+ const outFile = path.join(cwd, "lqips.json");
23
+
24
+ const EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".avif"]);
25
+ const EXCLUDED_DIRS = new Set(["node_modules", ".astro", "dist", ".git"]);
26
+
27
+ if (!fs.existsSync(publicDir)) {
28
+ console.error(`[lqip] 未找到 ${publicDir},请在 site 根目录运行本脚本`);
29
+ process.exit(1);
30
+ }
31
+
32
+ /** 递归收集图片相对路径(posix 风格,排除构建/依赖目录) */
33
+ function collectImages(dir, base = "") {
34
+ const out = [];
35
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
36
+ if (entry.isDirectory()) {
37
+ if (EXCLUDED_DIRS.has(entry.name)) continue;
38
+ out.push(
39
+ ...collectImages(
40
+ path.join(dir, entry.name),
41
+ base ? `${base}/${entry.name}` : entry.name,
42
+ ),
43
+ );
44
+ } else if (EXTS.has(path.extname(entry.name).toLowerCase())) {
45
+ out.push(base ? `${base}/${entry.name}` : entry.name);
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+
51
+ /** sharp 2×2 fill 采样 → 取 [0][1][3] 三像素(左上/右上/右下)rgb hex 拼接 */
52
+ async function sampleColors(file) {
53
+ const { data, info } = await sharp(file)
54
+ .resize(2, 2, { fit: "fill" })
55
+ .removeAlpha()
56
+ .raw()
57
+ .toBuffer({ resolveWithObject: true });
58
+ const hex = (i) => {
59
+ const o = i * info.channels;
60
+ return [data[o], data[o + 1], data[o + 2]]
61
+ .map((v) => v.toString(16).padStart(2, "0"))
62
+ .join("");
63
+ };
64
+ return hex(0) + hex(1) + hex(3);
65
+ }
66
+
67
+ try {
68
+ const files = collectImages(publicDir);
69
+ const entries = await Promise.all(
70
+ files.map(async (rel) => [
71
+ `public:${rel}`,
72
+ await sampleColors(path.join(publicDir, rel)),
73
+ ]),
74
+ );
75
+ fs.writeFileSync(
76
+ outFile,
77
+ JSON.stringify(Object.fromEntries(entries), null, 2) + "\n",
78
+ );
79
+ console.log(`[lqip] ${files.length} 张图片 → ${path.relative(cwd, outFile)}`);
80
+ } catch (err) {
81
+ console.error("[lqip] 生成失败:", err);
82
+ process.exit(1);
83
+ }