14470 字
72 分钟
Shirone Markdown 增强
Shirone 提供了一批主题专属的 Markdown 扩展与自定义语法容器。这些扩展构建在我们原生的 unified AST 处理流水线之上,所有扩展都会在站点构建期渲染成可访问、语义化的 HTML,零客户端 JavaScript 水合开销,并且与 M3E 设计令牌 100% 对齐。
文件树#
文件树能把多层级项目结构、源码层级关系以及终端输出的目录内容,变成紧凑的交互式树形视图,并自带扩展名图标、差异高亮与可折叠分支。
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 source tree
src
components
- +Navigation.svelteadded component
- -Button.astroremoved component
content
posts
- markdown-enhancements.md
layouts
- PostLayout.astro
plugins
markdown
- rehype-file-tree.mjs
styles
markdown
- trees.css
- content.config.tsimportant file
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```Build output
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 Component Demo
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)Site Configuration
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: "The only way to do great work is to love what you do", // 公告内容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: "Share your thoughts...",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: "Desk Setup",20 icon: "material-symbols:desktop-windows-outline-rounded",21 description: "Workstation & home office hardware",22 },23 {24 key: "mobile",25 label: "Mobile & EDC",26 icon: "material-symbols:phone-iphone",27 description: "Daily portable devices & smart gadgets",28 },29 {30 key: "audio",31 label: "Audio & Visual",32 icon: "material-symbols:headphones-rounded",33 description: "Headphones, speakers & monitoring gears",34 },35 {36 key: "peripheral",37 label: "Peripherals",38 icon: "material-symbols:keyboard-outline-rounded",39 description: "Keyboards, mice & desk accessories",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: "fontsource",60 variants: [61 {62 file: "@fontsource/outfit/400.css",63 weight: 400,64 style: "normal",65 },66 {67 file: "@fontsource/outfit/500.css",68 weight: 500,69 style: "normal",70 },71 {72 file: "@fontsource/outfit/700.css",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: "fontsource",110 variants: [111 {112 file: "@fontsource-variable/jetbrains-mono/index.css",113 weight: "100 800",114 style: "normal",115 },116 {117 file: "@fontsource-variable/jetbrains-mono/wght-italic.css",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 { introConfig } from "./introConfig";45export { licenseConfig } from "./licenseConfig";46export { llmsConfig } from "./llmsConfig";47export {48 clampMusicVolume,49 musicConfig,50 type ResolvedMusicOptions,51 resolveMusicOptions,52} from "./musicConfig";53export { LinkPresets, navBarConfig } from "./navBarConfig";54export { openingConfig } from "./openingConfig";55export { permalinkConfig } from "./permalinkConfig";56export { POST_CARD_MIN_WIDTH, postListConfig } from "./postListConfig";57export { profileConfig } from "./profileConfig";58export { projectsConfig } from "./projectsConfig";59export { sidebarConfig } from "./sidebarConfig";60export {61 getDefaultSpec,62 getDefaultStyle,63 resolveDisplaySettings,64 resolveTextureOptions,65 siteConfig,66} from "./siteConfig";67export { skillsConfig } from "./skillsConfig";68export { timelineConfig } from "./timelineConfig";69export {70 type ResolvedUmamiOptions,71 resolveUmamiOptions,72 umamiConfig,73} from "./umamiConfig";1import type { IntroConfig } from "@/types/introConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 开场加载动画(IntroSplash)。6 *7 * 随主题附带的开场视频底色为 #f7b7ea(浅粉),backdrop 与之保持一致,8 * 这样 contain 模式下视频与幕布完全无缝;更换视频后请同步调整。9 * `enable: false` 时零 DOM、零请求、零运行时。10 */11export const introConfig: IntroConfig = withUserConfig("intro", {12 enable: true,13 src: "/assets/video/logo.mp4",14 fit: "contain",15 backdrop: "#f7b7ea",16 oncePerSession: true,17 maxWait: 6000,18 exitDuration: 600,19 skippable: true,20});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: "Home",98 url: "/",99 description: "Main blog entrance and latest post stream.",100 },101 {102 title: "About",103 url: "/about/",104 description: "Author profile, technical stack, and background.",105 },106 {107 title: "Archive",108 url: "/archive/",109 description: "Chronological index of all published writings.",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 *24 * 国际化:用 `$t:home` 这种引用前缀标记需要按当前语言解析的词条;25 * `name` 是字面量时直接当作原文(外部品牌名、技术术语等不翻译时用)。26 * 这样 LinkPresets 才能在布局每次渲染时拿到当前语言的展示文案。27 */28const t = (key: keyof typeof I18nKey) => `$t:${key}`;29
30export const LinkPresets: Record<string, NavBarLink> = {31 Home: {32 name: t("home"),33 url: "/",34 icon: "material-symbols:home-outline-rounded",35 pageKey: "home",36 },37 Archive: {38 name: t("archive"),39 url: "/archive/",40 icon: "material-symbols:archive-outline-rounded",41 pageKey: "archive",42 },43 Friends: {44 name: t("friends"),45 url: "/friends/",46 icon: "material-symbols:handshake-outline-rounded",47 pageKey: "friends",48 },49 Moments: {50 name: t("moments"),51 url: "/moments/",52 icon: "material-symbols:auto-awesome-outline-rounded",53 pageKey: "moments",54 },55 Anime: {56 name: t("anime"),57 url: "/anime/",58 icon: "material-symbols:live-tv-outline-rounded",59 pageKey: "anime",60 },61 Compass: {62 name: t("compass"),63 url: "/compass/",64 icon: "material-symbols:explore-rounded",65 pageKey: "compass",66 },67 Skills: {68 name: t("skills"),69 url: "/skills/",70 icon: "material-symbols:workspaces-outline-rounded",71 pageKey: "skills",72 },73 Projects: {74 name: t("projects"),75 url: "/projects/",76 icon: "material-symbols:deployed-code-outline-rounded",77 pageKey: "projects",78 },79 Devices: {80 name: t("devices"),81 url: "/devices/",82 icon: "material-symbols:devices-rounded",83 pageKey: "devices",84 },85 Timeline: {86 name: t("timeline"),87 url: "/timeline/",88 icon: "material-symbols:timeline-rounded",89 pageKey: "timeline",90 },91 Albums: {92 name: t("albums"),93 url: "/albums/",94 icon: "material-symbols:photo-library-outline-rounded",95 pageKey: "albums",96 },97 Categories: {98 name: t("categories"),99 url: "/categories/",100 icon: "material-symbols:folder-outline-rounded",101 pageKey: "categories",102 },103 Tags: {104 name: t("tags"),105 url: "/tags/",106 icon: "material-symbols:tag-rounded",107 pageKey: "tags",108 },109 About: {110 name: t("about"),111 url: "/about/",112 icon: "material-symbols:info-outline-rounded",113 pageKey: "about",114 },115 GitHub: {116 // 外部品牌名,保持英文。指向 fork 仓库主人的个人 GitHub117 name: "GitHub",118 url: "https://github.com/Znfooe",119 icon: "fa6-brands:github",120 external: true,121 pageKey: "github",122 },123 // 上游开源原项目(fork 来源),保留作为单独入口便于致敬作者124 SourceRepo: {125 name: "Original",126 url: "https://github.com/LyraVoid/Shirone",127 icon: "fa6-brands:github",128 external: true,129 pageKey: "source",130 },131};132
133const defaultNavBarConfig: NavBarConfig = {134 links: [135 LinkPresets.Home,136 LinkPresets.Archive,137 LinkPresets.Friends,138 LinkPresets.Moments,139 LinkPresets.Anime,140 LinkPresets.Compass,141 LinkPresets.Albums,142 {143 name: t("more"),144 icon: "material-symbols:apps-rounded",145 children: [146 ...(timelineConfig.enable ? [LinkPresets.Timeline] : []),147 ...(projectsConfig.enable ? [LinkPresets.Projects] : []),148 ...(devicesConfig.enable ? [LinkPresets.Devices] : []),149 ...(skillsConfig.enable ? [LinkPresets.Skills] : []),150 // 分类/标签入口不进导航菜单(避免菜单项过多),预设已登记指向独立页面,151 // 需要时取消注释即可152 // LinkPresets.Categories,153 // LinkPresets.Tags,154 LinkPresets.About,155 LinkPresets.GitHub,156 LinkPresets.SourceRepo,157 ],158 },159 ],160};161
162/** `$t:home` 形式的 i18n 引用前缀;不带前缀的 name 一律按字面量处理。 */163const I18N_REFERENCE_PREFIX = "$t:";164
165function fail(message: string): never {166 throw new Error(`[config] nav-bar:${message}`);167}168
169function resolveName(name: string): string {170 if (!name.startsWith(I18N_REFERENCE_PREFIX)) return name;171
172 const key = name.slice(I18N_REFERENCE_PREFIX.length);173 if (!Object.hasOwn(I18nKey, key)) {174 fail(175 `未知的 i18n 词条 "${key}"。可用词条见 src/i18n/i18nKey.ts;` +176 " 若本意是普通文本,去掉开头的 $t: 即可。",177 );178 }179 return i18n(I18nKey[key as keyof typeof I18nKey]);180}181
182/**183 * 把内容仓的声明式导航条目还原成 `NavBarLink`。184 *185 * 预设名与 i18n 词条只有在这里才能校验(`LinkPresets` 与 `I18nKey` 都住在代码仓,186 * 生成期的 Node 脚本受路径别名所限读不到),因此错误在构建加载配置时抛出。187 */188export function resolveNavBarLinks(189 entries: readonly NavBarLinkOverride[],190 presets: Record<string, NavBarLink> = LinkPresets,191): NavBarLink[] {192 return entries.map((entry) => {193 let base: NavBarLink | null = null;194 if (entry.preset !== undefined) {195 base = presets[entry.preset] ?? null;196 if (!base) {197 fail(198 `未知的预设 "${entry.preset}"。可用预设:${Object.keys(presets).join("、")}。`,199 );200 }201 }202
203 const name =204 entry.name !== undefined ? resolveName(entry.name) : base?.name;205 if (name === undefined) {206 fail("每个条目都需要 name,或用 preset 引用一个内置预设。");207 }208
209 // 未声明 children 时沿用预设自带的子菜单(已由 ...base 带入)。210 return {211 ...base,212 name,213 ...(entry.url !== undefined ? { url: entry.url } : {}),214 ...(entry.icon !== undefined ? { icon: entry.icon } : {}),215 ...(entry.pageKey !== undefined ? { pageKey: entry.pageKey } : {}),216 ...(entry.external !== undefined ? { external: entry.external } : {}),217 ...(entry.children218 ? { children: resolveNavBarLinks(entry.children, presets) }219 : {}),220 };221 });222}223
224const userNavBar = getUserConfig("navBar") as NavBarConfigOverride | undefined;225
226export const navBarConfig: NavBarConfig = userNavBar227 ? { links: resolveNavBarLinks(userNavBar.links) }228 : defaultNavBarConfig;1import type { OpeningConfig } from "@/types/openingConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 开场提示弹窗(OpeningNotice)。6 *7 * 展示本项目二开出处(fork 自 Shirone)并感谢上游作者,同时声明站点仍在开发中。8 * 「我已阅读」按钮在 `acknowledgeDelay`(默认 3000ms)后解锁;9 * 用户确认一次后(`once: true`,localStorage 持久化)后续访问不再出现。10 * `enable: false` 时零 DOM、零请求、零运行时。11 */12export const openingConfig: OpeningConfig = withUserConfig("opening", {13 enable: true,14 title: "欢迎",15 repoUrl: "https://github.com/LyraVoid/Shirone",16 repoAuthor: "LyraVoid",17 acknowledgeDelay: 3000,18 once: true,19});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/avatar.webp", // 用户头像:原图 头像.jpg 居中裁切为正方形后转 WebP10 name: "Znfooe",11 bio: "The rain remembers what the sky forgot to say.",12 links: [13 {14 name: "GitHub",15 icon: "fa6-brands:github", // 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://github.com/Znfooe",19 },20 {21 name: "Bilibili",22 icon: "fa6-brands:bilibili",23 url: "https://space.bilibili.com/1385099245?spm_id_from=333.1007.0.0",24 },25 ],26 /**27 * 联系方式:QQ 与 WhatsApp 走纯文字行(不渲染图标按钮),28 * 由 Profile 组件消费:在社交图标下方逐行展示。29 */30 contacts: [31 { name: "QQ", value: "3274098996" },32 { name: "WhatsApp", value: "+86 131 8481 8420" },33 ],34});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: "Theme",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| `introConfig.ts` | 开场加载动画(IntroSplash):总开关(`enable: false` 时零 DOM / 零视频请求 / 零客户端运行时)、视频地址(`public` 内路径或外链)、`contain`/`cover` 适配、幕布底色(建议与视频底色一致)、会话内只播一次、最长等待与收场淡出时长、跳过按钮;阶段契约(`pending → exiting → done`)见 `src/utils/intro-splash.ts` |108| `openingConfig.ts` | 开场提示弹窗(OpeningNotice):总开关、标题、上游仓库 URL 与作者名(二开出处声明 + 感谢开源)、「我已阅读」按钮解锁前的倒计时(`acknowledgeDelay`)、`once`(localStorage 持久化,确认一次后不再出现);运行时契约见 `src/utils/opening-notice.ts` |109| `expressiveCodeConfig.ts` | 代码块明暗主题 |110| `navBarConfig.ts` | 导航栏链接(`LinkPresets` 预设表 + 组装) |111| `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) |112| `fabConfig.ts` | 右下角悬浮控制流(FAB)配置:总开关、各操作项(返回顶部、悬浮目录、直达评论、返回首页、自定义操作)、细粒度设备受控矩阵(`devices?: ("mobile" | "tablet" | "desktop")[]`)、页面范围过滤与图标定制;架构见 `docs/fab-system.md` |113| `announcementConfig.ts` | 公告内容(侧栏 announcement widget 消费,text 为空不渲染) |114| `musicConfig.ts` | 侧栏音乐全局配置:总开关(默认关闭)、`provider` 模式切换接口、`defaultVolume` 初始音量与 `defaultMode` 初始播放模式(本地曲目清单维护在 `src/data/music.ts`);与 `sidebarConfig` 的 music 条目共同控制 `MusicSidebar`,详见下文与 `docs/sidebar-widgets.md` |115| `postListConfig.ts` | 文章列表:分页大小 + 布局(list/grid 模式、封面位置、grid 卡片宽度档位) |116| `articleConfig.ts` | 文章详情:最后更新提示、延伸阅读(相关/随机文章抽样)、以及文章尾部分享区块(总开关、海报生成与封面配置) |117| `commentConfig.ts` | 评论系统:全局开关(默认关闭)、Provider 选择(Twikoo 等)、视口懒加载与服务凭据配置 |118| `contextMenuConfig.ts` | 桌面端右键增强:可选开关(当前默认开启);配置允许页面与操作顺序,关闭时零 DOM、零监听器、零客户端资源 |119| `umamiConfig.ts` | Umami 统计:全局开关(默认关闭)、公开分享统计读取,以及可选的官方访问采集脚本配置;支持内容仓 `config/umami.yaml` 覆盖(领域键 `umami`) |120| `skillsConfig.ts` | 技能页行为控制:页面总开关、分类清单与单项禁用列表(技能内容维护在 `src/data/skills.ts`);关闭页面时导航入口同步隐藏 |121| `projectsConfig.ts` | 项目页行为控制:页面总开关、分类清单与单项禁用列表(项目内容维护在 `src/data/projects.ts`);关闭页面时导航入口同步隐藏 |122| `timelineConfig.ts` | 时间线页行为控制:页面总开关、分类清单、排序方向与单项禁用列表(时间线内容维护在 `src/data/timeline.ts`);关闭页面时导航入口同步隐藏 |123| `devicesConfig.ts` | 设备页行为控制:页面总开关、场景分类清单与单项禁用列表(设备清单维护在 `src/data/devices.ts`);关闭页面时导航入口同步隐藏 |124| `animeConfig.ts` | 番剧页与外部追番数据源:数据源选择(本地 / Bangumi 快照 / Bilibili 快照)、失败降级、提供方凭据环境配置与快照生命周期管理(本地番剧维护在 `src/data/anime.ts`) |125| `llmsConfig.ts` | 大语言模型与 AI 友好内容系统:`/llms.txt`(索引)与 `/llms-full.txt`(全量正文汇编)静态端点生成控制、加密文章过滤、排除标签与自定义章节配置;支持内容仓 `config/llms.yaml` 覆盖(领域键 `llms`) |126
127非首页 Banner 的标题、说明和可选日期由各页面通过 `MainGridLayout` 提供,并在 Swup 导航后从被替换的主内容容器同步。该上下文默认显示、不设配置开关;说明为空或与标题相同时自动省略,移动端非首页仍沿用紧凑布局并隐藏 Banner。128
129## 侧栏编排与页框宽度130
131- `arrangement: "single"`(默认)——全部 widget 渲染进唯一侧栏,页框 85rem;132- `arrangement: "dual"`——`column: "secondary"` 的 widget 进入副栏(视口 ≥ 1280px 起三列),133 其余留在主栏;页框自动放宽到 96rem,TOC 悬浮 rail 自动让位(右侧余量被副栏占据)。134 1280px 以下自动退化为单栏(只显主栏),无需配置。135- `side: "left" | "right"` 决定主栏物理侧,dual 线下副栏落在对面。136- 页框宽度用 `resolvePageWidth()`(`src/utils/responsive-utils.ts`)按编排自动解析,137 常量在 `src/constants/constants.ts`(`PAGE_WIDTH` / `PAGE_WIDTH_DUAL`),不提供手动覆盖。138
139## 右下角悬浮控制流(FAB)配置契约140
141`fabConfig.ts` 控制右下角悬浮操作栏的呈现与交互:142
143```typescript144export const fabConfig: FabConfig = {145 enable: true,146 items: [147 {148 type: "top",149 enable: true,150 icon: "material-symbols:keyboard-arrow-up-rounded",151 // 未指定 devices 时全设备生效;滚过横幅高度阈值后平滑浮现152 },153 {154 type: "toc",155 enable: true,156 icon: "material-symbols:format-list-bulleted-rounded",157 pages: ["post"],158 // 桌面端侧栏已有粘性 TOC,故默认仅在移动端与平板端显示悬浮目录159 devices: ["mobile", "tablet"],160 },161 {162 type: "comment",163 enable: true,164 icon: "material-symbols:comment-outline-rounded",165 pages: ["post"],166 devices: ["mobile", "tablet"],167 },168 {169 type: "home",170 enable: false,171 icon: "material-symbols:home-outline-rounded",172 },173 ],174};175```176
177### 核心规则与受控特性178
1791. **细粒度设备受控(`devices`)**:180 - `"mobile"`(< 768px)181 - `"tablet"`(768px ~ 1023px)182 - `"desktop"`(≥ 1024px)183 - 省略时默认所有设备均允许;SSR 阶段直接输出 Tailwind CSS 响应式类(如 `flex lg:hidden`),零首屏闪烁,CLS = 0。1842. **页面范围过滤(`pages`)**:185 - 过滤逻辑与侧栏 `SidebarPage` 统一,通过 `#swup-container` 的 `data-current-page` 在 Swup 站内导航时联动隐藏/显示。1863. **评论按钮零额外负担**:187 - 全局未开启评论系统(`commentConfig.enable: false`)或当前文章关闭评论时,评论 FAB 按钮产物为 0 DOM,零多余外链请求与布局偏移。1884. **无音乐挂件(保持纯粹)**:189 - FAB 控制流不集成音乐播放器,避免与侧边栏 `MusicSidebar` 产生双重状态混乱及包体积膨胀。190
191## 侧栏音乐启用契约与四种模式192
193侧栏音乐是默认关闭的可选能力,必须同时满足以下三项才加载并渲染:194
1951. `musicConfig.enable` 为 `true`;1962. 对应数据源(本地 `src/data/music.ts`、自定义 `tracks` 或 `meting` 远端歌单)至少包含一首有效曲目/合法歌单 ID;1973. `sidebarConfig.components` 中 `type: "music"` 的条目存在且 `enable: true`(默认条目为 `false`)。198
199### 四种数据源工作模式(`provider`)200
201| 模式 | 配置名 | 数据源 | 特点与适用场景 |202|---|---|---|---|203| **本地模式** | `"local"` | `src/data/music.ts` | 默认模式。零外部 API 依赖,构建期静态打包,首屏毫秒级就绪,支持离线/断网播放。 |204| **自定义列表** | `"custom"` | `musicConfig.tracks` | 灵活自定义。直接在配置中传入曲目数组(支持外链音频与封面),无需修改通用数据文件。 |205| **云端歌单** | `"meting"` | `musicConfig.meting` | 接入 Meting API(网易云、QQ 音乐、酷狗等),客户端异步按需拉取,曲目与封面自动清洗加载。 |206| **混合增强模式** | `"mixed"` | 本地数据 + Meting API | **推荐**。首屏立即播放本地音乐,后台无感拉取远端歌单并在就绪后无缝追加合并;若遇断网或云端接口故障,自动静默降级为本地曲目,绝不破版。 |207
208任一条件不满足时,音乐功能不得输出 DOM 或样式,不得请求音频/封面等资源,也不得把播放器代码或依赖带入主 bundle。配置消费者应先完成三项校验,再动态加载 `MusicSidebar`;不能用隐藏空卡片代替短路。209
210`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 × Znfooe",16 subtitle: "A Material 3 anime blog",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 // 站点默认语言:无路径前缀的根路由即使用此语言。改为中文后,英文站迁移到 /en/ 前缀。31 lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.32 // IANA time zone for precise post and moment timestamps. It is independent of lang.33 timeZone: "Asia/Shanghai",34 themeColor: {35 hue: 205, // Default hue 0-360. 淡蓝色调;原默认粉紫 315(偏二次元)已改为天蓝36 fixed: false, // Hide the theme color picker for visitors37 // Dynamic Material 3 palette style (TonalSpot/Vibrant/Content/Expressive/Rainbow/FruitSalad/Monochrome/Neutral/Fidelity)38 style: "tonalSpot",39 // Design spec version: "2021" (MD3) or "2025" (M3 Expressive)。角色集一致,40 // 差异仅在调色板派生(库的 colorSpec 静态为 2025 委托)41 spec: "2025",42 },43 // 默认页面背景模式:"banner" 使用壁纸横幅,"none" 使用主题纯色。44 // 访客在“显示设置”中的选择会保存在浏览器中,并覆盖这里的默认值。45 wallpaperMode: {46 defaultMode: "banner",47 },48 // 页面背景纹理系统配置(5 大精美预设 + 零开销 HCT 动态取色)49 texture: {50 enable: true, // 是否启用背景纹理系统51 defaultPreset: "starlight", // 默认纹理预设:"none" | "starlight" | "cyber-dots" | "topography" | "geometric" | "sakura"52 defaultOpacity: 0.12, // 默认纹理浓度 (0.05 ~ 0.25)53 allowMotion: true, // 是否允许背景微动效(开启 reduced-motion 时自动静止)54 },55 banner: {56 // 推荐将图片放入 src/assets,并填写相对 src 的路径,以启用构建期 AVIF/WebP 响应式优化。57 // 以 "/" 开头的 public 路径与远程 URL 仍可用,但会保留原图、不生成候选。58 // desktop 用于 >= 1024px;mobile 仅用于 < 1024px 的首页,手机非首页不显示壁纸。59 // 数组顺序就是轮播顺序;只需要静态 Banner 时,每组保留一张图片即可。60 src: {61 desktop: ["/banner/1.png"],62 mobile: ["/banner/1.png"],63 },64 // 图片裁切焦点:"top"、"center" 或 "bottom"。65 position: "center",66 dim: {67 // 在图片上覆盖黑色遮罩以提高标题和顶部栏的对比度;opacity 范围为 0-1。68 enable: true,69 opacity: 0.24,70 },71 homeText: {72 // 仅在首页 Banner 中显示,标题与副标题会上下居中排列。73 enable: true,74 title: "Shirone × Znfooe",75 subtitle: [76 "特別なことはないけど、君がいると十分です",77 "今でもあなたは私の光",78 "君ってさ、知らないうちに私の毎日になってたよ",79 "君と話すと、なんか毎日がちょっと楽しくなるんだ",80 "今日はなんでもない日。でも、ちょっとだけいい日",81 ],82 typewriter: {83 // 副标题逐字显示;关闭后直接显示完整副标题。84 enable: true,85 // 打字速度(每个字符间隔,毫秒)。86 speed: 100,87 // 回退反向删除速度(每个字符间隔,毫秒)。88 deleteSpeed: 50,89 // 打字完成后停顿时间,单位为毫秒。90 pauseTime: 2000,91 // 完成后是否循环播放;关闭表示只播放一次。92 loop: true,93 },94 },95 carousel: {96 // 是否开启多张图片自动轮播;多张图片时生效,单张图片时自动降级为静态展示。97 enable: true,98 // 轮播切换间隔时间(毫秒),运行时最小值限制为 3000ms。99 interval: 6000,100 // 交叉淡入淡出(Crossfade)过渡时长(毫秒,默认 1200ms)。101 fadeDuration: 1200,102 // 运镜呼吸动画模式:"ken-burns"(默认,循环运镜)| "zoom-in"(推进)| "zoom-out"(拉远)| "pan-left"(左移)| "pan-right"(右移)| "none"(无运镜)。103 animation: "ken-burns",104 },105 waves: {106 // 在 Banner 底部渲染页面背景色水波纹;关闭后不输出波浪 DOM。107 enable: true,108 },109 },110 // Markdown 正文图片处理;仅匹配远程图片,不会产生额外网络请求或客户端代码。111 imageOptimization: {112 // 为需要防盗链兼容的图片 CDN 添加 referrerpolicy="no-referrer",支持通配符。113 noReferrerDomains: ["*.hdslb.com"],114 },115 toc: {116 enable: true, // Display the table of contents on the right side of the post117 depth: 2, // Maximum heading depth to show in the table, from 1 to 3118 },119 progressIndicator: {120 // 进度条预设样式:dual 双向扫描(官方默认双线)/ single 单向扫描(单线)121 style: "dual",122 },123 favicon: [124 // 浏览器标签页图标,路径相对于 public 目录。125 { src: "/logo/icon.webp" },126 ],127});128
129/**130 * 解析并返回背景纹理配置选项(包含关闭短路与 0 开销优化判定)131 */132export function resolveTextureOptions(133 config: boolean | TextureConfig | undefined = siteConfig.texture,134 displaySettingsTexture: boolean = siteConfig.displaySettings?.texture ?? true,135): ResolvedTextureOptions {136 if (config === false || config === undefined) {137 return {138 enable: false,139 defaultPreset: "none",140 defaultOpacity: 0.12,141 allowMotion: false,142 };143 }144
145 if (config === true) {146 return {147 enable: true,148 defaultPreset: "starlight",149 defaultOpacity: 0.12,150 allowMotion: true,151 };152 }153
154 const enable = config.enable ?? true;155 const defaultPreset = config.defaultPreset ?? "starlight";156 const defaultOpacity = config.defaultOpacity ?? 0.12;157 const allowMotion = config.allowMotion ?? true;158
159 // 性能短路优化:160 // 如果配置 enable: false,或者 defaultPreset: "none" 且显示设置面板未允许切换(访客也无法开启),161 // 则自动视为完全关闭以达成零 DOM、零 CSS、零运行时代价。162 const effectiveEnable =163 enable && (defaultPreset !== "none" || displaySettingsTexture);164
165 return {166 enable: effectiveEnable,167 defaultPreset,168 defaultOpacity,169 allowMotion,170 };171}172
173/** 站点默认配色风格(访客未做选择时的回退值) */174export function getDefaultStyle(): string {175 return siteConfig.themeColor.style;176}177
178/** 站点默认 Color Spec(2021 / 2025) */179export function getDefaultSpec(): string {180 return siteConfig.themeColor.spec;181}182
183/** 解析并返回显示设置面板各项开关(未配置时默认 true) */184export function resolveDisplaySettings(): {185 colorStyle: boolean;186 colorSpec: boolean;187 wallpaperMode: boolean;188 layoutMode: boolean;189 reduceMotion: boolean;190 texture: boolean;191} {192 const cfg = siteConfig.displaySettings;193 const textureOpts = resolveTextureOptions(194 siteConfig.texture,195 cfg?.texture ?? true,196 );197 return {198 colorStyle: cfg?.colorStyle ?? true,199 colorSpec: cfg?.colorSpec ?? true,200 wallpaperMode: cfg?.wallpaperMode ?? true,201 layoutMode: cfg?.layoutMode ?? true,202 reduceMotion: cfg?.reduceMotion ?? true,203 texture: textureOpts.enable && (cfg?.texture ?? true),204 };205}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: "Frontend",20 icon: "material-symbols:web-rounded",21 },22 {23 key: "backend",24 label: "Backend",25 icon: "material-symbols:dns-rounded",26 },27 {28 key: "tooling",29 label: "Tooling",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: "Milestones",21 icon: "material-symbols:flag-rounded",22 },23 {24 key: "project",25 label: "Projects",26 icon: "material-symbols:code-rounded",27 },28 {29 key: "career",30 label: "Career",31 icon: "material-symbols:work-rounded",32 },33 {34 key: "education",35 label: "Education",36 icon: "material-symbols:school-rounded",37 },38 {39 key: "life",40 label: "Life",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/分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
沿着主题读
基于共同的标签与分类
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 16 天
部分内容可能已过时