Shirone provides a collection of theme-exclusive Markdown extensions and custom syntax containers. Built on top of our native unified AST processing pipeline, all extensions render into accessible, semantic HTML during site build time with zero client JavaScript hydration overhead and 100% M3E design token alignment.
File Trees#
File Trees turn multi-level project structures, source hierarchies, and terminal directory outputs into compact, interactive tree views with automatic extension icons, diff highlighting, and collapsible branches.
1. Nested List Syntax (:::file-tree)#
Use the :::file-tree block directive when writing the file hierarchy directly as a Markdown nested list.
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:::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
Authoring Rules & Markers#
- Diff States: Prefix an item with
++(green background & badge) or--(red background & strikethrough) to highlight changes. - Comments: Any text following a
#is rendered as a muted, right-aligned inline comment. - Emphasis: Wrap names in
**bold**to give key files prominent visual weight. - Collapsible Folders: Directories inferred from nested list items start expanded by default. Add a trailing slash (e.g.
components/) to create a collapsed directory that readers can expand on click or via keyboard navigation.
2. Terminal Output Syntax (```file-tree)#
When you already have directory tree text generated from command-line tools like tree, paste it directly into a file-tree fenced code block. Both Unicode branch characters (├──, └──, │) and ASCII branches are automatically parsed.
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
Configuration Options#
title="string": Sets a custom header title and accessible label for the tree.icon="colored" | "simple": Choose between multi-color extension icons (colored, default) or minimal monochrome icons (simple).
Code Trees#
Interactive Code Trees pair a multi-level file hierarchy navigation pane on the left with instant code panel switching on the right. They provide an IDE-like reading experience for multi-file examples, modules, or whole directory walk-throughs.
1. Container Syntax (:::code-tree)#
Combine multiple fenced code blocks within a :::code-tree block directive. Each code block specifies its path via 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:::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}Configuration & Markers#
title="string": Sets the header title and accessible label for the code tree.height="string": Sets the height for the desktop view (default420px, e.g.380px,26rem).entry="filepath": Specifies which file is active upon first load.icon="colored" | "simple": Switch between colorful or minimal monochrome file icons.:active: Place:activeon any fenced code block to designate it as the default active tab.
2. Local Directory Auto-Import (@[code-tree])#
Point directly to any local directory path in the workspace to automatically scan and generate an interactive code tree at build time without manually copying file contents.
1@[code-tree title="Anime Utilities" entry="status.ts"](/src/utils/anime)1import type { AboutConfig } from "../types/aboutConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const aboutConfig: AboutConfig = withUserConfig("about", {5 enable: true,6 title: "$t:about",7 description: "$t:about",8});1import type { AlbumsConfig } from "../types/albumsConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const albumsConfig: AlbumsConfig = withUserConfig("albums", {5 enable: true,6 title: "$t:albums",7 description: "$t:albumsBanner",8});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: "snapshot",48 provider: "bilibili",49 // file: "bilibili.json",50 // fetchOnDev: true,51 },52
53 /** 异常降级策略(快照丢失或解析失败时回退本地数据) */54 fallback: {55 kind: "local",56 },57
58 /** 外部提供方配置 */59 providers: {60 bangumi: {61 enable: false,62 userId: "MoeMoe1616238", // 填入你的 Bangumi 数字 UID 或公开用户名(测试可填 "sai")63 request: {64 pageSize: 50,65 maxItems: 300,66 minDelayMs: 200,67 },68 },69 bilibili: {70 enable: true,71 vmid: "1467613419", // 填入你的 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 {2 CommentConfig,3 GiscusConfig,4 TwikooConfig,5} from "@/types/commentConfig";6import { withUserConfig } from "../utils/config-overlay.ts";7
8/**9 * 评论系统配置单一真源。10 *11 * 遵循「零额外负担」原则:默认全局关闭(enable: false),12 * 在未开启时不产生任何外部网络请求、零额外 DOM 占位与零包体积膨胀。13 *14 * 【开启 Twikoo 评论配置步骤】15 * 1. 部署 Twikoo 服务端并获取环境 ID(腾讯云 CloudBase / Vercel / Railway / 私有部署等);16 * 2. 将 `enable` 置为 `true`,并将 `provider` 设置为 `"twikoo"`;17 * 3. 填入你的 `twikoo.envId`;18 * 4. (可选)自定义 `scriptUrl`(如使用自建 CDN 或官方 unpkg/jsdelivr 源)。19 *20 * 【开启 Giscus 评论配置步骤】21 * 1. 准备一个公开 GitHub 仓库,并在仓库设置中开启 Discussions 功能;22 * 2. 安装 giscus App(https://github.com/apps/giscus)到该仓库;23 * 3. 在 https://giscus.app 按引导选择仓库与 Discussion 分类,24 * 取生成的 `data-repo-id` 与 `data-category-id`;25 * 4. 将 `enable` 置为 `true`,并将 `provider` 设置为 `"giscus"`,26 * 填入 `giscus.repo` / `giscus.repoId` / `giscus.categoryId` 三个必填字段;27 * 5. (可选)调整 `mapping`、`reactionsEnabled`、`inputPosition`、28 * `theme.light` / `theme.dark`(giscus 主题键或自定义主题 CSS URL)、29 * `lang` 与 `scriptUrl`(自托管 giscus 时替换)。30 */31export const commentConfig: CommentConfig = withUserConfig("comment", {32 /** 全局评论总开关:false 时完全不加载评论脚本与 DOM */33 enable: false,34 /** 评论提供商类型:"none" | "twikoo" | "giscus" */35 provider: "none",36 /** 是否开启视口懒加载:滚动进入视口才动态加载评论组件(推荐 true) */37 lazy: true,38 /** Twikoo 专有配置 */39 twikoo: {40 /** Twikoo 环境 ID(如 "https://your-twikoo.vercel.app" 或腾讯云环境 ID) */41 envId: "",42 /** Twikoo 前端 JS 脚本 CDN 地址 */43 scriptUrl: "https://cdn.jsdelivr.net/npm/twikoo@1.7.20/dist/twikoo.min.js",44 /** 评论语言:"auto"(跟随站点)| "zh-CN" | "zh-TW" | "en" | "ja" 等 */45 lang: "auto",46 /** 评论输入框占位提示文本 */47 placeholder: "Share your thoughts...",48 },49 /** Giscus 专有配置(基于 GitHub Discussions,评论数据存储在公开仓库中) */50 giscus: {51 /** 公开仓库,格式 "owner/repo"(必填) */52 repo: "",53 /** 仓库 ID,从 giscus.app 配置生成器获取(必填) */54 repoId: "",55 /** Discussion 分类名,如 "Announcements";留空表示不限制分类搜索范围 */56 category: "Announcements",57 /** 分类 ID,从 giscus.app 配置生成器获取(必填) */58 categoryId: "",59 /** 页面 ↔ Discussion 映射:pathname(默认)/ url / title / og:title / specific / number */60 mapping: "pathname",61 /** 严格标题匹配(SHA-1 校验),避免 GitHub 模糊搜索误配相似标题 */62 strict: false,63 /** 是否显示主贴表情反应 */64 reactionsEnabled: true,65 /** 是否向父页面周期性发送 Discussion 元数据(供脚本消费) */66 emitMetadata: false,67 /** 评论输入框位置:bottom(默认,评论框在列表下方)| top(评论框在列表上方) */68 inputPosition: "bottom",69 /** 明暗两套 giscus 主题(giscus 主题键或自定义主题 CSS URL),跟随站点明暗切换 */70 theme: { light: "light", dark: "dark" },71 /** 评论语言:"auto"(跟随站点)| giscus 语言码(如 "zh-CN"、"en") */72 lang: "auto",73 /** giscus client.js 地址;自托管 giscus 时替换为自有地址 */74 scriptUrl: "https://giscus.app/client.js",75 },76});77
78export type ResolvedCommentOptions =79 | {80 provider: "twikoo";81 lazy: boolean;82 twikoo: TwikooConfig;83 }84 | {85 provider: "giscus";86 lazy: boolean;87 giscus: GiscusConfig;88 }89 | null;90
91/**92 * 解析并校验评论配置。未启用、提供商为 none 或关键参数缺失时返回 null。93 * 校验收敛在此纯函数中,消费组件只消费解析结果并短路。94 */95export function resolveCommentOptions(96 config: CommentConfig,97): ResolvedCommentOptions {98 if (!config.enable || config.provider === "none") {99 return null;100 }101 if (config.provider === "twikoo") {102 const envId = config.twikoo.envId?.trim();103 const scriptUrl = config.twikoo.scriptUrl?.trim();104 if (!envId || !scriptUrl) {105 return null;106 }107 return {108 provider: "twikoo",109 lazy: config.lazy,110 twikoo: {111 ...config.twikoo,112 envId,113 scriptUrl,114 },115 };116 }117 if (config.provider === "giscus") {118 const repo = config.giscus.repo?.trim();119 const repoId = config.giscus.repoId?.trim();120 const categoryId = config.giscus.categoryId?.trim();121 if (!repo || !repoId || !categoryId) {122 return null;123 }124 return {125 provider: "giscus",126 lazy: config.lazy,127 giscus: {128 ...config.giscus,129 repo,130 repoId,131 categoryId,132 category: config.giscus.category?.trim() ?? "",133 theme: {134 light: config.giscus.theme?.light?.trim() || "light",135 dark: config.giscus.theme?.dark?.trim() || "dark",136 },137 scriptUrl: config.giscus.scriptUrl?.trim(),138 },139 };140 }141 return null;142}1import type { CompassConfig } from "../types/compassConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const compassConfig: CompassConfig = withUserConfig("compass", {5 enable: true,6 title: "$t:compass",7 description: "$t:compassBanner",8});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: false,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});1import type { FriendsConfig } from "../types/friendsConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const friendsConfig: FriendsConfig = withUserConfig("friends", {5 enable: true,6 title: "$t:friends",7 description: "$t:friendsBanner",8});1import type { I18nConfig } from "../types/i18nConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const i18nConfig: I18nConfig = withUserConfig("i18n", {});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";52// 👇 新增了这一行,专门解决你刚才的报错53export { momentsConfig } from "./momentsConfig";54export { LinkPresets, navBarConfig } from "./navBarConfig";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 { 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 type { MomentsConfig } from "../types/momentsConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const momentsConfig: MomentsConfig = withUserConfig("moments", {5 enable: true,6 title: "$t:moments",7 description: "$t:momentsBanner",8});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: "远辰",11 bio: "记录日常与生活",12 links: [13 {14 name: "GitHub",15 icon: "fa6-brands:github",16 url: "https://github.com/far-chen",17 },18 ],19});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: false,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| `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 / Giscus)、视口懒加载与服务凭据配置;Giscus 基于 GitHub Discussions(需公开仓库 + 安装 giscus App + 从 giscus.app 取 repoId/categoryId),主题明暗双值跟随站点切换 |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: "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 lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.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 { aboutConfig } from "./aboutConfig.ts";2import { albumsConfig } from "./albumsConfig.ts";3import { animeConfig } from "./animeConfig.ts";4import { compassConfig } from "./compassConfig.ts";5import { devicesConfig } from "./devicesConfig.ts";6import { friendsConfig } from "./friendsConfig.ts";7import { momentsConfig } from "./momentsConfig.ts";8import { projectsConfig } from "./projectsConfig.ts";9import { skillsConfig } from "./skillsConfig.ts";10import { timelineConfig } from "./timelineConfig.ts";11
12/** 获取所有被配置 enable: false 关闭的页面标识清单 */13export function getDisabledPages(): string[] {14 const disabled: string[] = [];15 if (skillsConfig.enable === false) disabled.push("skills");16 if (projectsConfig.enable === false) disabled.push("projects");17 if (timelineConfig.enable === false) disabled.push("timeline");18 if (devicesConfig.enable === false) disabled.push("devices");19 if (animeConfig.enable === false) disabled.push("anime");20 if (aboutConfig.enable === false) disabled.push("about");21 if (friendsConfig.enable === false) disabled.push("friends");22 if (momentsConfig.enable === false) disabled.push("moments");23 if (albumsConfig.enable === false) disabled.push("albums");24 if (compassConfig.enable === false) disabled.push("compass");25 return disabled;26}27
28/**29 * 校验页面路径是否应当被收录进 sitemap。30 * 排除被关闭页面(例如 /skills/, /skills/index.html 等)以及 404 跳转存根。31 */32export function isSitemapPageAllowed(pageUrl: string): boolean {33 const disabled = getDisabledPages();34 for (const p of disabled) {35 if (36 pageUrl.endsWith(`/${p}/`) ||37 pageUrl.endsWith(`/${p}`) ||38 pageUrl.includes(`/${p}/`)39 ) {40 return false;41 }42 }43 return true;44}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: false,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 };分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 25 天
部分内容可能已过时