13924 字
70 分钟
Shirone Markdown 增强功能
Shirone 提供了一组主题专属的 Markdown 扩展与自定义语法容器。这些扩展构建在我们原生的 unified AST 处理管线之上,全部在站点构建期间渲染为无障碍、语义化的 HTML,零客户端 JavaScript 水合开销,并100% 对齐 M3E 设计令牌。
文件树#
文件树可以把多级项目结构、源码层级和终端目录输出转换成紧凑的交互式树状视图,自带扩展名图标、差异高亮与可折叠分支。
1. 嵌套列表语法(:::file-tree)#
当你想以 Markdown 嵌套列表的形式直接书写文件层级时,使用 :::file-tree 块级指令。
1:::file-tree{title="Shirone source tree"}2- src3 - components/4 - ++ Navigation.svelte # added component5 - -- Button.astro # removed component6 - content7 - posts/8 - markdown-enhancements.md9 - layouts/10 - PostLayout.astro11 - plugins12 - markdown/13 - rehype-file-tree.mjs14 - styles15 - markdown/16 - trees.css17 - **content.config.ts** # important file18- public/19 - favicon.svg20- package.json21:::Shirone 源码树
src
components
- +Navigation.svelte新增组件
- -Button.astro删除的组件
content
posts
- markdown-enhancements.md
layouts
- PostLayout.astro
plugins
markdown
- rehype-file-tree.mjs
styles
markdown
- trees.css
- content.config.ts重要文件
public
- favicon.svg
- package.json
编写规则与标记#
- 差异状态:在条目前加上
++(绿色背景与徽标)或--(红色背景与删除线),以突出变更。 - 注释:
#之后的任何文本都会渲染为弱化的右对齐行内注释。 - 强调:用
**粗体**包裹名称,让关键文件获得醒目的视觉分量。 - 可折叠文件夹:由嵌套列表项推断出的目录默认处于展开状态。加上末尾斜杠(例如
components/)可以创建默认折叠的目录,读者可以通过点击或键盘导航将其展开。
2. 终端输出语法(```file-tree)#
当你已经有 tree 等命令行工具生成的目录树文本时,直接把它粘贴进 file-tree 围栏代码块即可。Unicode 分支字符(├──、└──、│)与 ASCII 分支都会被自动解析。
1```file-tree title="Build output" icon="simple"2dist3├── _astro/4│ ├── index.css5│ └── page.js6└── favicon.ico7```构建输出
dist
_astro
- index.css
- page.js
- favicon.ico
配置选项#
title="string":为树设置自定义标题与无障碍标签。icon="colored" | "simple":在多彩扩展名图标(colored,默认)与极简单色图标(simple)之间选择。
代码树#
交互式代码树在左侧提供多级文件层级导航窗格,在右侧提供即时切换的代码面板,为多文件示例、模块或整目录讲解带来类 IDE 的阅读体验。
1. 容器语法(:::code-tree)#
在 :::code-tree 块级指令中组合多个围栏代码块。每个代码块通过 title="path/to/file" 指定自己的路径。
1:::code-tree{title="Shirone Component Demo" height="380px" entry="src/Button.svelte"}2```svelte title="src/Button.svelte"3<script lang="ts">4 let { label = "Click me" } = $props();5</script>6
7<button class="m3-btn">{label}</button>8```9
10```stylus title="src/styles/button.styl"11.m3-btn12 background: var(--primary)13 color: var(--on-primary)14 border-radius: var(--shape-corner-m)15```16
17```json title="package.json"18{19 "name": "button-demo",20 "version": "1.0.0"21}22```23:::Shirone 组件演示
1<script lang="ts">2 let { label = "Click me" } = $props();3</script>4
5<button class="m3-btn">{label}</button>1.m3-btn2 background: var(--primary)3 color: var(--on-primary)4 border-radius: var(--shape-corner-m)1{2 "name": "button-demo",3 "version": "1.0.0"4}配置与标记#
title="string":设置代码树的标题与无障碍标签。height="string":设置桌面视图的高度(默认420px,例如380px、26rem)。entry="filepath":指定首次加载时处于激活状态的文件。icon="colored" | "simple":在彩色与极简单色文件图标之间切换。:active:在任意围栏代码块上放置:active,即可将其指定为默认激活的标签页。
2. 本地目录自动导入(@[code-tree])#
直接指向工作区中的任意本地目录路径,即可在构建时自动扫描并生成交互式代码树,无需手动复制文件内容。
1@[code-tree title="Anime Utilities" entry="status.ts"](/src/utils/anime)站点配置
1import type {2 AnimeConfig,3 AnimeFallbackKind,4 AnimeProvider,5 AnimeSourceKind,6 ResolvedAnimeOptions,7} from "../types/animeConfig.ts";8import { withUserConfig } from "../utils/config-overlay.ts";9
10/**11 * ─────────────────────────────────────────────────────────────────────────────12 * Shirone 番剧页面与外部数据源配置13 * ─────────────────────────────────────────────────────────────────────────────14 *15 * 遵循「零额外负担」原则与双平面模型(`docs/remote-data-system.md`):16 * - 本地模式 (local):完全离线,直接使用 `src/data/anime.ts`,零网络、零构建脚本负担;17 * - 快照模式 (snapshot):读取构建期抓取清洗后的本地脱敏 JSON 快照(`src/data/anime-snapshots/`);18 * - 外部同步完全发生在显式 `pnpm anime:sync` 阶段,严禁页面运行时或默认构建时直接请求外部 API;19 * - 私密凭据(如 B站 SESSDATA)仅通过环境变量注入同步进程,绝不进入客户端代码与 Git 提交。20 *21 * ─────────────────────────────────────────────────────────────────────────────22 * 【常用配置场景】23 * ─────────────────────────────────────────────────────────────────────────────24 * 场景 A:使用本地手写数据(默认,最稳定安全)25 * ```ts26 * source: { kind: "local" }27 * ```28 *29 * 场景 B:使用 Bangumi 追番快照30 * 1. 填入你的 Bangumi 用户 ID,将 `providers.bangumi.enable` 置为 `true`;31 * 2. 将 `source` 设置为 `{ kind: "snapshot", provider: "bangumi" }`;32 * 3. 终端执行 `pnpm.cmd anime:sync --provider bangumi` 生成快照。33 *34 * 场景 C:使用 Bilibili 追番快照35 * 1. 填入你的 B 站 UID (`vmid`),将 `providers.bilibili.enable` 置为 `true`;36 * 2. 若追番列表设为私密,在 `.env` 中配置 `BILI_SESSDATA="your_sessdata"`;37 * 3. 将 `source` 设置为 `{ kind: "snapshot", provider: "bilibili" }`;38 * 4. 终端执行 `pnpm.cmd anime:sync --provider bilibili` 生成快照。39 * ─────────────────────────────────────────────────────────────────────────────40 */41export const animeConfig: AnimeConfig = withUserConfig("anime", {42 /** 是否启用番剧页(仅控制页面渲染,不发起任何外部网络连接) */43 enable: true,44
45 /** 主数据源选择 */46 source: {47 kind: "local",48 // provider: "bangumi",49 // file: "bangumi.json",50 // fetchOnDev: true,51 },52
53 /** 异常降级策略(快照丢失或解析失败时回退本地数据) */54 fallback: {55 kind: "local",56 },57
58 /** 外部提供方配置 */59 providers: {60 bangumi: {61 enable: false,62 userId: "", // 填入你的 Bangumi 数字 UID 或公开用户名(测试可填 "sai")63 request: {64 pageSize: 50,65 maxItems: 300,66 minDelayMs: 200,67 },68 },69 bilibili: {70 enable: false,71 vmid: "", // 填入你的 B 站公开 UID72 sessdataEnv: "BILI_SESSDATA",73 cover: {74 mode: "local", // "local" 站内下载缓存(推荐)| "remote" 远程链接 | "none"75 useWebp: true,76 },77 request: {78 pageSize: 30,79 maxItems: 300,80 minDelayMs: 300,81 },82 },83 },84
85 /** 快照存储管理 */86 snapshot: {87 directory: "src/data/anime-snapshots",88 staleAfterDays: 30,89 keepLastValid: true,90 },91});92
93const SAFE_FILENAME_PATTERN = /^[a-zA-Z0-9_-]+\.json$/;94
95/**96 * 校验并解析 Anime 配置,返回只读的标准选项97 */98export function resolveAnimeOptions(config: AnimeConfig): ResolvedAnimeOptions {99 const enable = Boolean(config.enable);100 const fallback: AnimeFallbackKind =101 config.fallback?.kind === "empty" ? "empty" : "local";102
103 const directory =104 typeof config.snapshot?.directory === "string" &&105 config.snapshot.directory.trim() &&106 !config.snapshot.directory.includes("..")107 ? config.snapshot.directory.trim().replace(/[\\/]+$/, "")108 : "src/data/anime-snapshots";109
110 const staleAfterDays =111 typeof config.snapshot?.staleAfterDays === "number" &&112 Number.isFinite(config.snapshot.staleAfterDays) &&113 config.snapshot.staleAfterDays > 0114 ? Math.floor(config.snapshot.staleAfterDays)115 : 30;116
117 const keepLastValid = config.snapshot?.keepLastValid ?? true;118
119 const rawKind = config.source?.kind;120 let kind: AnimeSourceKind = "local";121 let provider: AnimeProvider | undefined;122 let file: string | undefined;123
124 if (rawKind === "snapshot") {125 const rawProvider = config.source?.provider;126 if (rawProvider === "bangumi" || rawProvider === "bilibili") {127 provider = rawProvider;128 }129
130 const rawFile = config.source?.file?.trim();131 if (132 rawFile &&133 SAFE_FILENAME_PATTERN.test(rawFile) &&134 // 若指定了 provider 但 file 误填了另一 provider 的 json,自动校正为对应 provider 的 json 文件135 !(provider === "bilibili" && rawFile === "bangumi.json") &&136 !(provider === "bangumi" && rawFile === "bilibili.json")137 ) {138 file = rawFile;139 } else if (provider) {140 file = `${provider}.json`;141 }142
143 const fetchOnDev = config.source?.fetchOnDev ?? true;144
145 if (file) {146 kind = "snapshot";147 }148
149 return Object.freeze({150 enable,151 source: Object.freeze({152 kind,153 ...(provider ? { provider } : {}),154 ...(file ? { file } : {}),155 fetchOnDev,156 }),157 fallback,158 snapshot: Object.freeze({159 directory,160 staleAfterDays,161 keepLastValid,162 }),163 });164 }165
166 return Object.freeze({167 enable,168 source: Object.freeze({169 kind,170 ...(provider ? { provider } : {}),171 ...(file ? { file } : {}),172 fetchOnDev: config.source?.fetchOnDev ?? true,173 }),174 fallback,175 snapshot: Object.freeze({176 directory,177 staleAfterDays,178 keepLastValid,179 }),180 });181}182
183export const resolvedAnimeOptions: ResolvedAnimeOptions =184 resolveAnimeOptions(animeConfig);1import type { AnnouncementConfig } from "@/types/announcementConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 公告栏配置6 * 组件显示由 sidebarConfig 统一控制7 */8export const announcementConfig: AnnouncementConfig = withUserConfig(9 "announcement",10 {11 title: "", // 公告标题,填空使用 i18n 字符串 Key.announcement12 content: "成就伟大事业的唯一途径,就是热爱自己所做的事", // 公告内容13 closable: true, // 允许用户关闭公告14 link: {15 enable: true, // 启用链接16 text: "GitHub", // 链接文本17 url: "https://github.com", // 链接 URL18 external: true, // 外部链接19 },20 },21);1import type { ArticleConfig } from "@/types/articleConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 文章详情页配置。6 */7export const articleConfig: ArticleConfig = withUserConfig("article", {8 lastUpdated: {9 // 关闭后不渲染最后更新提示。10 enable: true,11 // 按 UTC 日历日计算;达到该天数当天开始显示,0 表示立即显示。12 minimumAgeDays: 90,13 },14 discovery: {15 // 总开关关闭后不计算、不渲染文章尾部的延伸阅读区域。16 enable: true,17 related: {18 // 只展示至少共享一个标签或分类的文章。19 enable: true,20 count: 3,21 },22 random: {23 // 按当前文章标识稳定抽样;同一构建中的结果不会随刷新变化。24 enable: true,25 count: 2,26 },27 },28 share: {29 // 关闭后不渲染文章尾部的分享区块,不引入客户端水合。30 enable: true,31 // 生成海报时是否默认包含文章封面(封面不可用时自动降级为无封面排版)。32 includeCover: true,33 },34});35
36const MAX_DISCOVERY_COUNT = 6;37
38export interface ArticleDiscoveryOptions {39 relatedCount: number;40 randomCount: number;41}42
43export interface ArticleShareOptions {44 includeCover: boolean;45}46
47export function normalizeDiscoveryCount(value: number): number {48 return Number.isFinite(value)49 ? Math.min(MAX_DISCOVERY_COUNT, Math.max(0, Math.floor(value)))50 : 0;51}52
53export function resolveArticleDiscoveryOptions(54 config: Pick<ArticleConfig, "discovery">,55): ArticleDiscoveryOptions | null {56 if (!config.discovery.enable) return null;57
58 const relatedCount = config.discovery.related.enable59 ? normalizeDiscoveryCount(config.discovery.related.count)60 : 0;61 const randomCount = config.discovery.random.enable62 ? normalizeDiscoveryCount(config.discovery.random.count)63 : 0;64
65 return relatedCount > 0 || randomCount > 066 ? { relatedCount, randomCount }67 : null;68}69
70export function resolveArticleShareOptions(71 config: Pick<ArticleConfig, "share">,72): ArticleShareOptions | null {73 if (!config.share.enable) return null;74 return { includeCover: config.share.includeCover };75}76
77export function resolveLastUpdatedNoticeOptions(78 config: Pick<ArticleConfig, "lastUpdated">,79): ArticleConfig["lastUpdated"] | null {80 return config.lastUpdated.enable ? config.lastUpdated : null;81}1import type { CommentConfig, TwikooConfig } from "@/types/commentConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 评论系统配置单一真源。6 *7 * 遵循「零额外负担」原则:默认全局关闭(enable: false),8 * 在未开启时不产生任何外部网络请求、零额外 DOM 占位与零包体积膨胀。9 *10 * 【开启 Twikoo 评论配置步骤】11 * 1. 部署 Twikoo 服务端并获取环境 ID(腾讯云 CloudBase / Vercel / Railway / 私有部署等);12 * 2. 将 `enable` 置为 `true`,并将 `provider` 设置为 `"twikoo"`;13 * 3. 填入你的 `twikoo.envId`;14 * 4. (可选)自定义 `scriptUrl`(如使用自建 CDN 或官方 unpkg/jsdelivr 源)。15 */16export const commentConfig: CommentConfig = withUserConfig("comment", {17 /** 全局评论总开关:false 时完全不加载评论脚本与 DOM */18 enable: false,19 /** 评论提供商类型:"none" | "twikoo" */20 provider: "none",21 /** 是否开启视口懒加载:滚动进入视口才动态加载评论组件(推荐 true) */22 lazy: true,23 /** Twikoo 专有配置 */24 twikoo: {25 /** Twikoo 环境 ID(如 "https://your-twikoo.vercel.app" 或腾讯云环境 ID) */26 envId: "",27 /** Twikoo 前端 JS 脚本 CDN 地址 */28 scriptUrl: "https://cdn.jsdelivr.net/npm/twikoo@1.7.19/dist/twikoo.min.js",29 /** 评论语言:"auto"(跟随站点)| "zh-CN" | "zh-TW" | "en" | "ja" 等 */30 lang: "auto",31 /** 评论输入框占位提示文本 */32 placeholder: "说点什么吧…",33 },34});35
36export type ResolvedCommentOptions = {37 provider: "twikoo";38 lazy: boolean;39 twikoo: TwikooConfig;40} | null;41
42/**43 * 解析并校验评论配置。未启用、提供商为 none 或关键参数缺失时返回 null。44 */45export function resolveCommentOptions(46 config: CommentConfig,47): ResolvedCommentOptions {48 if (!config.enable || config.provider === "none") {49 return null;50 }51 if (config.provider === "twikoo") {52 const envId = config.twikoo.envId?.trim();53 const scriptUrl = config.twikoo.scriptUrl?.trim();54 if (!envId || !scriptUrl) {55 return null;56 }57 return {58 provider: "twikoo",59 lazy: config.lazy,60 twikoo: {61 ...config.twikoo,62 envId,63 scriptUrl,64 },65 };66 }67 return null;68}1import type { ContextMenuConfig } from "@/types/contextMenuConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/** Optional desktop context-menu enhancement. */5export const contextMenuConfig: ContextMenuConfig = withUserConfig(6 "contextMenu",7 {8 enable: true,9 actions: ["copySelection", "backToTop", "sharePageLink"],10 },11);1import type { DevicesConfig } from "@/types/devicesConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 设备展示页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /devices/ 跳转 404;9 * - categories:场景分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledIds:可选被禁用的设备 ID 列表;11 *12 * 注:设备的具体清单数据(设备名、品牌、规格、感受说明、图片等)请在 `src/data/devices.ts` 中维护。13 */14export const devicesConfig: DevicesConfig = withUserConfig("devices", {15 enable: true,16 categories: [17 {18 key: "desk",19 label: "桌面工作台",20 icon: "material-symbols:desktop-windows-outline-rounded",21 description: "工作站与家庭办公硬件",22 },23 {24 key: "mobile",25 label: "移动与 EDC",26 icon: "material-symbols:phone-iphone",27 description: "日常随身设备与智能小物",28 },29 {30 key: "audio",31 label: "影音设备",32 icon: "material-symbols:headphones-rounded",33 description: "耳机、音箱与监听设备",34 },35 {36 key: "peripheral",37 label: "外设",38 icon: "material-symbols:keyboard-outline-rounded",39 description: "键盘、鼠标与桌面配件",40 },41 ],42 // disabledIds: [],43});1import type { ExpressiveCodeConfig } from "@/types/config";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * Expressive Code 代码块主题(astro.config.mjs 与 setting-utils 消费)。6 * 类型见 src/types/config.ts。7 */8export const expressiveCodeConfig: ExpressiveCodeConfig = withUserConfig(9 "expressiveCode",10 {11 // Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.12 // 代码块跟随明暗模式切换深浅主题13 theme: "github-dark",14 lightTheme: "github-light",15 darkTheme: "github-dark",16 },17);1import type { FabConfig } from "@/types/fabConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 右下角悬浮控制流(FAB)导航配置。6 *7 * 【核心配置项】8 * - enable:是否开启悬浮操作栏;9 * - align:"start"(靠左)| "end"(靠右,默认);10 * - size:"small" | "regular"(默认)| "large";11 * - offset:右下角边距(支持 CSS 变量或具体像素);12 * - items:操作按钮清单(按数组顺序渲染):13 * - type: "top" —— 平滑返回顶部按钮(滚过横幅后自动浮现);14 * - type: "toc" —— 悬浮文章目录面板(桌面端已有侧栏粘性 TOC,默认仅在 mobile/tablet 显示);15 * - type: "comment" —— 直达评论区按钮(评论系统关闭或文章关闭评论时零 DOM 产物);16 * - type: "home" —— 返回首页按钮(onlySubPages: true 表示仅在非首页展示);17 * - devices:受控设备矩阵("mobile" | "tablet" | "desktop"),省略表示全设备生效;18 * - pages:页面范围过滤(如 ["post"])。19 *20 * 架构规范见 docs/fab-system.md。21 */22export const fabConfig: FabConfig = withUserConfig("fab", {23 enable: true,24 align: "end",25 size: "regular",26 offset: {27 bottom: "var(--m3e-space-8)",28 right: "var(--m3e-space-6)",29 },30 items: [31 {32 type: "top",33 enable: true,34 devices: ["mobile", "tablet", "desktop"],35 },36 {37 type: "toc",38 enable: true,39 devices: ["mobile", "tablet"],40 pages: ["post"],41 depth: 3,42 closeOnSelect: true,43 },44 {45 type: "comment",46 enable: true,47 devices: ["mobile", "tablet"],48 pages: ["post"],49 },50 {51 type: "home",52 enable: true,53 devices: ["mobile", "tablet"],54 onlySubPages: true,55 },56 ],57});1import type { FontConfig, ResolvedFontOptions } from "../types/fontConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3import { resolveFontOptions as resolve } from "../utils/font-options.ts";4
5/**6 * ─────────────────────────────────────────────────────────────────────────────7 * Shirone 全站字体配置指南8 * ─────────────────────────────────────────────────────────────────────────────9 *10 * 博客的字体分为 3 种角色(Role),每个角色各司其职:11 * 1. `body`:西文与默认基础正文字体(英文字母、数字、基础标点)12 * 2. `cjk` :中日韩字体(汉字、日文平假名/片假名、韩文)13 * 3. `mono`:等宽代码字体(文章代码块、行内代码、终端输出)14 *15 * ─────────────────────────────────────────────────────────────────────────────16 * 【常见修改场景】17 * ─────────────────────────────────────────────────────────────────────────────18 * 场景 A:完全使用系统默认字体(零字体打包,极速加载,最省流量)19 * - 将 `mode` 设置为 `"system"`,并将 `fontFamilies` 设为空数组 `[]`。20 *21 * 场景 B:更换本地中文字体或英文字体(.woff2 文件)22 * 1. 准备你的 `.woff2` 字体文件,放入项目 `src/assets/fonts/` 目录下;23 * 2. 找到对应角色的配置(如 `role: "cjk"` 或 `role: "body"`);24 * 3. 设置 `source: "local"`,并在 `file` 中填入你的字体路径(例如 `"src/assets/fonts/MyFont.woff2"`);25 * 4. 将 `family` 设为该字体的真实族名称。26 *27 * 场景 C:使用 npm 的 Fontsource 字体包28 * 1. 安装字体包(如 `pnpm.cmd add @fontsource/inter`);29 * 2. 设置 `source: "fontsource"`,并在 `file` 中填入对应的 CSS 路径(如 `"@fontsource/inter/400.css"`);30 * 3. 将 `family` 设为对应的字体名称(如 `"Inter"`)。31 *32 * ─────────────────────────────────────────────────────────────────────────────33 * 【修改后的验证命令】34 * 在终端依次执行:35 * 1. `npx.cmd astro check` -> 校验配置与页面语法36 * 2. `pnpm.cmd build` -> 执行生产构建与字体打包37 * 3. `pnpm.cmd fonts:check` -> 校验字体格式与体积预算38 * ─────────────────────────────────────────────────────────────────────────────39 */40export const fontConfig: FontConfig = withUserConfig("font", {41 /**42 * 构建模式:43 * - `"custom"`: 启用自定义字体(加载下方 fontFamilies 中配置的字体)44 * - `"system"`: 纯系统字体模式(不打包任何自定义字体文件,完全依赖访客设备)45 */46 mode: "custom",47
48 /**49 * 字体清单列表(按需配置 body、cjk、mono 角色)50 */51 fontFamilies: [52 // ---------------------------------------------------------------------53 // 1. 正文字体(现代几何圆润西文字体 Outfit,与 M3E 大圆角及悠哉圆体绝配)54 // ---------------------------------------------------------------------55 {56 id: "outfit-body",57 family: "Outfit",58 role: "body",59 source: "local",60 variants: [61 {62 file: "src/assets/fonts/outfit-latin-400-normal.woff2",63 weight: 400,64 style: "normal",65 },66 {67 file: "src/assets/fonts/outfit-latin-500-normal.woff2",68 weight: 500,69 style: "normal",70 },71 {72 file: "src/assets/fonts/outfit-latin-700-normal.woff2",73 weight: 700,74 style: "normal",75 },76 ],77 fallback: ["ui-sans-serif", "system-ui", "sans-serif"],78 display: "swap",79 preload: false,80 },81
82 // ---------------------------------------------------------------------83 // 2. 中文 / 日文 CJK 字体(悠哉圆体 Yozai Medium,全量简繁中日韩 100% 覆盖)84 // ---------------------------------------------------------------------85 {86 id: "yozai-cjk",87 family: "Yozai Medium",88 role: "cjk",89 source: "local",90 variants: [91 {92 file: "src/assets/fonts/Yozai-Medium.ttf",93 weight: 500,94 style: "normal",95 },96 ],97 fallback: ["system-ui", "sans-serif"],98 display: "swap",99 preload: false,100 },101
102 // ---------------------------------------------------------------------103 // 3. 代码等宽字体(渲染代码块与终端文本,对应 CSS 变量 --font-mono)104 // ---------------------------------------------------------------------105 {106 id: "jetbrains-mono",107 family: "JetBrains Mono",108 role: "mono",109 source: "local",110 variants: [111 {112 file: "src/assets/fonts/jetbrains-mono-latin-wght-normal.woff2",113 weight: "100 800",114 style: "normal",115 },116 {117 file: "src/assets/fonts/jetbrains-mono-latin-wght-italic.woff2",118 weight: "100 800",119 style: "italic",120 },121 ],122 fallback: [123 "ui-monospace",124 "SFMono-Regular",125 "Menlo",126 "Monaco",127 "Consolas",128 "monospace",129 ],130 display: "swap",131 preload: false,132 },133 ],134
135 /**136 * 字体子集化配置(生产构建时自动从文章、i18n、配置及 Meting 歌曲中提取字符,生成极速精简版 .woff2)137 * - Dev 开发环境:自动加载完整原字体,任意输入新汉字实时可见,极速 HMR 零等待;138 * - Build 生产构建:自动执行子集裁剪,将几十兆大字体压缩为几百 KB 的专属子集,秒开加载。139 */140 subsetting: {141 enable: true, // 启用自动化子集裁剪142 includeContent: true, // 扫描 src/content/ 下所有文章143 includeI18n: true, // 扫描全部 10 种语言词典144 includeConfig: true, // 扫描站点配置与导航145 includeCommon: true, // 包含常用标点与基础字符146 allowRemoteText: true, // 允许抓取 Meting 云端歌单曲目文本参与字形提取147 },148
149 /**150 * 字体打包体积预算限制(子集化后通常仅 300KB ~ 1MB)151 */152 budget: {153 maxTotalBytes: 6 * 1024 * 1024, // 全站引用自定义字体总大小上限:6MB154 maxFamilyBytes: 4 * 1024 * 1024, // 单个字体族文件大小上限:4MB155 },156});157
158/** 经过校验与标准化处理后的字体配置对象,由 Astro 模板与 CSS 消费 */159export const resolvedFontOptions: ResolvedFontOptions = resolve(fontConfig);160
161/** 字体配置解析与校验函数 */162export const resolveFontOptions: (config: FontConfig) => ResolvedFontOptions =163 resolve;1<!--2 在这里添加自定义的页脚 HTML 内容(例如 ICP 备案号、公安备案图标与链接、自定义声明等)。3 需要在 src/config/footerConfig.ts 中保持 enable: true;4 内容将注入在页脚版权信息上方;若文件留空或仅保留注释,则不会产生任何额外的 DOM 结构。5-->1import type { FooterConfig } from "@/types/footerConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 页脚自定义 HTML 注入配置。6 * 开启后将读取 src/config/FooterConfig.html 文件内容并注入到页脚版权信息上方。7 * 关闭时(enable: false)零额外 DOM 占位、零文件读取开销。8 */9export const footerConfig: FooterConfig = withUserConfig("footer", {10 enable: false,11});1/**2 * Tonal Bloom(色调辉光占位)默认配置。3 * 与 M3E HCT 色彩系统同源,为全站图片提供防抖动尺寸占位与色彩过渡体验。4 */5import type { ImageBloomConfig } from "@/types/imageBloomConfig";6import { withUserConfig } from "../utils/config-overlay.ts";7
8export const imageBloomConfig: ImageBloomConfig = withUserConfig("imageBloom", {9 enable: true,10 blurRadius: 20,11 opacity: 0.7,12 transitionDuration: 300,13});14
15export function resolveImageBloomOptions(16 config: Partial<ImageBloomConfig> = imageBloomConfig,17): ImageBloomConfig {18 return {19 enable: config.enable ?? true,20 blurRadius: config.blurRadius ?? 20,21 opacity: config.opacity ?? 0.7,22 transitionDuration: config.transitionDuration ?? 300,23 };24}1/**2 * 配置统一出口(barrel):消费方一律 `import { xxx } from "@/config"`。3 *4 * 约定(详见本目录 README.md):5 * - 值放在 `src/config/<domain>Config.ts`,类型放在 `src/types/<domain>Config.ts`;6 * - 存在反向依赖的模块(如 i18n/translation.ts 依赖 siteConfig)只允许从7 * 具体文件导入(`@/config/siteConfig`),禁止走本 barrel,避免循环依赖。8 */9
10export {11 animeConfig,12 resolveAnimeOptions,13 resolvedAnimeOptions,14} from "./animeConfig";15export { announcementConfig } from "./announcementConfig";16export {17 type ArticleDiscoveryOptions,18 type ArticleShareOptions,19 articleConfig,20 normalizeDiscoveryCount,21 resolveArticleDiscoveryOptions,22 resolveArticleShareOptions,23 resolveLastUpdatedNoticeOptions,24} from "./articleConfig";25export {26 commentConfig,27 type ResolvedCommentOptions,28 resolveCommentOptions,29} from "./commentConfig";30export { contextMenuConfig } from "./contextMenuConfig";31export { devicesConfig } from "./devicesConfig";32export { expressiveCodeConfig } from "./expressiveCodeConfig";33export { fabConfig } from "./fabConfig";34export {35 fontConfig,36 resolvedFontOptions,37 resolveFontOptions,38} from "./fontConfig";39export { footerConfig } from "./footerConfig";40export {41 imageBloomConfig,42 resolveImageBloomOptions,43} from "./imageBloomConfig";44export { licenseConfig } from "./licenseConfig";45export { llmsConfig } from "./llmsConfig";46export {47 clampMusicVolume,48 musicConfig,49 type ResolvedMusicOptions,50 resolveMusicOptions,51} from "./musicConfig";52export { LinkPresets, navBarConfig } from "./navBarConfig";53export { permalinkConfig } from "./permalinkConfig";54export { POST_CARD_MIN_WIDTH, postListConfig } from "./postListConfig";55export { profileConfig } from "./profileConfig";56export { projectsConfig } from "./projectsConfig";57export { sidebarConfig } from "./sidebarConfig";58export {59 getDefaultSpec,60 getDefaultStyle,61 resolveDisplaySettings,62 resolveTextureOptions,63 siteConfig,64} from "./siteConfig";65export { skillsConfig } from "./skillsConfig";66export { timelineConfig } from "./timelineConfig";67export {68 type ResolvedUmamiOptions,69 resolveUmamiOptions,70 umamiConfig,71} from "./umamiConfig";1import type { LicenseConfig } from "@/types/config";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 文章版权声明(文章页 License 区块消费)。类型见 src/types/config.ts。6 */7export const licenseConfig: LicenseConfig = withUserConfig("license", {8 enable: true,9 name: "CC BY-NC-SA 4.0",10 url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",11});1import type { LlmsConfig } from "@/types/llmsConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * ─────────────────────────────────────────────────────────────────────────────6 * Shirone LLMs.txt 与 AI 友好内容系统配置指南7 * ─────────────────────────────────────────────────────────────────────────────8 *9 * 遵循「零额外负担」原则与 https://llmstxt.org/ 官方规范:10 * - 为大语言模型 (ChatGPT, Claude, Perplexity, Cursor 等) 提供结构化 Markdown 索引;11 * - 纯服务端静态生成 `/llms.txt`(精简索引)与 `/llms-full.txt`(全量正文汇编);12 * - 客户端 JS 主包增加 0 KB,前台读者浏览速度 0 影响;13 * - 安全隔离:自动过滤密码保护文章 (encrypted: true) 与草稿 (draft: true),绝不泄漏私密内容。14 *15 * ─────────────────────────────────────────────────────────────────────────────16 * 【自动化机制说明(平时写作无需维护本文件)】17 * ─────────────────────────────────────────────────────────────────────────────18 * 1. 公开博客文章(Articles):19 * - 构建期系统自动调用 `getSortedPosts()` 扫描全站 Markdown 文件;20 * - 自动提取每篇文章的标题、链接、简介与标签,无需手动登记!21 * 2. 站点基本信息:22 * - 站点标题、副标题与简介默认自动继承 `siteConfig` 与 `profileConfig`;23 * 3. 正文脱敏与清洗:24 * - `/llms-full.txt` 自动展开 `<llm-only>` AI 专属提示,自动剔除 `<llm-exclude>` 内容。25 *26 * ─────────────────────────────────────────────────────────────────────────────27 * 【常用配置场景】28 * ─────────────────────────────────────────────────────────────────────────────29 * 场景 A:使用默认配置(开箱即用,最推荐)30 * - 保持下方默认配置即可,全站文章自动收录并生成 `/llms.txt` 与 `/llms-full.txt`。31 *32 * 场景 B:完全关闭 AI 检索端点33 * - 将 `enable` 设置为 `false`(访问对应链接返回 404,不生成任何静态文件)。34 *35 * 场景 C:文章量极大时仅生成精简目录,不生成超长全文 dump36 * - 将 `generateFull` 设置为 `false`(只生成 `/llms.txt`,跳过 `/llms-full.txt`)。37 *38 * 场景 D:防止某些私密标签被大模型检索39 * - 在 `excludeTags` 中追加标签名,例如:`excludeTags: ["secret", "private", "diary"]`。40 *41 * 场景 E:内容仓(external 模式)覆盖42 * - 在内容仓 `config/llms.yaml` 里只写想改的键即可(如 `siteSummary`、`excludeTags`);43 * - 合并规则为「对象递归合并,数组整体替换」,因此改 `corePages` / `customSections`44 * 需要把整个清单写全。契约见 `docs/content-separation/config-overlay.md`。45 * ─────────────────────────────────────────────────────────────────────────────46 */47export const llmsConfig: LlmsConfig = withUserConfig("llms", {48 /**49 * 是否启用 /llms.txt 与 /llms-full.txt 静态端点生成50 * - true (默认): 构建期自动在 dist/ 输出纯文本 Markdown 文件;51 * - false: 彻底禁用此功能,访问返回 404,不产生任何构建文件。52 */53 enable: true,54
55 /**56 * 是否同时生成包含全站公开文章完整正文的 /llms-full.txt 文件57 * - true (默认): 将所有公开非加密文章的正文清洗后合并为一个文件,方便 AI 全量学习与 RAG 导入;58 * - false: 仅生成目录索引 /llms.txt,不生成全量正文。59 */60 generateFull: true,61
62 /**63 * 站点在大模型眼中的自我介绍(可选)64 * - 省略或留空时:自动回退使用 `siteConfig.subtitle` 或 `profileConfig.bio`;65 * - 填写字符串时:优先使用此处的自定义英文/中文介绍覆盖默认值。66 */67 siteSummary: "",68
69 /**70 * 单篇文章在 /llms.txt 目录索引中的摘要截断字数上限(默认 200 字)71 * - 超出长度时会自动在句尾添加省略号 "…";72 * - 不影响 /llms-full.txt 中的完整正文输出。73 */74 descriptionMaxLength: 200,75
76 /**77 * 敏感标签黑名单过滤(可选)78 * - 凡是包含此列表中任意标签的文章,将同时从 /llms.txt 与 /llms-full.txt 中剔除;79 * - 即使文章本身为公开状态(非加密),只要命中黑名单标签也绝不暴露给 AI 模型。80 */81 excludeTags: ["secret", "private"],82
83 /**84 * 敏感分类黑名单过滤(可选)85 * - 凡是属于此分类的文章,将彻底从 LLM 产物中排除。86 */87 excludeCategories: [],88
89 /**90 * 核心引导页面清单(Core Pages)91 * - 向大模型重点介绍站点的核心栏目与功能入口;92 * - 可填写站内相对路径(如 "/about/")或外部完整 URL;93 * - 省略或设为空数组 [] 时,系统会自动使用默认核心页面。94 */95 corePages: [96 {97 title: "首页",98 url: "/",99 description: "博客主入口与最新文章流。",100 },101 {102 title: "关于",103 url: "/about/",104 description: "作者简介、技术栈与背景。",105 },106 {107 title: "归档",108 url: "/archive/",109 description: "全部已发布文章的时间线索引。",110 },111 ],112
113 /**114 * 自定义扩展章节(可选)115 * - 用于向 AI Agent 额外推荐外部开源项目、API 文档或衍生资源;116 * - 默认为空数组 [],不输出额外章节。117 *118 * 示例:119 * ```ts120 * customSections: [121 * {122 * title: "Open Source Projects",123 * description: "Featured open source repositories maintained by the author.",124 * items: [125 * { title: "Shirone Theme", url: "https://github.com/LyraVoid/Shirone", description: "M3E blog theme for Astro." },126 * ],127 * },128 * ]129 * ```130 */131 customSections: [],132});1import { musicTracks } from "../data/music.ts";2import type {3 MetingMusicConfig,4 MusicConfig,5 MusicProvider,6 PlaybackMode,7 TrackDescriptor,8} from "../types/musicConfig.ts";9import { withUserConfig } from "../utils/config-overlay.ts";10
11/**12 * 侧栏音乐配置单一真源。13 * 遵循「零额外负担」原则:禁用时不产生任何网络请求与额外 DOM。14 *15 * ─────────────────────────────────────────────────────────────────────────────16 * 【四种工作模式(Provider)使用指南】17 * ─────────────────────────────────────────────────────────────────────────────18 * 1. "local"(本地独立模式,默认):19 * - 数据源:src/data/music.ts20 * - 特点:零外部 API 依赖,首屏毫秒级就绪,静态打包直出,断网也能正常播放。21 * - 示例:22 * provider: "local"23 *24 * 2. "custom"(自定义列表模式):25 * - 数据源:直接在 tracks 字段显式传入曲目数组(支持外链音频与封面)26 * - 特点:灵活自定义,无需修改通用数据文件。27 * - 示例:28 * provider: "custom",29 * tracks: [30 * { id: "song-1", title: "Song", artist: "Artist", source: "https://.../a.mp3", cover: "https://.../c.jpg" }31 * ]32 *33 * 3. "meting"(云端歌单模式):34 * - 数据源:Meting API 远端歌单(网易云 / QQ音乐 / 酷狗等)35 * - 特点:客户端异步按需拉取,海量曲库与封面自动解析。36 * - 示例:37 * provider: "meting",38 * meting: { server: "netease", type: "playlist", id: "14164869977" }39 *40 * 4. "mixed"(混合增强模式,推荐):41 * - 数据源:本地曲目(src/data/music.ts)+ Meting API 远端歌单自动合并42 * - 特点:首屏立即可播本地音乐,后台无感拉取远端歌单并在就绪后无缝扩容;43 * 若遇断网或云端接口故障,自动静默降级为本地曲目播放,绝不报红破版。44 * - 示例:45 * provider: "mixed",46 * meting: { server: "netease", type: "playlist", id: "14164869977" }47 * ─────────────────────────────────────────────────────────────────────────────48 */49export const musicConfig: MusicConfig = withUserConfig("music", {50 enable: true,51 provider: "mixed",52 // tracks: [53 // {54 // id: "custom-1",55 // title: "示例曲目",56 // artist: "艺术家",57 // cover: "/assets/music/cover/example.webp",58 // source: "/assets/music/url/example.mp3",59 // duration: 240,60 // },61 // ],62 meting: {63 server: "netease",64 type: "playlist",65 id: "14164869977",66 },67 defaultVolume: 0.7,68 defaultMode: "sequence",69});70
71export interface ResolvedMusicOptions {72 readonly provider: MusicProvider;73 readonly playlist: readonly TrackDescriptor[];74 readonly meting?: MetingMusicConfig;75 readonly defaultVolume: number;76 readonly defaultMode: PlaybackMode;77}78
79const ABSOLUTE_MEDIA_SOURCE = /^(?:https?:)?\/\//i;80const UNSAFE_SCHEME = /^[a-z][a-z\d+.-]*:/i;81
82function normalizeMediaSource(value: string): string | null {83 const source = value.trim();84 if (!source) return null;85 if (ABSOLUTE_MEDIA_SOURCE.test(source) || source.startsWith("/")) {86 return source;87 }88 if (UNSAFE_SCHEME.test(source)) return null;89 return `/${source.replace(/^\.\//, "")}`;90}91
92function normalizeTrack(93 track: TrackDescriptor,94 usedIds: Set<string>,95): TrackDescriptor | null {96 const id = track.id.trim();97 const title = track.title.trim();98 const source = normalizeMediaSource(track.source);99 if (!id || !title || !source || usedIds.has(id)) return null;100
101 usedIds.add(id);102 const artist = track.artist?.trim() || undefined;103 const cover = track.cover104 ? (normalizeMediaSource(track.cover) ?? undefined)105 : undefined;106 const duration =107 typeof track.duration === "number" &&108 Number.isFinite(track.duration) &&109 track.duration > 0110 ? track.duration111 : undefined;112
113 return Object.freeze({ id, title, source, artist, cover, duration });114}115
116export function clampMusicVolume(value: number, fallback = 0.7): number {117 if (!Number.isFinite(value)) return fallback;118 return Math.min(1, Math.max(0, value));119}120
121export function resolveMusicOptions(122 config: MusicConfig,123): ResolvedMusicOptions | null {124 if (!config.enable) return null;125
126 const provider: MusicProvider = config.provider ?? "local";127
128 if (provider === "meting") {129 const id = config.meting?.id?.trim();130 if (!id) return null;131 return Object.freeze({132 provider: "meting",133 playlist: Object.freeze([]),134 meting: config.meting,135 defaultVolume: clampMusicVolume(config.defaultVolume),136 defaultMode: config.defaultMode,137 });138 }139
140 let rawTracks: readonly TrackDescriptor[] = [];141 if (provider === "local" || provider === "mixed") {142 rawTracks = config.tracks ?? musicTracks;143 } else if (provider === "custom") {144 rawTracks = config.tracks ?? [];145 }146
147 const usedIds = new Set<string>();148 const playlist = rawTracks149 .map((track) => normalizeTrack(track, usedIds))150 .filter((track): track is TrackDescriptor => track !== null);151
152 if (provider === "mixed") {153 const metingId = config.meting?.id?.trim();154 if (playlist.length === 0 && !metingId) return null;155 return Object.freeze({156 provider: "mixed",157 playlist: Object.freeze(playlist),158 meting: config.meting,159 defaultVolume: clampMusicVolume(config.defaultVolume),160 defaultMode: config.defaultMode,161 });162 }163
164 if (playlist.length === 0) return null;165
166 return Object.freeze({167 provider,168 playlist: Object.freeze(playlist),169 defaultVolume: clampMusicVolume(config.defaultVolume),170 defaultMode: config.defaultMode,171 });172}1import I18nKey from "@i18n/i18nKey";2import { i18n } from "@i18n/translation";3import { devicesConfig } from "@/config/devicesConfig";4import { projectsConfig } from "@/config/projectsConfig";5import { skillsConfig } from "@/config/skillsConfig";6import { timelineConfig } from "@/config/timelineConfig";7import type {8 NavBarConfig,9 NavBarConfigOverride,10 NavBarLink,11 NavBarLinkOverride,12} from "@/types/navBarConfig";13import { getUserConfig } from "../utils/config-overlay.ts";14
15/**16 * 导航栏配置(统一单一来源)。17 * - LinkPresets:命名链接预设表 —— 名称 / 地址 / 图标单点维护,可整体复用;18 * - navBarConfig:导航结构 —— 顺序 + 分组(children 子菜单),19 * 同时驱动顶栏下拉菜单与全端导航抽屉。20 * 新增入口:先在 LinkPresets 登记预设,再在 navBarConfig.links 按序引用。21 *22 * 内容仓可用 `config/nav-bar.yaml` 整体替换 `links`,写法见 `NavBarLinkOverride`。23 */24export const LinkPresets: Record<string, NavBarLink> = {25 Home: {26 name: i18n(I18nKey.home),27 url: "/",28 icon: "material-symbols:home-outline-rounded",29 pageKey: "home",30 },31 Archive: {32 name: i18n(I18nKey.archive),33 url: "/archive/",34 icon: "material-symbols:archive-outline-rounded",35 pageKey: "archive",36 },37 Friends: {38 name: i18n(I18nKey.friends),39 url: "/friends/",40 icon: "material-symbols:handshake-outline-rounded",41 pageKey: "friends",42 },43 Moments: {44 name: i18n(I18nKey.moments),45 url: "/moments/",46 icon: "material-symbols:auto-awesome-outline-rounded",47 pageKey: "moments",48 },49 Anime: {50 name: i18n(I18nKey.anime),51 url: "/anime/",52 icon: "material-symbols:live-tv-outline-rounded",53 pageKey: "anime",54 },55 Compass: {56 name: i18n(I18nKey.compass),57 url: "/compass/",58 icon: "material-symbols:explore-rounded",59 pageKey: "compass",60 },61 Skills: {62 name: i18n(I18nKey.skills),63 url: "/skills/",64 icon: "material-symbols:workspaces-outline-rounded",65 pageKey: "skills",66 },67 Projects: {68 name: i18n(I18nKey.projects),69 url: "/projects/",70 icon: "material-symbols:deployed-code-outline-rounded",71 pageKey: "projects",72 },73 Devices: {74 name: i18n(I18nKey.devices),75 url: "/devices/",76 icon: "material-symbols:devices-rounded",77 pageKey: "devices",78 },79 Timeline: {80 name: i18n(I18nKey.timeline),81 url: "/timeline/",82 icon: "material-symbols:timeline-rounded",83 pageKey: "timeline",84 },85 Albums: {86 name: i18n(I18nKey.albums),87 url: "/albums/",88 icon: "material-symbols:photo-library-outline-rounded",89 pageKey: "albums",90 },91 Categories: {92 name: i18n(I18nKey.categories),93 url: "/categories/",94 icon: "material-symbols:folder-outline-rounded",95 pageKey: "categories",96 },97 Tags: {98 name: i18n(I18nKey.tags),99 url: "/tags/",100 icon: "material-symbols:tag-rounded",101 pageKey: "tags",102 },103 About: {104 name: i18n(I18nKey.about),105 url: "/about/",106 icon: "material-symbols:info-outline-rounded",107 pageKey: "about",108 },109 GitHub: {110 name: "GitHub",111 url: "https://github.com/LyraVoid/Shirone",112 icon: "fa6-brands:github",113 external: true,114 pageKey: "github",115 },116};117
118const defaultNavBarConfig: NavBarConfig = {119 links: [120 LinkPresets.Home,121 LinkPresets.Archive,122 LinkPresets.Friends,123 LinkPresets.Moments,124 LinkPresets.Anime,125 LinkPresets.Compass,126 LinkPresets.Albums,127 {128 name: i18n(I18nKey.more),129 icon: "material-symbols:apps-rounded",130 children: [131 ...(timelineConfig.enable ? [LinkPresets.Timeline] : []),132 ...(projectsConfig.enable ? [LinkPresets.Projects] : []),133 ...(devicesConfig.enable ? [LinkPresets.Devices] : []),134 ...(skillsConfig.enable ? [LinkPresets.Skills] : []),135 // 分类/标签入口不进导航菜单(避免菜单项过多),预设已登记指向独立页面,136 // 需要时取消注释即可137 // LinkPresets.Categories,138 // LinkPresets.Tags,139 LinkPresets.About,140 LinkPresets.GitHub,141 ],142 },143 ],144};145
146/** `$t:home` 形式的 i18n 引用前缀;不带前缀的 name 一律按字面量处理。 */147const I18N_REFERENCE_PREFIX = "$t:";148
149function fail(message: string): never {150 throw new Error(`[config] nav-bar:${message}`);151}152
153function resolveName(name: string): string {154 if (!name.startsWith(I18N_REFERENCE_PREFIX)) return name;155
156 const key = name.slice(I18N_REFERENCE_PREFIX.length);157 if (!Object.hasOwn(I18nKey, key)) {158 fail(159 `未知的 i18n 词条 "${key}"。可用词条见 src/i18n/i18nKey.ts;` +160 " 若本意是普通文本,去掉开头的 $t: 即可。",161 );162 }163 return i18n(I18nKey[key as keyof typeof I18nKey]);164}165
166/**167 * 把内容仓的声明式导航条目还原成 `NavBarLink`。168 *169 * 预设名与 i18n 词条只有在这里才能校验(`LinkPresets` 与 `I18nKey` 都住在代码仓,170 * 生成期的 Node 脚本受路径别名所限读不到),因此错误在构建加载配置时抛出。171 */172export function resolveNavBarLinks(173 entries: readonly NavBarLinkOverride[],174 presets: Record<string, NavBarLink> = LinkPresets,175): NavBarLink[] {176 return entries.map((entry) => {177 let base: NavBarLink | null = null;178 if (entry.preset !== undefined) {179 base = presets[entry.preset] ?? null;180 if (!base) {181 fail(182 `未知的预设 "${entry.preset}"。可用预设:${Object.keys(presets).join("、")}。`,183 );184 }185 }186
187 const name =188 entry.name !== undefined ? resolveName(entry.name) : base?.name;189 if (name === undefined) {190 fail("每个条目都需要 name,或用 preset 引用一个内置预设。");191 }192
193 // 未声明 children 时沿用预设自带的子菜单(已由 ...base 带入)。194 return {195 ...base,196 name,197 ...(entry.url !== undefined ? { url: entry.url } : {}),198 ...(entry.icon !== undefined ? { icon: entry.icon } : {}),199 ...(entry.pageKey !== undefined ? { pageKey: entry.pageKey } : {}),200 ...(entry.external !== undefined ? { external: entry.external } : {}),201 ...(entry.children202 ? { children: resolveNavBarLinks(entry.children, presets) }203 : {}),204 };205 });206}207
208const userNavBar = getUserConfig("navBar") as NavBarConfigOverride | undefined;209
210export const navBarConfig: NavBarConfig = userNavBar211 ? { links: resolveNavBarLinks(userNavBar.links) }212 : defaultNavBarConfig;1import type { PermalinkConfig } from "../types/permalinkConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * Permalink 固定链接配置6 * 控制文章 URL 路由与生成模板7 */8export const permalinkConfig: PermalinkConfig = withUserConfig("permalink", {9 /** 是否启用全局 permalink 功能,关闭时使用默认的文件名作为链接 (/posts/<slug>/) */10 enable: false,11 /**12 * permalink 格式模板13 * 支持的占位符:14 * - %year% : 4 位年份 (如 2024)15 * - %monthnum% : 2 位月份 (01-12)16 * - %day% : 2 位日期 (01-31)17 * - %hour% : 2 位小时 (00-23)18 * - %minute% : 2 位分钟 (00-59)19 * - %second% : 2 位秒数 (00-59)20 * - %post_id% : 文章序号(按发布时间升序排列,最早的文章为 1)21 * - %postname% : 文章文件名(slug,通常为全小写)22 * - %raw_postname% : 文章原始文件名(保留大小写)23 * - %category% : 分类名(无分类时为 "uncategorized")24 *25 * 示例:26 * - "%year%-%monthnum%-%postname%" => "/2024-12-my-post/"27 * - "%post_id%-%postname%" => "/42-my-post/"28 * - "%category%-%postname%" => "/tech-my-post/"29 * - "%year%/%monthnum%/%day%/%postname%" => "/2024/12/01/my-post/"30 *31 * 注意:支持使用斜杠 "/" 构建嵌套路径。32 */33 format: "%postname%",34});1import type { PostCardWidth, PostListConfig } from "@/types/postListConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 文章列表页配置:分页大小与排版布局。6 *7 * 【核心配置项】8 * - pageSize:每页展示的文章数量(默认 8 篇);9 * - layout:10 * - mode:"list"(经典纵向列表)| "grid"(双列/三列卡片网格);11 * - cover:"left"(封面在左)| "right"(封面在右,默认);12 * - cardWidth(仅在 grid 模式下生效):13 * - "compact":紧凑卡片(最小宽 20rem,适合高密度展示);14 * - "regular":标准卡片(最小宽 24rem,默认推荐);15 * - "relaxed":宽松大卡(最小宽 28rem,突出大图)。16 *17 * 注意:访客可在前端显示设置面板中动态切换 list/grid,此处为站点初始默认值。18 * GridUI 仅在主内容容器至少能容纳两张所选宽度的卡片时生效;侧栏等因素压窄19 * 内容后会暂时回退 ListUI,但保留 grid 偏好,空间恢复后自动切回。20 */21export const postListConfig: PostListConfig = withUserConfig("postList", {22 pageSize: 8,23 layout: {24 mode: "list",25 cover: "right",26 cardWidth: "regular",27 },28});29
30/** grid 档位 → 卡片最小宽度(--post-card-min 预设,与 shape/type 分档哲学同构)。31 页面框架 90rem:regular 24rem 保证宽屏为 2 列大卡(3 列窄卡会让32 日期/分类/字数元信息行换行),compact 才给密排选项。 */33export const POST_CARD_MIN_WIDTH: Record<PostCardWidth, string> = {34 compact: "20rem",35 regular: "24rem",36 relaxed: "28rem",37};1import type { ProfileConfig } from "@/types/config";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 博主资料:头像 / 名称 / 简介 / 社交链接(侧栏 Profile 卡片、页脚、RSS 作者等消费)。6 * 类型见 src/types/config.ts。7 */8export const profileConfig: ProfileConfig = withUserConfig("profile", {9 avatar: "assets/images/demo-avatar.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'10 name: "Shirone",11 bio: "雨水记得天空忘记说的话。",12 links: [13 {14 name: "Twitter",15 icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes16 // You will need to install the corresponding icon set if it's not already included17 // `pnpm add @iconify-json/<icon-set-name>`18 url: "https://twitter.com",19 },20 {21 name: "Steam",22 icon: "fa6-brands:steam",23 url: "https://store.steampowered.com",24 },25 {26 name: "GitHub",27 icon: "fa6-brands:github",28 url: "https://github.com/LyraVoid/Shirone",29 },30 ],31});1import type { ProjectsConfig } from "@/types/projectsConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 项目页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /projects/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledKeys:可选被禁用的项目 key 列表(例如 ["folkpatch"]);11 *12 * 注:项目的具体内容数据(标题、描述、技术栈、链接、封面等)请在 `src/data/projects.ts` 中维护。13 */14export const projectsConfig: ProjectsConfig = withUserConfig("projects", {15 enable: true,16 categories: [17 {18 key: "theme",19 label: "主题",20 icon: "material-symbols:palette-outline-rounded",21 },22 {23 key: "android",24 label: "Android",25 icon: "material-symbols:android-rounded",26 },27 ],28 // disabledKeys: [],29});1# 配置目录约定2
3本目录是 Shirone 全部用户可选配置的唯一入口。约定如下:4
5## 文件组织6
7| 内容 | 位置 | 示例 |8|---|---|---|9| 配置值(带注释的默认值) | `src/config/<domain>Config.ts` | `siteConfig.ts`、`sidebarConfig.ts`、`fabConfig.ts` |10| 配置类型 | `src/types/<domain>Config.ts` | `types/sidebarConfig.ts`、`types/fabConfig.ts` |11
12通用类型(多领域共享,如 `Favicon`、`LIGHT_DARK_MODE`)放在 `src/types/config.ts`。13
14## 导入规则15
161. **消费方统一从 barrel 导入**:`import { siteConfig, fabConfig } from "@/config"`;17 只需要单一领域时可用具体文件:`import { fabConfig } from "@/config/fabConfig"`。182. **禁止相对路径杂写法**:不允许 `../../config`、`../config`、`src/config` 三种历史写法。193. **循环依赖规避**:`i18n/translation.ts` 依赖 `siteConfig`,而 `navBarConfig` 等又消费20 i18n——该类反向依赖模块只允许从具体文件导入(如 `@/config/siteConfig`),21 **禁止走 barrel**,否则形成 `index → navBar → translation → index` 环。224. `astro.config.mjs` 在 Astro 配置层运行,用相对路径 `./src/config/<file>.ts` 导入。23
24## 配置(Behavior)与数据(Content)分层原则25
26Shirone 遵循「配置管行为,数据管内容」的清晰分层架构:27
28- **`src/config/*Config.ts`**:控制**展示行为与页面能力**(页面总开关 `enable`、分类显示顺序 `categories`、单项禁用列表 `disabledKeys`、排序方向 `order`、源切换与服务凭据);29- **`src/data/*.ts`**:承载**具体的站点内容实体**(项目条目 `projects.ts`、技能清单 `skills.ts`、时间线节点 `timeline.ts`、设备列表 `devices.ts`、友链 `friends.ts`、罗盘 `compass.ts`、番剧 `anime.ts` 与本地音乐 `music.ts`);30- **`src/utils/feature-data.ts`**:提供构建期纯函数,将 config 的过滤/排序等行为规则应用到对应 data 实体集合上,输出给页面/组件。31
32| 判别问题 | 归属 | 处理方式 |33|---|---|---|34| 它控制「页面是否开启 / 排序 / 凭据」? | **Config** | 写在 `src/config/*Config.ts`(如 `enable: boolean`, `categories`, `order`) |35| 它是「站点要展示的具体条目与说明」? | **Data** | 写在 `src/data/*.ts`(如 `ProjectItem[]`, `SkillItem[]`, `TimelineItem[]`) |36| 单项内容的停用 / 过滤? | **Config** | 在 config 中声明 `disabledKeys`(或对应 ID 列表),data 保持纯净内容 |37
38## 新增一个配置项 / 配置文件39
401. 类型定义加入 `src/types/<domain>Config.ts`(新领域则新建文件,字段带中文注释说明语义与默认值);412. 值加入 `src/config/<domain>Config.ts`,保持注释完整——注释是配置的文档;423. 新文件在 `src/config/index.ts` barrel 注册导出;用 `withUserConfig("<domain>", { ... })`43 包住默认值字面量,并在 `scripts/content/config-domains.mjs` 登记该领域(见下文「用户覆盖层」);444. **安全默认与零额外负担**:可选外部服务/重量级特性默认必须为关闭(如 `enable: false`)。在关闭或未配置时,必须满足「零外部请求、零占位 DOM、零性能损耗、零主包膨胀」的零额外负担要求;45 落地做法与验证方法见 `docs/on-demand-loading.md`;465. UI 文案走 `I18nKey` 枚举 + `i18n()`(如 `navBarConfig` 的用法),**不写死字符串**;47 新增 i18n key 必须同步补全 `src/i18n/languages/` 下全部 10 种语言;486. 跑 `npx.cmd astro check` 确认 0 错误 0 警告。49
50## 用户覆盖层(内容仓 `config/*.yaml`)51
52本目录的每个领域配置都把自己的字面量默认值交给 `withUserConfig()`:53
54```typescript55export const siteConfig: SiteConfig = withUserConfig("site", {56 title: "Shirone",57 // ...默认值连同注释一起留在代码仓58});59```60
61`local` 模式下 `withUserConfig()` 原样返回默认值,零开销。`external` 模式下,62内容仓 `config/site.yaml` 里的键会与默认值**深合并**(对象递归合并、数组整体替换)63后返回,合并源是 `pnpm content:sync` 生成的 `src/user/user-config.ts`。64
65因此本目录的定位没有变:**它是默认值与配置文档的唯一真源**,66注释写得越清楚,内容仓那边越不需要猜。契约与 YAML 写法见67[`docs/content-separation/config-overlay.md`](../../docs/content-separation/config-overlay.md)。68
69新增配置领域时,除了本文下方的清单,还要在70`scripts/content/config-domains.mjs` 补一行登记(领域名、YAML 文件名、类型),71它同时驱动生成、校验与 `content:eject` 的起步文件;`tests/content/content-config.test.mjs`72会检查登记表的文件名唯一且为 kebab-case。73
74`navBarConfig` 是唯一不走 `withUserConfig()` 的领域:导航项要引用 `LinkPresets`75并调用 `i18n()`,深合并只会得到一堆未解析的引用,因此它由 `resolveNavBarLinks()`76把内容仓的声明式条目还原成 `NavBarLink`。77
78### 反向导出覆盖层(`content:export --config`)79
80覆盖层是双向的:`pnpm content:export --config` 会求「当前生效配置」与「主题默认值」的差,81把最小覆盖集写回内容仓的 `config/*.yaml`(保留用户已有的注释与格式,只增改不删键)。82它是 `deepMerge` 的精确逆运算,因此喂回 `content:sync` 之后生效配置逐字段不变。83
84典型用途是救援:`content:clean` 会把 `src/user/user-config.ts` 重置成空覆盖层,85在那份生成物里的改动会因此丢失;导出能先把它固化成 YAML。默认只预演,用法与安全机制见86[`docs/content-separation/cli-workflows.md`](../../docs/content-separation/cli-workflows.md)。87
88两条边界要知道:89
90- **`navBar` 不参与导出**。`resolveNavBarLinks()` 的解析不可逆,`config/nav-bar.yaml` 只能手工维护;91- **不会把本目录的源码改动提升成覆盖**。直接修改 `<domain>Config.ts` 里的默认值字面量时,92 「默认值」与「生效值」同步变化,差分为空,那处改动不会进入导出计划。93 这是有意为之:把当前默认值提升成内容仓的永久覆盖,等于把配置冻结在这一版主题上。94 想固化就在 fork 里提交它,或照常在内容仓写一条对应的 YAML 覆盖。95
96领域键与 YAML 文件名一律 kebab-case 对应驼峰:`llms` ↔ `config/llms.yaml` ↔ `llmsConfig`,97`postList` ↔ `config/post-list.yaml` ↔ `postListConfig`。98
99## 现有配置一览100
101| 文件 | 职责 |102|---|---|103| `footerConfig.ts` | 页脚自定义 HTML 注入开关(控制是否读取并注入 `src/config/FooterConfig.html`,关闭时零开销) |104| `siteConfig.ts` | 站点部署 URL / base 路径 / 标题标识 / 语言 / IANA 时区 / HCT 主题色 / 背景纹理系统 / 显示设置浮层开关 / 横幅 / TOC 深度 / 进度条 / favicon(含 `getDefaultStyle` / `getDefaultSpec` / `resolveDisplaySettings` 回退值) |105| `profileConfig.ts` | 博主资料:头像 / 名称 / 简介 / 社交链接 |106| `licenseConfig.ts` | 文章版权声明 |107| `expressiveCodeConfig.ts` | 代码块明暗主题 |108| `navBarConfig.ts` | 导航栏链接(`LinkPresets` 预设表 + 组装) |109| `sidebarConfig.ts` | 侧栏编排与 widget 清单(`arrangement` 单/双栏、`side` 主栏物理侧、widget `column` 分栏标签;判别联合类型见 `types/sidebarConfig.ts`;编排指导见 `docs/sidebar-system.md`,组件文档见 `docs/sidebar-widgets.md`,新增 widget checklist 见 `docs/common-components.md` §3.1) |110| `fabConfig.ts` | 右下角悬浮控制流(FAB)配置:总开关、各操作项(返回顶部、悬浮目录、直达评论、返回首页、自定义操作)、细粒度设备受控矩阵(`devices?: ("mobile" | "tablet" | "desktop")[]`)、页面范围过滤与图标定制;架构见 `docs/fab-system.md` |111| `announcementConfig.ts` | 公告内容(侧栏 announcement widget 消费,text 为空不渲染) |112| `musicConfig.ts` | 侧栏音乐全局配置:总开关(默认关闭)、`provider` 模式切换接口、`defaultVolume` 初始音量与 `defaultMode` 初始播放模式(本地曲目清单维护在 `src/data/music.ts`);与 `sidebarConfig` 的 music 条目共同控制 `MusicSidebar`,详见下文与 `docs/sidebar-widgets.md` |113| `postListConfig.ts` | 文章列表:分页大小 + 布局(list/grid 模式、封面位置、grid 卡片宽度档位) |114| `articleConfig.ts` | 文章详情:最后更新提示、延伸阅读(相关/随机文章抽样)、以及文章尾部分享区块(总开关、海报生成与封面配置) |115| `commentConfig.ts` | 评论系统:全局开关(默认关闭)、Provider 选择(Twikoo 等)、视口懒加载与服务凭据配置 |116| `contextMenuConfig.ts` | 桌面端右键增强:可选开关(当前默认开启);配置允许页面与操作顺序,关闭时零 DOM、零监听器、零客户端资源 |117| `umamiConfig.ts` | Umami 统计:全局开关(默认关闭)、公开分享统计读取,以及可选的官方访问采集脚本配置;支持内容仓 `config/umami.yaml` 覆盖(领域键 `umami`) |118| `skillsConfig.ts` | 技能页行为控制:页面总开关、分类清单与单项禁用列表(技能内容维护在 `src/data/skills.ts`);关闭页面时导航入口同步隐藏 |119| `projectsConfig.ts` | 项目页行为控制:页面总开关、分类清单与单项禁用列表(项目内容维护在 `src/data/projects.ts`);关闭页面时导航入口同步隐藏 |120| `timelineConfig.ts` | 时间线页行为控制:页面总开关、分类清单、排序方向与单项禁用列表(时间线内容维护在 `src/data/timeline.ts`);关闭页面时导航入口同步隐藏 |121| `devicesConfig.ts` | 设备页行为控制:页面总开关、场景分类清单与单项禁用列表(设备清单维护在 `src/data/devices.ts`);关闭页面时导航入口同步隐藏 |122| `animeConfig.ts` | 番剧页与外部追番数据源:数据源选择(本地 / Bangumi 快照 / Bilibili 快照)、失败降级、提供方凭据环境配置与快照生命周期管理(本地番剧维护在 `src/data/anime.ts`) |123| `llmsConfig.ts` | 大语言模型与 AI 友好内容系统:`/llms.txt`(索引)与 `/llms-full.txt`(全量正文汇编)静态端点生成控制、加密文章过滤、排除标签与自定义章节配置;支持内容仓 `config/llms.yaml` 覆盖(领域键 `llms`) |124
125非首页 Banner 的标题、说明和可选日期由各页面通过 `MainGridLayout` 提供,并在 Swup 导航后从被替换的主内容容器同步。该上下文默认显示、不设配置开关;说明为空或与标题相同时自动省略,移动端非首页仍沿用紧凑布局并隐藏 Banner。126
127## 侧栏编排与页框宽度128
129- `arrangement: "single"`(默认)——全部 widget 渲染进唯一侧栏,页框 85rem;130- `arrangement: "dual"`——`column: "secondary"` 的 widget 进入副栏(视口 ≥ 1280px 起三列),131 其余留在主栏;页框自动放宽到 96rem,TOC 悬浮 rail 自动让位(右侧余量被副栏占据)。132 1280px 以下自动退化为单栏(只显主栏),无需配置。133- `side: "left" | "right"` 决定主栏物理侧,dual 线下副栏落在对面。134- 页框宽度用 `resolvePageWidth()`(`src/utils/responsive-utils.ts`)按编排自动解析,135 常量在 `src/constants/constants.ts`(`PAGE_WIDTH` / `PAGE_WIDTH_DUAL`),不提供手动覆盖。136
137## 右下角悬浮控制流(FAB)配置契约138
139`fabConfig.ts` 控制右下角悬浮操作栏的呈现与交互:140
141```typescript142export const fabConfig: FabConfig = {143 enable: true,144 items: [145 {146 type: "top",147 enable: true,148 icon: "material-symbols:keyboard-arrow-up-rounded",149 // 未指定 devices 时全设备生效;滚过横幅高度阈值后平滑浮现150 },151 {152 type: "toc",153 enable: true,154 icon: "material-symbols:format-list-bulleted-rounded",155 pages: ["post"],156 // 桌面端侧栏已有粘性 TOC,故默认仅在移动端与平板端显示悬浮目录157 devices: ["mobile", "tablet"],158 },159 {160 type: "comment",161 enable: true,162 icon: "material-symbols:comment-outline-rounded",163 pages: ["post"],164 devices: ["mobile", "tablet"],165 },166 {167 type: "home",168 enable: false,169 icon: "material-symbols:home-outline-rounded",170 },171 ],172};173```174
175### 核心规则与受控特性176
1771. **细粒度设备受控(`devices`)**:178 - `"mobile"`(< 768px)179 - `"tablet"`(768px ~ 1023px)180 - `"desktop"`(≥ 1024px)181 - 省略时默认所有设备均允许;SSR 阶段直接输出 Tailwind CSS 响应式类(如 `flex lg:hidden`),零首屏闪烁,CLS = 0。1822. **页面范围过滤(`pages`)**:183 - 过滤逻辑与侧栏 `SidebarPage` 统一,通过 `#swup-container` 的 `data-current-page` 在 Swup 站内导航时联动隐藏/显示。1843. **评论按钮零额外负担**:185 - 全局未开启评论系统(`commentConfig.enable: false`)或当前文章关闭评论时,评论 FAB 按钮产物为 0 DOM,零多余外链请求与布局偏移。1864. **无音乐挂件(保持纯粹)**:187 - FAB 控制流不集成音乐播放器,避免与侧边栏 `MusicSidebar` 产生双重状态混乱及包体积膨胀。188
189## 侧栏音乐启用契约与四种模式190
191侧栏音乐是默认关闭的可选能力,必须同时满足以下三项才加载并渲染:192
1931. `musicConfig.enable` 为 `true`;1942. 对应数据源(本地 `src/data/music.ts`、自定义 `tracks` 或 `meting` 远端歌单)至少包含一首有效曲目/合法歌单 ID;1953. `sidebarConfig.components` 中 `type: "music"` 的条目存在且 `enable: true`(默认条目为 `false`)。196
197### 四种数据源工作模式(`provider`)198
199| 模式 | 配置名 | 数据源 | 特点与适用场景 |200|---|---|---|---|201| **本地模式** | `"local"` | `src/data/music.ts` | 默认模式。零外部 API 依赖,构建期静态打包,首屏毫秒级就绪,支持离线/断网播放。 |202| **自定义列表** | `"custom"` | `musicConfig.tracks` | 灵活自定义。直接在配置中传入曲目数组(支持外链音频与封面),无需修改通用数据文件。 |203| **云端歌单** | `"meting"` | `musicConfig.meting` | 接入 Meting API(网易云、QQ 音乐、酷狗等),客户端异步按需拉取,曲目与封面自动清洗加载。 |204| **混合增强模式** | `"mixed"` | 本地数据 + Meting API | **推荐**。首屏立即播放本地音乐,后台无感拉取远端歌单并在就绪后无缝追加合并;若遇断网或云端接口故障,自动静默降级为本地曲目,绝不破版。 |205
206任一条件不满足时,音乐功能不得输出 DOM 或样式,不得请求音频/封面等资源,也不得把播放器代码或依赖带入主 bundle。配置消费者应先完成三项校验,再动态加载 `MusicSidebar`;不能用隐藏空卡片代替短路。207
208`defaultVolume` 与 `defaultMode` 只定义播放器首次初始化的音量和播放模式。播放器挂载后由持久侧栏运行时持有当前曲目、播放位置、音量与模式,Swup 站内导航不应重新读取默认值或重建播放器。1/**2 * 侧边栏布局配置(数据驱动编排)。3 *4 * 【核心概念】5 * 1. arrangement(侧栏编排模式):6 * - "single"(单栏,默认):所有 widget 放入主侧栏,适合紧凑布局(页框 85rem);7 * - "dual"(双栏):column: "secondary" 的 widget 放入副侧栏(视口 ≥ 1280px 展开三列,页框 96rem),8 * 在 1024px~1279px 之间会自动优雅退化为单栏,无需手动适配。9 * 2. side(主栏物理位置):10 * - "left":主侧栏在左侧(默认),dual 模式下副栏自动落右侧;11 * - "right":主侧栏在右侧,dual 模式下副栏落左侧。12 * 3. widget 属性:13 * - type:组件类型("profile" | "music" | "announcement" | "categories" | "tags" | "stats" | "calendar" | "toc");14 * - enable:是否启用该 widget;15 * - slot:"top"(固定在顶部)| "sticky"(页面滚动时吸顶跟随);16 * - column:"primary"(主栏,默认)| "secondary"(副栏,仅在 arrangement: "dual" 时生效);17 * - pages:仅在指定页面展示(如 ["home", "post"],省略时默认全页面展示);18 * - collapseAfter:折叠阈值(适用于 categories/tags,超出条数显示展开按钮)。19 *20 * 类型定义见 src/types/sidebarConfig.ts。21 */22import type { SidebarConfig } from "@/types/sidebarConfig";23import { withUserConfig } from "../utils/config-overlay.ts";24
25export const sidebarConfig: SidebarConfig = withUserConfig("sidebar", {26 enable: true,27 arrangement: "dual",28 side: "left",29 components: [30 { type: "profile", enable: true, slot: "top" },31 { type: "music", enable: true, slot: "top" },32 { type: "announcement", enable: true, slot: "top", pages: ["home"] },33 {34 type: "categories",35 enable: true,36 slot: "sticky",37 collapseAfter: 5,38 pages: [39 "home",40 "archive",41 "friends",42 "moments",43 "anime",44 "compass",45 "skills",46 "projects",47 "devices",48 "timeline",49 "albums",50 "about",51 "post",52 "categories",53 "tags",54 ],55 },56 {57 type: "tags",58 enable: true,59 slot: "sticky",60 collapseAfter: 6,61 pages: [62 "home",63 "archive",64 "friends",65 "moments",66 "anime",67 "compass",68 "skills",69 "projects",70 "devices",71 "timeline",72 "albums",73 "about",74 "post",75 "categories",76 "tags",77 ],78 },79 {80 type: "stats",81 enable: true,82 slot: "top",83 column: "secondary",84 pages: ["home", "archive", "categories", "tags"],85 },86 { type: "calendar", enable: true, slot: "top", column: "secondary" },87 {88 type: "toc",89 enable: true,90 slot: "sticky",91 column: "secondary",92 pages: ["post"],93 },94 ],95});1import type { SiteConfig } from "@/types/config";2import type {3 ResolvedTextureOptions,4 TextureConfig,5} from "@/types/textureConfig";6import { withUserConfig } from "../utils/config-overlay.ts";7
8/**9 * 站点核心配置:标题 / 语言 / 主题色(HCT 动态配色)/ 横幅 / 目录 / 进度条 / favicon。10 * 类型见 src/types/config.ts。11 */12export const siteConfig: SiteConfig = withUserConfig("site", {13 site: "https://shirone.mysqil.com/",14 base: "/",15 title: "Shirone",16 subtitle: "基于 Material 3 的二次元博客",17 // 电脑端顶栏标题与导航内容区域:"left" 左对齐,"center" 居中。18 topAppBar: {19 contentAlign: "center",20 },21 // 显示设置面板控制:配置各项前端切换项的可见性(默认全部开启)。22 displaySettings: {23 colorStyle: true, // 是否展示配色风格 9 宫格24 colorSpec: true, // 是否展示 Color Spec 调色规范切换25 wallpaperMode: true, // 是否展示页面背景(纯色/横幅)切换26 layoutMode: true, // 是否展示文章列表布局(列表/网格)切换27 reduceMotion: true, // 是否展示减少动效切换28 texture: true, // 是否展示背景纹理选择29 },30 lang: "zh_CN", // 语言代码;多语言切换已停用,全站固定简体中文(见 src/i18n/translation.ts)31 // IANA time zone for precise post and moment timestamps. It is independent of lang.32 timeZone: "Asia/Shanghai",33 themeColor: {34 hue: 315, // Default hue 0-360. 站点设计默认粉紫(偏二次元);262 紫 / 345 粉 也可选35 fixed: false, // Hide the theme color picker for visitors36 // Dynamic Material 3 palette style (TonalSpot/Vibrant/Content/Expressive/Rainbow/FruitSalad/Monochrome/Neutral/Fidelity)37 style: "tonalSpot",38 // Design spec version: "2021" (MD3) or "2025" (M3 Expressive)。角色集一致,39 // 差异仅在调色板派生(库的 colorSpec 静态为 2025 委托)40 spec: "2025",41 },42 // 默认页面背景模式:"banner" 使用壁纸横幅,"none" 使用主题纯色。43 // 访客在“显示设置”中的选择会保存在浏览器中,并覆盖这里的默认值。44 wallpaperMode: {45 defaultMode: "banner",46 },47 // 页面背景纹理系统配置(5 大精美预设 + 零开销 HCT 动态取色)48 texture: {49 enable: true, // 是否启用背景纹理系统50 defaultPreset: "starlight", // 默认纹理预设:"none" | "starlight" | "cyber-dots" | "topography" | "geometric" | "sakura"51 defaultOpacity: 0.12, // 默认纹理浓度 (0.05 ~ 0.25)52 allowMotion: true, // 是否允许背景微动效(开启 reduced-motion 时自动静止)53 },54 banner: {55 // 推荐将图片放入 src/assets,并填写相对 src 的路径,以启用构建期 AVIF/WebP 响应式优化。56 // 以 "/" 开头的 public 路径与远程 URL 仍可用,但会保留原图、不生成候选。57 // desktop 用于 >= 1024px;mobile 仅用于 < 1024px 的首页,手机非首页不显示壁纸。58 // 数组顺序就是轮播顺序;只需要静态 Banner 时,每组保留一张图片即可。59 src: {60 desktop: ["assets/images/banner/desktop/1.webp"],61 mobile: ["assets/images/banner/mobile/1.webp"],62 },63 // 图片裁切焦点:"top"、"center" 或 "bottom"。64 position: "center",65 dim: {66 // 在图片上覆盖黑色遮罩以提高标题和顶部栏的对比度;opacity 范围为 0-1。67 enable: true,68 opacity: 0.24,69 },70 homeText: {71 // 仅在首页 Banner 中显示,标题与副标题会上下居中排列。72 enable: true,73 title: "Shirone",74 subtitle: [75 "虽然没有什么特别的事,但有你在就已经足够了",76 "时至今日,你依然是我的光",77 "你知道吗,不知不觉间你已经成了我的每一天",78 "和你说话的时候,不知怎么每天都变得开心了一点",79 "今天是平凡无奇的一天。不过,也是稍微不错的一天",80 ],81 typewriter: {82 // 副标题逐字显示;关闭后直接显示完整副标题。83 enable: true,84 // 打字速度(每个字符间隔,毫秒)。85 speed: 100,86 // 回退反向删除速度(每个字符间隔,毫秒)。87 deleteSpeed: 50,88 // 打字完成后停顿时间,单位为毫秒。89 pauseTime: 2000,90 // 完成后是否循环播放;关闭表示只播放一次。91 loop: true,92 },93 },94 carousel: {95 // 是否开启多张图片自动轮播;多张图片时生效,单张图片时自动降级为静态展示。96 enable: true,97 // 轮播切换间隔时间(毫秒),运行时最小值限制为 3000ms。98 interval: 6000,99 // 交叉淡入淡出(Crossfade)过渡时长(毫秒,默认 1200ms)。100 fadeDuration: 1200,101 // 运镜呼吸动画模式:"ken-burns"(默认,循环运镜)| "zoom-in"(推进)| "zoom-out"(拉远)| "pan-left"(左移)| "pan-right"(右移)| "none"(无运镜)。102 animation: "ken-burns",103 },104 waves: {105 // 在 Banner 底部渲染页面背景色水波纹;关闭后不输出波浪 DOM。106 enable: true,107 },108 },109 // Markdown 正文图片处理;仅匹配远程图片,不会产生额外网络请求或客户端代码。110 imageOptimization: {111 // 为需要防盗链兼容的图片 CDN 添加 referrerpolicy="no-referrer",支持通配符。112 noReferrerDomains: ["*.hdslb.com"],113 },114 toc: {115 enable: true, // Display the table of contents on the right side of the post116 depth: 2, // Maximum heading depth to show in the table, from 1 to 3117 },118 progressIndicator: {119 // 进度条预设样式:dual 双向扫描(官方默认双线)/ single 单向扫描(单线)120 style: "dual",121 },122 favicon: [123 // 浏览器标签页图标,路径相对于 public 目录。124 { src: "/logo/icon.webp" },125 ],126});127
128/**129 * 解析并返回背景纹理配置选项(包含关闭短路与 0 开销优化判定)130 */131export function resolveTextureOptions(132 config: boolean | TextureConfig | undefined = siteConfig.texture,133 displaySettingsTexture: boolean = siteConfig.displaySettings?.texture ?? true,134): ResolvedTextureOptions {135 if (config === false || config === undefined) {136 return {137 enable: false,138 defaultPreset: "none",139 defaultOpacity: 0.12,140 allowMotion: false,141 };142 }143
144 if (config === true) {145 return {146 enable: true,147 defaultPreset: "starlight",148 defaultOpacity: 0.12,149 allowMotion: true,150 };151 }152
153 const enable = config.enable ?? true;154 const defaultPreset = config.defaultPreset ?? "starlight";155 const defaultOpacity = config.defaultOpacity ?? 0.12;156 const allowMotion = config.allowMotion ?? true;157
158 // 性能短路优化:159 // 如果配置 enable: false,或者 defaultPreset: "none" 且显示设置面板未允许切换(访客也无法开启),160 // 则自动视为完全关闭以达成零 DOM、零 CSS、零运行时代价。161 const effectiveEnable =162 enable && (defaultPreset !== "none" || displaySettingsTexture);163
164 return {165 enable: effectiveEnable,166 defaultPreset,167 defaultOpacity,168 allowMotion,169 };170}171
172/** 站点默认配色风格(访客未做选择时的回退值) */173export function getDefaultStyle(): string {174 return siteConfig.themeColor.style;175}176
177/** 站点默认 Color Spec(2021 / 2025) */178export function getDefaultSpec(): string {179 return siteConfig.themeColor.spec;180}181
182/** 解析并返回显示设置面板各项开关(未配置时默认 true) */183export function resolveDisplaySettings(): {184 colorStyle: boolean;185 colorSpec: boolean;186 wallpaperMode: boolean;187 layoutMode: boolean;188 reduceMotion: boolean;189 texture: boolean;190} {191 const cfg = siteConfig.displaySettings;192 const textureOpts = resolveTextureOptions(193 siteConfig.texture,194 cfg?.texture ?? true,195 );196 return {197 colorStyle: cfg?.colorStyle ?? true,198 colorSpec: cfg?.colorSpec ?? true,199 wallpaperMode: cfg?.wallpaperMode ?? true,200 layoutMode: cfg?.layoutMode ?? true,201 reduceMotion: cfg?.reduceMotion ?? true,202 texture: textureOpts.enable && (cfg?.texture ?? true),203 };204}1import type { SkillsConfig } from "@/types/skillsConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 技能页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /skills/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledNames:可选被禁用的技能名称列表(例如 ["PHP"]);11 *12 * 注:技能的具体内容数据(技能名称、熟练度等级、图标、描述等)请在 `src/data/skills.ts` 中维护。13 */14export const skillsConfig: SkillsConfig = withUserConfig("skills", {15 enable: true,16 categories: [17 {18 key: "frontend",19 label: "前端",20 icon: "material-symbols:web-rounded",21 },22 {23 key: "backend",24 label: "后端",25 icon: "material-symbols:dns-rounded",26 },27 {28 key: "tooling",29 label: "工程工具",30 icon: "material-symbols:construction-rounded",31 },32 ],33 // disabledNames: [],34});1import type { TimelineConfig } from "@/types/timelineConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 时间线页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /timeline/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - order:排序方向,默认为 "desc"(时间倒序,最新在前);可选 "asc"(正序);11 * - disabledTitles:可选被禁用的事件标题列表;12 *13 * 注:时间线的具体节点数据(标题、日期、经历描述、要点列表、关联链接等)请在 `src/data/timeline.ts` 中维护。14 */15export const timelineConfig: TimelineConfig = withUserConfig("timeline", {16 enable: true,17 categories: [18 {19 key: "milestone",20 label: "里程碑",21 icon: "material-symbols:flag-rounded",22 },23 {24 key: "project",25 label: "项目",26 icon: "material-symbols:code-rounded",27 },28 {29 key: "career",30 label: "职业",31 icon: "material-symbols:work-rounded",32 },33 {34 key: "education",35 label: "教育",36 icon: "material-symbols:school-rounded",37 },38 {39 key: "life",40 label: "生活",41 icon: "material-symbols:favorite-rounded",42 },43 ],44 order: "desc",45 // disabledTitles: [],46});1import type { ResolvedUmamiOptions, UmamiConfig } from "@/types/umamiConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * Umami 统计配置单一真源(由 oddmisc 提供)。6 *7 * 遵循「零额外负担」原则:默认全局关闭(enable: false),8 * 在未开启时不产生任何外部网络请求、零额外 DOM 占位与零包体积膨胀。9 *10 * 详细用法见:`docs/umami-guide.md`11 */12export const umamiConfig: UmamiConfig = withUserConfig("umami", {13 /** 全局 Umami 统计总开关:false 时完全不加载 oddmisc 运行时脚本与 DOM */14 enable: false,15 /** Umami 分享链接(必填) */16 shareUrl: "",17 /** Umami Website ID;与 scriptUrl 同时填写时启用访问采集 */18 websiteId: "",19 /** Umami 采集脚本 URL;与 websiteId 同时填写时启用访问采集 */20 scriptUrl: "",21});22
23/**24 * 解析并校验 Umami 配置。未启用或关键参数缺失时返回 null。25 */26export function resolveUmamiOptions(config: UmamiConfig): ResolvedUmamiOptions {27 if (!config.enable) {28 return null;29 }30 const shareUrl = config.shareUrl?.trim();31 if (!shareUrl) {32 return null;33 }34 return {35 shareUrl,36 websiteId: config.websiteId?.trim() || undefined,37 scriptUrl: config.scriptUrl?.trim() || undefined,38 };39}40
41export type { ResolvedUmamiOptions };Shirone Markdown 增强功能
https://shirone.mysqil.com/posts/markdown-enhancements/分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
沿着主题读
基于共同的标签与分类
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 14 天
部分内容可能已过时