18095 字
90 分钟
页面浏览--·访问次数--
Markdown 代码树
代码树 = 左侧文件树导航 + 右侧代码高亮面板,用来展示多文件示例。点击左侧文件名即可切换右侧内容。
容器写法#
效果(默认展示 entry 指定的文件,点击左侧其它文件可切换)
按钮组件封装
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="路径" 的围栏提供)
1:::code-tree{title="按钮组件封装" 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:::参数#
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
title | string(≤200 字符) | 空 | 头部标题与无障碍标签 |
height | CSS 长度 | 420px | 桌面端面板高度,如 380px、26rem |
entry | 文件路径 | 第一个文件 | 首次加载默认激活的文件 |
icon | colored / simple | colored | 文件图标风格(数据来源见下) |
:active | 标记 | — | 写在某个围栏上,指定它为默认激活项 |
目录自动导入#
效果(构建期扫描主题仓里的一个本地目录,自动生成代码树)
站点配置目录
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 /** 是否启用番剧页;false 时导航入口同步隐藏,访问 /anime/ 跳转 404 */43 enable: true,44 title: "$t:anime",45 description: "$t:animeBanner",46
47 /** 主数据源选择 */48 source: {49 kind: "local",50 // provider: "bangumi",51 // file: "bangumi.json",52 // fetchOnDev: true,53 },54
55 /** 异常降级策略(快照丢失或解析失败时回退本地数据) */56 fallback: {57 kind: "local",58 },59
60 /** 外部提供方配置 */61 providers: {62 bangumi: {63 enable: false,64 userId: "", // 填入你的 Bangumi 数字 UID 或公开用户名(测试可填 "sai")65 request: {66 pageSize: 50,67 maxItems: 300,68 minDelayMs: 200,69 },70 },71 bilibili: {72 enable: false,73 vmid: "", // 填入你的 B 站公开 UID74 sessdataEnv: "BILI_SESSDATA",75 cover: {76 mode: "local", // "local" 站内下载缓存(推荐)| "remote" 远程链接 | "none"77 useWebp: true,78 },79 request: {80 pageSize: 30,81 maxItems: 300,82 minDelayMs: 300,83 },84 },85 },86
87 /** 快照存储管理 */88 snapshot: {89 directory: "src/data/anime-snapshots",90 staleAfterDays: 30,91 keepLastValid: true,92 },93});94
95const SAFE_FILENAME_PATTERN = /^[a-zA-Z0-9_-]+\.json$/;96
97/**98 * 校验并解析 Anime 配置,返回只读的标准选项99 */100export function resolveAnimeOptions(config: AnimeConfig): ResolvedAnimeOptions {101 const enable = Boolean(config.enable);102 const fallback: AnimeFallbackKind =103 config.fallback?.kind === "empty" ? "empty" : "local";104
105 const directory =106 typeof config.snapshot?.directory === "string" &&107 config.snapshot.directory.trim() &&108 !config.snapshot.directory.includes("..")109 ? config.snapshot.directory.trim().replace(/[\\/]+$/, "")110 : "src/data/anime-snapshots";111
112 const staleAfterDays =113 typeof config.snapshot?.staleAfterDays === "number" &&114 Number.isFinite(config.snapshot.staleAfterDays) &&115 config.snapshot.staleAfterDays > 0116 ? Math.floor(config.snapshot.staleAfterDays)117 : 30;118
119 const keepLastValid = config.snapshot?.keepLastValid ?? true;120
121 const rawKind = config.source?.kind;122 let kind: AnimeSourceKind = "local";123 let provider: AnimeProvider | undefined;124 let file: string | undefined;125
126 if (rawKind === "snapshot") {127 const rawProvider = config.source?.provider;128 if (rawProvider === "bangumi" || rawProvider === "bilibili") {129 provider = rawProvider;130 }131
132 const rawFile = config.source?.file?.trim();133 if (134 rawFile &&135 SAFE_FILENAME_PATTERN.test(rawFile) &&136 // 若指定了 provider 但 file 误填了另一 provider 的 json,自动校正为对应 provider 的 json 文件137 !(provider === "bilibili" && rawFile === "bangumi.json") &&138 !(provider === "bangumi" && rawFile === "bilibili.json")139 ) {140 file = rawFile;141 } else if (provider) {142 file = `${provider}.json`;143 }144
145 const fetchOnDev = config.source?.fetchOnDev ?? true;146
147 if (file) {148 kind = "snapshot";149 }150
151 return Object.freeze({152 enable,153 source: Object.freeze({154 kind,155 ...(provider ? { provider } : {}),156 ...(file ? { file } : {}),157 fetchOnDev,158 }),159 fallback,160 snapshot: Object.freeze({161 directory,162 staleAfterDays,163 keepLastValid,164 }),165 });166 }167
168 return Object.freeze({169 enable,170 source: Object.freeze({171 kind,172 ...(provider ? { provider } : {}),173 ...(file ? { file } : {}),174 fetchOnDev: config.source?.fetchOnDev ?? true,175 }),176 fallback,177 snapshot: Object.freeze({178 directory,179 staleAfterDays,180 keepLastValid,181 }),182 });183}184
185export const resolvedAnimeOptions: ResolvedAnimeOptions =186 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: true,16 title: "$t:devices",17 description: "$t:devicesBanner",18 categories: [19 {20 key: "desk",21 label: "Desk Setup",22 icon: "material-symbols:desktop-windows-outline-rounded",23 description: "Workstation & home office hardware",24 },25 {26 key: "mobile",27 label: "Mobile & EDC",28 icon: "material-symbols:phone-iphone",29 description: "Daily portable devices & smart gadgets",30 },31 {32 key: "audio",33 label: "Audio & Visual",34 icon: "material-symbols:headphones-rounded",35 description: "Headphones, speakers & monitoring gears",36 },37 {38 key: "peripheral",39 label: "Peripherals",40 icon: "material-symbols:keyboard-outline-rounded",41 description: "Keyboards, mice & desk accessories",42 },43 ],44 // disabledIds: [],45});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 { GamesConfig } from "@/types/gamesConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 游戏展示页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /games/ 跳转 404;9 * - categories:游戏分类清单(数组顺序即页面顶部 Chips 顺序;10 * 没有任何条目的分类会自动隐藏,因此可放心保留暂时用不到的分类);11 * - disabledIds:可选被禁用的游戏 ID 列表;12 *13 * 注:游戏的具体清单数据(游戏名、开发商、封面、评分、时长、简评等)请在 `src/data/games.ts` 中维护。14 */15export const gamesConfig: GamesConfig = withUserConfig("games", {16 enable: true,17 title: "$t:games",18 description: "$t:gamesBanner",19 categories: [20 {21 key: "open-world",22 label: "Open World",23 icon: "material-symbols:explore-outline-rounded",24 description: "Open-world adventures",25 },26 {27 key: "sandbox",28 label: "Sandbox",29 icon: "material-symbols:widgets-rounded",30 description: "Building, crafting & creative worlds",31 },32 {33 key: "rpg",34 label: "RPG",35 icon: "material-symbols:shield-outline-rounded",36 description: "Role-playing stories & builds",37 },38 {39 key: "action",40 label: "Action",41 icon: "material-symbols:swords-outline-rounded",42 description: "Action, fighting & shooters",43 },44 {45 key: "casual",46 label: "Casual",47 icon: "material-symbols:extension-outline-rounded",48 description: "Cozy, casual & party games",49 },50 ],51 // disabledIds: [],52});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 { aboutConfig } from "./aboutConfig";11export { albumsConfig } from "./albumsConfig";12export {13 animeConfig,14 resolveAnimeOptions,15 resolvedAnimeOptions,16} from "./animeConfig";17export { announcementConfig } from "./announcementConfig";18export {19 type ArticleDiscoveryOptions,20 type ArticleShareOptions,21 articleConfig,22 normalizeDiscoveryCount,23 resolveArticleDiscoveryOptions,24 resolveArticleShareOptions,25 resolveLastUpdatedNoticeOptions,26} from "./articleConfig";27export {28 commentConfig,29 type ResolvedCommentOptions,30 resolveCommentOptions,31} from "./commentConfig";32export { compassConfig } from "./compassConfig";33export { contextMenuConfig } from "./contextMenuConfig";34export { devicesConfig } from "./devicesConfig";35export { expressiveCodeConfig } from "./expressiveCodeConfig";36export { fabConfig } from "./fabConfig";37export {38 fontConfig,39 resolvedFontOptions,40 resolveFontOptions,41} from "./fontConfig";42export { footerConfig } from "./footerConfig";43export { friendsConfig } from "./friendsConfig";44export { gamesConfig } from "./gamesConfig";45export { i18nConfig } from "./i18nConfig";46export {47 imageBloomConfig,48 resolveImageBloomOptions,49} from "./imageBloomConfig";50export { licenseConfig } from "./licenseConfig";51export { llmsConfig } from "./llmsConfig";52export { momentsConfig } from "./momentsConfig";53export {54 clampMusicVolume,55 musicConfig,56 type ResolvedMusicOptions,57 resolveMusicOptions,58} from "./musicConfig";59export { LinkPresets, navBarConfig } from "./navBarConfig";60export { permalinkConfig } from "./permalinkConfig";61export { POST_CARD_MIN_WIDTH, postListConfig } from "./postListConfig";62export { profileConfig } from "./profileConfig";63export { projectsConfig } from "./projectsConfig";64export { seriesConfig } from "./seriesConfig";65export { sidebarConfig } from "./sidebarConfig";66export {67 getDefaultSpec,68 getDefaultStyle,69 resolveDisplaySettings,70 resolveTextureOptions,71 siteConfig,72} from "./siteConfig";73export { skillsConfig } from "./skillsConfig";74export { timelineConfig } from "./timelineConfig";75export {76 type ResolvedUmamiOptions,77 resolveUmamiOptions,78 umamiConfig,79} from "./umamiConfig";1/**2 * 下列 import 全部是 `import type`,在编译期被完全擦除,不产生任何运行时依赖。3 * 用 `Parameters<typeof fn>[0]` 而不是写死类型名,是为了让这里的标注跟着各集成4 * 自己的声明走——它们升级改了选项形状时,这里会跟着报错,而不是悄悄失配。5 */6
7import type mdx from "@astrojs/mdx";8import type swup from "@swup/astro";9import type expressiveCode from "astro-expressive-code";10import type icon from "astro-icon";11import type { AstroUserConfig } from "astro";12
13/**14 * 所有模式共享的集成选项。15 *16 * 主题只有一份 Astro 配置入口:`src/integration/index.ts` 在17 * `astro:config:setup` 阶段的 `updateConfig()`。源码模式(本仓 checkout)与18 * 包模式(npm 安装)都通过 `integrations: [shirones()]` 跑它——本仓的19 * `astro.config.mjs` 只剩这一行接线。源码模式由 in-repo 检测20 * (`paths.isThemeRepo`)把 config/data/content 指回 `src/` 自身,`src/` 下21 * 原生的 pages/组件照常被 Astro 拾取。22 *23 * **只放「选项值」和纯函数,不放接线(wiring)。** 接线集中在24 * `src/integration/index.ts`,按模式分支,不要往这里搬:25 *26 * - `vite.resolve.alias` —— 两侧根目录和依赖树不同;27 * - `svelte().preprocess` —— 由 integration 显式传 `vitePreprocess`(本仓的28 * `svelte.config.js` 只剩编辑器工具用途);29 * - `vite.optimizeDeps.include` —— 要过 `prebundleCandidates()`:Vite 从30 * **项目**根解析这个列表,包模式看不见主题自己的嵌套 node_modules;31 * - `vite.plugins` —— overlay(仅包模式)/ fallback-resolver / SSR shims;32 * - `expressiveCode()` 的 `themes` 和 `plugins` —— 必须走33 * `loadConfigModule()` / `loadPackageModule()`,用户的覆盖才生效;34 * - `fonts`、`markdown.processor`、`site`、`base` —— 由 integration 统一装载。35 *36 * 本文件不 import `../types/`、`../utils/` 等目录:模板拷贝时37 * `CONFIG_REWRITES` 不覆盖这些路径,一旦引用会让 `prepare-templates.mjs` 的38 * `assertNoEscapedImports` 直接失败。同理**不要走 `src/config/index.ts`39 * barrel**——barrel 不会拷进用户项目(`prepare-templates.mjs` 跳过40 * `index.ts`),两侧都必须按文件路径 import 本模块。41 */42
43/**44 * 全站尾斜杠策略。主题的页面和路由 pattern 全程假设 `"always"`,所以不提供45 * 配置项——暴露这个选择等于邀请用户配一个主题并不真正支持的组合。46 */47export const TRAILING_SLASH = "always";48
49/**50 * 图片端点路由。必须带尾斜杠,与 `TRAILING_SLASH` 配对。51 *52 * Astro 的 `image.endpoint.route` 只在 `core/config/schemas/relative.js` 的53 * zod transform 里被规范化一次,而那一步由 `validateConfig` 在 `resolveConfig`54 * 阶段调用。此时 `trailingSlash` 还是默认的 `"ignore"`,于是 route 定格为55 * `/_image`(无尾斜杠)。包模式随后才在 `astro:config:setup` 里把56 * `trailingSlash` 改成 `"always"`,而 `hooks.js` 之后只跑57 * `validateConfigRefined`——它**不含**这个 transform——route 再也不会被规范化。58 * 结果 URL 生成端吐 `/_image?…`,路由 pattern 却要求 `/_image/`,`astro dev`59 * 下每个图片请求都 404(build 和 preview 不受影响)。60 *61 * 这里直接把斜杠补上,等价于 transform 本该产出的结果。integration 的62 * `updateConfig()` 把 `trailingSlash` 与 `image.endpoint.route` 成对设置63 * (见 index.ts 中的注释),**所有模式**——源码模式在内——都走这条路径。64 *65 * ⚠️ 若 `TRAILING_SLASH` 改了,这里要跟着改:66 * `"always"` → `"/_image/"` | `"never"` / `"ignore"` → `"/_image"`67 * 若 Astro 修复了顺序(在 config:setup 之后重跑 relative transform),这一行68 * 会变成冗余但无害,可以删除。跟踪:withastro/astro#11568、#10149。69 */70export const IMAGE_ENDPOINT_ROUTE = "/_image/";71
72/**73 * `@swup/astro` 的选项。74 *75 * `ignore` 用数组形式:`@swup/astro` 1.8.0 的类型声明是76 * `(string | RegExp)[] | ((url, …) => …)`(见其 `dist/index.d.ts`),裸字符串77 * 并不符合——运行时 `script.js` 恰好能处理,但本仓 `astro.config.mjs` 是无类型78 * JS 才没报错。统一成数组后两侧都是合法的。79 *80 * `persistTags` 必须带 `:not([data-swup-optional])`:`data-swup-optional` 是81 * 主题自己的页面发出的(`[...permalink].astro`、`about.astro`、82 * `posts/[...slug].astro`),语义是「这份样式只对当前页有意义」。不带这个83 * 选择器,语法高亮等按页注入的样式会在 Swup 切换后残留。包模式同样注入这些84 * 页面,所以两侧行为必须一致。85 */86export const swupOptions: NonNullable<Parameters<typeof swup>[0]> = {87 theme: false as const,88 ignore: ['a[href="#"]'],89 animationClass: "transition-swup-",90 containers: ["main", "#toc"],91 smoothScrolling: true,92 cache: true,93 preload: true,94 accessibility: true,95 updateHead: {96 awaitAssets: false,97 // Keep base styles across Swup visits, but let syntax-scoped styles98 // disappear when the destination page no longer declares them.99 persistTags:100 "link[rel=stylesheet]:not([data-swup-optional]), style:not([data-swup-optional])",101 },102 updateBodyClass: false,103 globalInstance: true,104};105
106/**107 * `@swup/astro` 1.8.0 的 `Options` 类型不认识、但主题确实要传的选项。该版本在108 * 运行时会静默丢弃它们。109 *110 * 必须和 `swupOptions` 分开:`swupOptions` 标注为 `Partial<Options>`,把这两个111 * 键写进去会直接报 ts(2353)。这个标注本身是有意义的——它是本文件里唯一能让112 * swup 选项拼写错误在编译期暴露出来的东西,不要为了让这里能合并而把它去掉。113 *114 * 等 `@swup/astro` 的类型补上这两个键之后,才可以把它们并回 `swupOptions`。115 */116export const swupForwardOptions = {117 animateHistoryBrowsing: false,118 skipPopStateHandling: (event: { state?: { url?: string } }) =>119 Boolean(event.state?.url?.includes("#")),120};121
122/**123 * `astro-icon` 要打包的图标集。124 *125 * 五个集合一个都不能少:`src/` 里共 340 处图标引用,其中 `material-symbols`126 * 占 308 处、`simple-icons` 占 21 处。源码模式此前漏了这两个,只靠 astro-icon127 * 在扁平 `node_modules` 下的自动发现兜住;包模式在 pnpm 严格布局下没有这个128 * 退路,必须显式声明。129 */130export const iconInclude: NonNullable<131 NonNullable<Parameters<typeof icon>[0]>["include"]132> = {133 "material-symbols": ["*"],134 "simple-icons": ["*"],135 "fa6-brands": ["*"],136 "fa6-regular": ["*"],137 "fa6-solid": ["*"],138};139
140/**141 * `astro-expressive-code` 中与「取值方式」无关的那部分选项。142 *143 * `themes` 不在这里:两侧拿 `expressiveCodeConfig` 的途径不同(源码模式直接144 * import,包模式走 `loadConfigModule()` 让用户的覆盖生效)。`plugins` 也不在:145 * 包模式必须走 `loadPackageModule()`。146 */147export const expressiveCodeShared: Omit<148 NonNullable<Parameters<typeof expressiveCode>[0]>,149 "themes" | "plugins"150> = {151 defaultProps: {152 wrap: true,153 overridesByLang: {154 shellsession: {155 showLineNumbers: false,156 },157 },158 },159 styleOverrides: {160 codeBackground: "var(--codeblock-bg)",161 borderRadius: "0.75rem",162 borderColor: "none",163 codeFontSize: "0.875rem",164 codeFontFamily: "var(--m3e-font-mono-family)",165 codeLineHeight: "1.5rem",166 frames: {167 editorBackground: "var(--codeblock-bg)",168 terminalBackground: "var(--codeblock-bg)",169 terminalTitlebarBackground: "var(--codeblock-topbar-bg)",170 editorTabBarBackground: "var(--codeblock-topbar-bg)",171 editorActiveTabBackground: "none",172 editorActiveTabIndicatorBottomColor: "var(--primary)",173 editorActiveTabIndicatorTopColor: "none",174 editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)",175 terminalTitlebarBorderBottomColor: "none",176 },177 // Hue values are numbers at runtime — the source config has always178 // passed numbers — but the published types only accept unresolved CSS179 // strings. Cast here rather than at the use sites so both modes stay180 // type-checked everywhere else.181 textMarkers: { delHue: 0, insHue: 180, markHue: 250 } as never,182 },183 frames: {184 showCopyToClipboardButton: false,185 },186};187
188/**189 * `@astrojs/svelte` 的 `compilerOptions`。190 *191 * `preprocess` 不在这里,见文件头对 wiring 的说明。192 *193 * @param isDev 是否 dev 命令。源码模式从 `process.argv` 判断,包模式从194 * `astro:config:setup` 的 `command` 参数判断,两侧取值方式不同,所以由调用方195 * 传进来。196 */197export function svelteCompilerOptions(isDev: boolean) {198 return {199 // CSS-source hashing keeps SSR and client scope hashes stable after moves.200 cssHash: ({ css, hash }: { css: string; hash: (s: string) => string }) =>201 `svelte-${hash(css)}`,202 // Keep repeated Svelte compiler diagnostics out of the dev terminal;203 // check/build still surface the full warning set in CI.204 warningFilter: () => !isDev,205 };206}207
208/** `@astrojs/mdx` 的选项。 */209export const mdxOptions: NonNullable<Parameters<typeof mdx>[0]> = {210 syntaxHighlight: false as const,211 optimize: true,212};213
214/**215 * 两侧共用的 `vite.build` 选项。216 *217 * 不含 `esbuild`(`drop` / `pure`):那是源码模式独有的构建期优化,会连带剥掉218 * 用户自己代码里的 `console.log`,属于需要单独决策的行为差异,见219 * `shirones` 仓的 `docs/plans/single-source-config.md`。220 */221export const viteBuildShared: NonNullable<222 NonNullable<AstroUserConfig["vite"]>["build"]223> = {224 minify: "esbuild" as const,225 cssCodeSplit: true,226 cssMinify: "esbuild" as const,227 chunkSizeWarningLimit: 1000,228 rollupOptions: {229 // Parameter types are inferred from the build-options type. Hand-writing them230 // narrower than rolldown's OnwarnFunction made the whole object fail to231 // type-check.232 onwarn(warning, defaultHandler) {233 // Astro legitimately mixes static and dynamic imports for islands;234 // silence that specific advisory.235 if (236 warning.message.includes("is dynamically imported by") &&237 warning.message.includes("but also statically imported by")238 ) {239 return;240 }241 defaultHandler(warning);242 },243 },244};245
246/**247 * `vite.optimizeDeps.include` 的候选清单。248 *249 * 只是**候选**:包模式必须先过 `prebundleCandidates()` 过滤,因为 Vite 从项目250 * 根解析这些 id,看不见主题自己的嵌套依赖,列一个解析不了的 id 会让每次冷启动251 * 都刷一条警告。252 */253export const prebundleSpecifiers = [254 "mermaid",255 "@panzoom/panzoom",256 "overlayscrollbars",257 "@fancyapps/ui",258];259
260/**261 * 可选音乐侧栏的虚拟模块 id。262 *263 * 两侧的插件对象本身不共享(解析出来的侧栏路径不同:源码模式指向264 * `./src/components/...`,包模式指向 `paths.packageSrc` 下的同名文件),但虚拟265 * id 和「哪些产物算音乐包」的判定两侧完全一致,而且正是最容易悄悄改歪的地方,266 * 所以收敛到这里。267 */268export const MUSIC_SIDEBAR_VIRTUAL_ID = "virtual:shirone-music-sidebar";269
270/** 关掉音乐组件时,`generateBundle` 阶段要从产物里剔除的文件。 */271export function isMusicBundleFile(fileName: string): boolean {272 return (273 fileName.includes("MusicSidebarClient") ||274 fileName.startsWith("_astro/music.") ||275 fileName.includes("/music.")276 );277}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 * - 可选 `preload: "metadata"`:组件进入视口即预取歌单元数据(不含音频流),37 * 首屏直接显示第一首曲目;默认 "none"(不预取,交互后才请求)。38 * - 示例:39 * provider: "meting",40 * meting: { server: "netease", type: "playlist", id: "14164869977" }41 *42 * 4. "mixed"(混合增强模式,推荐):43 * - 数据源:本地曲目(src/data/music.ts)+ Meting API 远端歌单自动合并44 * - 特点:首屏立即可播本地音乐,后台无感拉取远端歌单并在就绪后无缝扩容;45 * 若遇断网或云端接口故障,自动静默降级为本地曲目播放,绝不报红破版。46 * - 示例:47 * provider: "mixed",48 * meting: { server: "netease", type: "playlist", id: "14164869977" }49 * ─────────────────────────────────────────────────────────────────────────────50 */51export const musicConfig: MusicConfig = withUserConfig("music", {52 enable: true,53 provider: "meting",54 // tracks: [55 // {56 // id: "custom-1",57 // title: "示例曲目",58 // artist: "艺术家",59 // cover: "/assets/music/cover/example.webp",60 // source: "/assets/music/url/example.mp3",61 // duration: 240,62 // },63 // ],64 meting: {65 server: "netease",66 type: "playlist",67 id: "18409884878",68 // 进入视口时预取歌单元数据(仅元信息,不预取音频流):69 // "metadata"(取)| "none"(默认,不取;交互后才请求,卡片显示「尚未请求」占位)70 preload: "none",71 },72 defaultVolume: 0.7,73 defaultMode: "sequence",74});75
76export interface ResolvedMusicOptions {77 readonly provider: MusicProvider;78 readonly playlist: readonly TrackDescriptor[];79 readonly meting?: MetingMusicConfig;80 readonly defaultVolume: number;81 readonly defaultMode: PlaybackMode;82}83
84const ABSOLUTE_MEDIA_SOURCE = /^(?:https?:)?\/\//i;85const UNSAFE_SCHEME = /^[a-z][a-z\d+.-]*:/i;86
87function normalizeMediaSource(value: string): string | null {88 const source = value.trim();89 if (!source) return null;90 if (ABSOLUTE_MEDIA_SOURCE.test(source) || source.startsWith("/")) {91 return source;92 }93 if (UNSAFE_SCHEME.test(source)) return null;94 return `/${source.replace(/^\.\//, "")}`;95}96
97function normalizeTrack(98 track: TrackDescriptor,99 usedIds: Set<string>,100): TrackDescriptor | null {101 const id = track.id.trim();102 const title = track.title.trim();103 const source = normalizeMediaSource(track.source);104 if (!id || !title || !source || usedIds.has(id)) return null;105
106 usedIds.add(id);107 const artist = track.artist?.trim() || undefined;108 const cover = track.cover109 ? (normalizeMediaSource(track.cover) ?? undefined)110 : undefined;111 const duration =112 typeof track.duration === "number" &&113 Number.isFinite(track.duration) &&114 track.duration > 0115 ? track.duration116 : undefined;117
118 return Object.freeze({ id, title, source, artist, cover, duration });119}120
121export function clampMusicVolume(value: number, fallback = 0.7): number {122 if (!Number.isFinite(value)) return fallback;123 return Math.min(1, Math.max(0, value));124}125
126/** 补齐 meting 配置的默认值(如 preload 默认 "none"),让 ResolvedMusicOptions 自包含。 */127function resolveMetingConfig(128 meting: MetingMusicConfig | undefined,129): MetingMusicConfig | undefined {130 if (!meting) return meting;131 return Object.freeze({ ...meting, preload: meting.preload ?? "none" });132}133
134export function resolveMusicOptions(135 config: MusicConfig,136): ResolvedMusicOptions | null {137 if (!config.enable) return null;138
139 const provider: MusicProvider = config.provider ?? "local";140
141 if (provider === "meting") {142 const id = config.meting?.id?.trim();143 if (!id) return null;144 return Object.freeze({145 provider: "meting",146 playlist: Object.freeze([]),147 meting: resolveMetingConfig(config.meting),148 defaultVolume: clampMusicVolume(config.defaultVolume),149 defaultMode: config.defaultMode,150 });151 }152
153 let rawTracks: readonly TrackDescriptor[] = [];154 if (provider === "local" || provider === "mixed") {155 rawTracks = config.tracks ?? musicTracks;156 } else if (provider === "custom") {157 rawTracks = config.tracks ?? [];158 }159
160 const usedIds = new Set<string>();161 const playlist = rawTracks162 .map((track) => normalizeTrack(track, usedIds))163 .filter((track): track is TrackDescriptor => track !== null);164
165 if (provider === "mixed") {166 const metingId = config.meting?.id?.trim();167 if (playlist.length === 0 && !metingId) return null;168 return Object.freeze({169 provider: "mixed",170 playlist: Object.freeze(playlist),171 meting: resolveMetingConfig(config.meting),172 defaultVolume: clampMusicVolume(config.defaultVolume),173 defaultMode: config.defaultMode,174 });175 }176
177 if (playlist.length === 0) return null;178
179 return Object.freeze({180 provider,181 playlist: Object.freeze(playlist),182 defaultVolume: clampMusicVolume(config.defaultVolume),183 defaultMode: config.defaultMode,184 });185}1import I18nKey from "@i18n/i18nKey";2import { i18n } from "@i18n/translation";3import { aboutConfig } from "@/config/aboutConfig";4import { albumsConfig } from "@/config/albumsConfig";5import { animeConfig } from "@/config/animeConfig";6import { compassConfig } from "@/config/compassConfig";7import { devicesConfig } from "@/config/devicesConfig";8import { friendsConfig } from "@/config/friendsConfig";9import { gamesConfig } from "@/config/gamesConfig";10import { momentsConfig } from "@/config/momentsConfig";11import { projectsConfig } from "@/config/projectsConfig";12import { seriesConfig } from "@/config/seriesConfig";13import { skillsConfig } from "@/config/skillsConfig";14import { timelineConfig } from "@/config/timelineConfig";15import type {16 NavBarConfig,17 NavBarConfigOverride,18 NavBarLink,19 NavBarLinkOverride,20} from "@/types/navBarConfig";21import { getUserConfig } from "../utils/config-overlay.ts";22import { pruneUnavailableNavLinks } from "../utils/nav-utils.ts";23
24/**25 * 导航栏配置(统一单一来源)。26 * - LinkPresets:命名链接预设表 —— 名称 / 地址 / 图标单点维护,可整体复用;27 * - navBarConfig:导航结构 —— 顺序 + 分组(children 子菜单),28 * 同时驱动顶栏下拉菜单与全端导航抽屉。29 * 新增入口:先在 LinkPresets 登记预设,再在 navBarConfig.links 按序引用。30 *31 * 内容仓可用 `config/nav-bar.yaml` 整体替换 `links`,写法见 `NavBarLinkOverride`。32 * 无论哪种来源,指向已关闭功能页面的入口都会在 `navBarConfig` 处被裁掉。33 */34export const LinkPresets: Record<string, NavBarLink> = {35 Home: {36 name: i18n(I18nKey.home),37 url: "/",38 icon: "material-symbols:home-outline-rounded",39 pageKey: "home",40 },41 Archive: {42 name: i18n(I18nKey.archive),43 url: "/archive/",44 icon: "material-symbols:archive-outline-rounded",45 pageKey: "archive",46 },47 Friends: {48 name: i18n(I18nKey.friends),49 url: "/friends/",50 icon: "material-symbols:handshake-outline-rounded",51 pageKey: "friends",52 },53 Moments: {54 name: i18n(I18nKey.moments),55 url: "/moments/",56 icon: "material-symbols:auto-awesome-outline-rounded",57 pageKey: "moments",58 },59 Anime: {60 name: i18n(I18nKey.anime),61 url: "/anime/",62 icon: "material-symbols:live-tv-outline-rounded",63 pageKey: "anime",64 },65 Compass: {66 name: i18n(I18nKey.compass),67 url: "/compass/",68 icon: "material-symbols:explore-rounded",69 pageKey: "compass",70 },71 Skills: {72 name: i18n(I18nKey.skills),73 url: "/skills/",74 icon: "material-symbols:workspaces-outline-rounded",75 pageKey: "skills",76 },77 Projects: {78 name: i18n(I18nKey.projects),79 url: "/projects/",80 icon: "material-symbols:deployed-code-outline-rounded",81 pageKey: "projects",82 },83 Devices: {84 name: i18n(I18nKey.devices),85 url: "/devices/",86 icon: "material-symbols:devices-rounded",87 pageKey: "devices",88 },89 Games: {90 name: i18n(I18nKey.games),91 url: "/games/",92 icon: "material-symbols:sports-esports-outline-rounded",93 pageKey: "games",94 },95 Timeline: {96 name: i18n(I18nKey.timeline),97 url: "/timeline/",98 icon: "material-symbols:timeline-rounded",99 pageKey: "timeline",100 },101 Albums: {102 name: i18n(I18nKey.albums),103 url: "/albums/",104 icon: "material-symbols:photo-library-outline-rounded",105 pageKey: "albums",106 },107 Categories: {108 name: i18n(I18nKey.categories),109 url: "/categories/",110 icon: "material-symbols:folder-outline-rounded",111 pageKey: "categories",112 },113 Tags: {114 name: i18n(I18nKey.tags),115 url: "/tags/",116 icon: "material-symbols:tag-rounded",117 pageKey: "tags",118 },119 Series: {120 name: i18n(I18nKey.series),121 url: "/series/",122 icon: "material-symbols:auto-stories-outline-rounded",123 pageKey: "series",124 },125 About: {126 name: i18n(I18nKey.about),127 url: "/about/",128 icon: "material-symbols:info-outline-rounded",129 pageKey: "about",130 },131 GitHub: {132 name: "GitHub",133 url: "https://github.com/LyraVoid/Shirone",134 icon: "fa6-brands:github",135 external: true,136 pageKey: "github",137 },138};139
140const defaultNavBarConfig: NavBarConfig = {141 links: [142 LinkPresets.Home,143 LinkPresets.Archive,144 LinkPresets.Friends,145 LinkPresets.Moments,146 LinkPresets.Anime,147 LinkPresets.Compass,148 LinkPresets.Albums,149 {150 name: i18n(I18nKey.more),151 icon: "material-symbols:apps-rounded",152 children: [153 LinkPresets.Timeline,154 LinkPresets.Projects,155 LinkPresets.Devices,156 LinkPresets.Games,157 LinkPresets.Skills,158 // 分类/标签入口不进导航菜单(避免菜单项过多),预设已登记指向独立页面,159 // 需要时取消注释即可160 // LinkPresets.Categories,161 // LinkPresets.Tags,162 LinkPresets.About,163 LinkPresets.GitHub,164 ],165 },166 ],167};168
169import { resolveI18nText } from "../utils/i18n-utils.ts";170
171function fail(message: string): never {172 throw new Error(`[config] nav-bar:${message}`);173}174
175function resolveName(name: string): string {176 return resolveI18nText(name);177}178
179/**180 * 已关闭功能对应的站内路由(去尾斜杠),供 `pruneUnavailableNavLinks()` 裁剪导航入口。181 *182 * 功能关闭时对应页面会 `Astro.redirect("/404/")`,因此这些路由不得再出现在导航里。183 * 关闭判定只看配置,与导航结构无关,因此默认结构与内容仓声明式条目共用同一张表。184 */185const unavailableFeatureRoutes: ReadonlySet<string> = new Set([186 ...(friendsConfig.enable ? [] : ["/friends"]),187 ...(momentsConfig.enable ? [] : ["/moments"]),188 ...(animeConfig.enable ? [] : ["/anime"]),189 ...(compassConfig.enable ? [] : ["/compass"]),190 ...(albumsConfig.enable ? [] : ["/albums"]),191 ...(skillsConfig.enable ? [] : ["/skills"]),192 ...(projectsConfig.enable ? [] : ["/projects"]),193 ...(devicesConfig.enable ? [] : ["/devices"]),194 ...(gamesConfig.enable ? [] : ["/games"]),195 ...(timelineConfig.enable ? [] : ["/timeline"]),196 ...(aboutConfig.enable ? [] : ["/about"]),197 ...(seriesConfig.enable ? [] : ["/series"]),198]);199
200/**201 * 把内容仓的声明式导航条目还原成 `NavBarLink`。202 *203 * 预设名与 i18n 词条只有在这里才能校验(`LinkPresets` 与 `I18nKey` 都住在代码仓,204 * 生成期的 Node 脚本受路径别名所限读不到),因此错误在构建加载配置时抛出。205 */206export function resolveNavBarLinks(207 entries: readonly NavBarLinkOverride[],208 presets: Record<string, NavBarLink> = LinkPresets,209): NavBarLink[] {210 return entries.map((entry) => {211 let base: NavBarLink | null = null;212 if (entry.preset !== undefined) {213 base = presets[entry.preset] ?? null;214 if (!base) {215 fail(216 `未知的预设 "${entry.preset}"。可用预设:${Object.keys(presets).join("、")}。`,217 );218 }219 }220
221 const name =222 entry.name !== undefined ? resolveName(entry.name) : base?.name;223 if (name === undefined) {224 fail("每个条目都需要 name,或用 preset 引用一个内置预设。");225 }226
227 // 未声明 children 时沿用预设自带的子菜单(已由 ...base 带入)。228 return {229 ...base,230 name,231 ...(entry.url !== undefined ? { url: entry.url } : {}),232 ...(entry.icon !== undefined ? { icon: entry.icon } : {}),233 ...(entry.pageKey !== undefined ? { pageKey: entry.pageKey } : {}),234 ...(entry.external !== undefined ? { external: entry.external } : {}),235 ...(entry.children236 ? { children: resolveNavBarLinks(entry.children, presets) }237 : {}),238 };239 });240}241
242const userNavBar = getUserConfig("navBar") as NavBarConfigOverride | undefined;243
244/**245 * 导航栏最终结构。246 *247 * 默认结构与内容仓 `config/nav-bar.yaml`(声明式列表,整体替换默认导航,不走任何248 * enable 分支)在这里汇合后统一裁剪:功能关掉时入口一并消失,两种模式下行为一致,249 * 不会留下点进去 404 的死链。250 */251export const navBarConfig: NavBarConfig = {252 links: pruneUnavailableNavLinks(253 resolveNavBarLinks(254 userNavBar ? userNavBar.links : defaultNavBarConfig.links,255 ),256 unavailableFeatureRoutes,257 ),258};1import type { PermalinkConfig } from "../types/permalinkConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * Permalink 固定链接配置6 * 控制文章 URL 路由与生成模板7 */8export const permalinkConfig: PermalinkConfig = withUserConfig("permalink", {9 /** 是否启用全局 permalink 功能,关闭时使用默认的文件名作为链接 (/posts/<slug>/) */10 enable: false,11 /**12 * permalink 格式模板13 * 支持的占位符:14 * - %year% : 4 位年份 (如 2024)15 * - %monthnum% : 2 位月份 (01-12)16 * - %day% : 2 位日期 (01-31)17 * - %hour% : 2 位小时 (00-23)18 * - %minute% : 2 位分钟 (00-59)19 * - %second% : 2 位秒数 (00-59)20 * - %post_id% : 文章序号(按发布时间升序排列,最早的文章为 1)21 * - %postname% : 文章文件名(slug,通常为全小写)22 * - %raw_postname% : 文章原始文件名(保留大小写)23 * - %category% : 分类名(无分类时为 "uncategorized")24 *25 * 示例:26 * - "%year%-%monthnum%-%postname%" => "/2024-12-my-post/"27 * - "%post_id%-%postname%" => "/42-my-post/"28 * - "%category%-%postname%" => "/tech-my-post/"29 * - "%year%/%monthnum%/%day%/%postname%" => "/2024/12/01/my-post/"30 *31 * 注意:支持使用斜杠 "/" 构建嵌套路径。32 */33 format: "%postname%",34});1import type { PostCardWidth, PostListConfig } from "@/types/postListConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 文章列表页配置:分页大小与排版布局。6 *7 * 【核心配置项】8 * - pageSize:每页展示的文章数量(默认 8 篇);9 * - layout:10 * - mode:"list"(经典纵向列表)| "grid"(双列/三列卡片网格);11 * - cover:"left"(封面在左)| "right"(封面在右,默认);12 * - cardWidth(仅在 grid 模式下生效):13 * - "compact":紧凑卡片(最小宽 20rem,适合高密度展示);14 * - "regular":标准卡片(最小宽 24rem,默认推荐);15 * - "relaxed":宽松大卡(最小宽 28rem,突出大图)。16 *17 * 注意:访客可在前端显示设置面板中动态切换 list/grid,此处为站点初始默认值。18 * GridUI 仅在主内容容器至少能容纳两张所选宽度的卡片时生效;侧栏等因素压窄19 * 内容后会暂时回退 ListUI,但保留 grid 偏好,空间恢复后自动切回。20 */21export const postListConfig: PostListConfig = withUserConfig("postList", {22 pageSize: 8,23 layout: {24 mode: "list",25 cover: "right",26 cardWidth: "regular",27 },28});29
30/** grid 档位 → 卡片最小宽度(--post-card-min 预设,与 shape/type 分档哲学同构)。31 页面框架 90rem:regular 24rem 保证宽屏为 2 列大卡(3 列窄卡会让32 日期/分类/字数元信息行换行),compact 才给密排选项。 */33export const POST_CARD_MIN_WIDTH: Record<PostCardWidth, string> = {34 compact: "20rem",35 regular: "24rem",36 relaxed: "28rem",37};1import type { ProfileConfig } from "@/types/config";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 博主资料:头像 / 名称 / 简介 / 社交链接(侧栏 Profile 卡片、页脚、RSS 作者等消费)。6 * 类型见 src/types/config.ts。7 */8export const profileConfig: ProfileConfig = withUserConfig("profile", {9 avatar: "assets/images/demo-avatar.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'10 name: "Shirone",11 bio: "The rain remembers what the sky forgot to say.",12 links: [13 {14 name: "X",15 icon: "fa6-brands:x-twitter", // Visit https://icones.js.org/ for icon codes16 // You will need to install the corresponding icon set if it's not already included17 // `pnpm add @iconify-json/<icon-set-name>`18 url: "https://x.com",19 },20 {21 name: "Steam",22 icon: "fa6-brands:steam",23 url: "https://store.steampowered.com",24 },25 {26 name: "GitHub",27 icon: "fa6-brands:github",28 url: "https://github.com/LyraVoid/Shirone",29 },30 ],31});1import type { ProjectsConfig } from "@/types/projectsConfig";2import { withUserConfig } from "../utils/config-overlay.ts";3
4/**5 * 项目页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /projects/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledKeys:可选被禁用的项目 key 列表(例如 ["folkpatch"]);11 *12 * 注:项目的具体内容数据(标题、描述、技术栈、链接、封面等)请在 `src/data/projects.ts` 中维护。13 */14export const projectsConfig: ProjectsConfig = withUserConfig("projects", {15 enable: true,16 title: "$t:projects",17 description: "$t:projectsBanner",18 categories: [19 {20 key: "theme",21 label: "Theme",22 icon: "material-symbols:palette-outline-rounded",23 },24 {25 key: "android",26 label: "Android",27 icon: "material-symbols:android-rounded",28 },29 ],30 // disabledKeys: [],31});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` 只做接线(`integrations: [shirones()]`),不直接读任何23 `src/config/` 文件;配置的最终装载入口是 `src/integration/index.ts`。245. **`integrationsConfig.ts` 是上述规则的例外**:它被 `src/integration/index.ts`25 按相对路径导入,且**禁止走 barrel**。原因见该文件头部注释——barrel 不会26 拷进用户项目,而包模式必须在构建期把它静态打进 bundle。它的 `import type`27 全部在编译期擦除,不产生运行时依赖。28
29## 配置(Behavior)与数据(Content)分层原则30
31Shirone 遵循「配置管行为,数据管内容」的清晰分层架构:32
33- **`src/config/*Config.ts`**:控制**展示行为与页面能力**(页面总开关 `enable`、分类显示顺序 `categories`、单项禁用列表 `disabledKeys`、排序方向 `order`、源切换与服务凭据);34- **`src/data/*.ts`**:承载**具体的站点内容实体**(项目条目 `projects.ts`、技能清单 `skills.ts`、时间线节点 `timeline.ts`、设备列表 `devices.ts`、友链 `friends.ts`、罗盘 `compass.ts`、番剧 `anime.ts` 与本地音乐 `music.ts`);35- **`src/utils/feature-data.ts`**:提供构建期纯函数,将 config 的过滤/排序等行为规则应用到对应 data 实体集合上,输出给页面/组件。36
37| 判别问题 | 归属 | 处理方式 |38|---|---|---|39| 它控制「页面是否开启 / 排序 / 凭据」? | **Config** | 写在 `src/config/*Config.ts`(如 `enable: boolean`, `categories`, `order`) |40| 它是「站点要展示的具体条目与说明」? | **Data** | 写在 `src/data/*.ts`(如 `ProjectItem[]`, `SkillItem[]`, `TimelineItem[]`) |41| 单项内容的停用 / 过滤? | **Config** | 在 config 中声明 `disabledKeys`(或对应 ID 列表),data 保持纯净内容 |42
43## 新增一个配置项 / 配置文件44
451. 类型定义加入 `src/types/<domain>Config.ts`(新领域则新建文件,字段带中文注释说明语义与默认值);462. 值加入 `src/config/<domain>Config.ts`,保持注释完整——注释是配置的文档;473. 新文件在 `src/config/index.ts` barrel 注册导出;用 `withUserConfig("<domain>", { ... })`48 包住默认值字面量,并在 `scripts/content/config-domains.mjs` 登记该领域(见下文「用户覆盖层」);494. **安全默认与零额外负担**:可选外部服务/重量级特性默认必须为关闭(如 `enable: false`)。在关闭或未配置时,必须满足「零外部请求、零占位 DOM、零性能损耗、零主包膨胀」的零额外负担要求;50 落地做法与验证方法见 `docs/on-demand-loading.md`;515. UI 文案走 `I18nKey` 枚举 + `i18n()`(如 `navBarConfig` 的用法),**不写死字符串**;52 新增 i18n key 必须同步补全 `src/i18n/languages/` 下全部 10 种语言;536. 跑 `npx.cmd astro check` 确认 0 错误 0 警告。54
55## 用户覆盖层(内容仓 `config/*.yaml`)56
57本目录的每个领域配置都把自己的字面量默认值交给 `withUserConfig()`:58
59```typescript60export const siteConfig: SiteConfig = withUserConfig("site", {61 title: "Shirone",62 // ...默认值连同注释一起留在代码仓63});64```65
66`local` 模式下 `withUserConfig()` 原样返回默认值,零开销。`external` 模式下,67内容仓 `config/site.yaml` 里的键会与默认值**深合并**(对象递归合并、数组整体替换)68后返回,合并源是 `pnpm content:sync` 生成的 `src/user/user-config.ts`。69
70因此本目录的定位没有变:**它是默认值与配置文档的唯一真源**,71注释写得越清楚,内容仓那边越不需要猜。契约与 YAML 写法见72[`docs/content-separation/config-overlay.md`](../../docs/content-separation/config-overlay.md)。73
74新增配置领域时,除了本文下方的清单,还要在75`scripts/content/config-domains.mjs` 补一行登记(领域名、YAML 文件名、类型),76它同时驱动生成、校验与 `content:eject` 的起步文件;`tests/content/content-config.test.mjs`77会检查登记表的文件名唯一且为 kebab-case。78
79`navBarConfig` 是唯一不走 `withUserConfig()` 的领域:导航项要引用 `LinkPresets`80并调用 `i18n()`,深合并只会得到一堆未解析的引用,因此它由 `resolveNavBarLinks()`81把内容仓的声明式条目还原成 `NavBarLink`。82
83`config/nav-bar.yaml` 是**整体替换**,不会跟着各功能的 `enable` 走,因此84`navBarConfig` 在任何来源的条目汇合后都会过一遍 `pruneUnavailableNavLinks()`85(`src/utils/nav-utils.ts`):指向已关闭功能页面的入口被裁掉,空掉的下拉分组一并隐藏。86判定按站内路由(去尾斜杠、忽略查询串与哈希),站外链接与锚点不受影响。87
88### 反向导出覆盖层(`content:export --config`)89
90覆盖层是双向的:`pnpm content:export --config` 会求「当前生效配置」与「主题默认值」的差,91把最小覆盖集写回内容仓的 `config/*.yaml`(保留用户已有的注释与格式,只增改不删键)。92它是 `deepMerge` 的精确逆运算,因此喂回 `content:sync` 之后生效配置逐字段不变。93
94典型用途是救援:`content:clean` 会把 `src/user/user-config.ts` 重置成空覆盖层,95在那份生成物里的改动会因此丢失;导出能先把它固化成 YAML。默认只预演,用法与安全机制见96[`docs/content-separation/cli-workflows.md`](../../docs/content-separation/cli-workflows.md)。97
98两条边界要知道:99
100- **`navBar` 不参与导出**。`resolveNavBarLinks()` 的解析不可逆,`config/nav-bar.yaml` 只能手工维护;101- **不会把本目录的源码改动提升成覆盖**。直接修改 `<domain>Config.ts` 里的默认值字面量时,102 「默认值」与「生效值」同步变化,差分为空,那处改动不会进入导出计划。103 这是有意为之:把当前默认值提升成内容仓的永久覆盖,等于把配置冻结在这一版主题上。104 想固化就在 fork 里提交它,或照常在内容仓写一条对应的 YAML 覆盖。105
106领域键与 YAML 文件名一律 kebab-case 对应驼峰:`llms` ↔ `config/llms.yaml` ↔ `llmsConfig`,107`postList` ↔ `config/post-list.yaml` ↔ `postListConfig`。108
109## 现有配置一览110
111| 文件 | 职责 |112|---|---|113| `footerConfig.ts` | 页脚自定义 HTML 注入开关(控制是否读取并注入 `src/config/FooterConfig.html`,关闭时零开销) |114| `siteConfig.ts` | 站点部署 URL / base 路径 / 标题标识 / 语言 / IANA 时区 / HCT 主题色 / 背景纹理系统 / 显示设置浮层开关 / 横幅 / TOC 深度 / 进度条 / favicon(含 `getDefaultStyle` / `getDefaultSpec` / `resolveDisplaySettings` 回退值) |115| `profileConfig.ts` | 博主资料:头像 / 名称 / 简介 / 社交链接 |116| `licenseConfig.ts` | 文章版权声明 |117| `expressiveCodeConfig.ts` | 代码块明暗主题 |118| `navBarConfig.ts` | 导航栏链接(`LinkPresets` 预设表 + 组装) |119| `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) |120| `fabConfig.ts` | 右下角悬浮控制流(FAB)配置:总开关、各操作项(返回顶部、悬浮目录、直达评论、返回首页、自定义操作)、细粒度设备受控矩阵(`devices?: ("mobile" | "tablet" | "desktop")[]`)、页面范围过滤与图标定制;架构见 `docs/fab-system.md` |121| `announcementConfig.ts` | 公告内容(侧栏 announcement widget 消费,text 为空不渲染) |122| `musicConfig.ts` | 侧栏音乐全局配置:总开关(默认关闭)、`provider` 模式切换接口、`defaultVolume` 初始音量与 `defaultMode` 初始播放模式(本地曲目清单维护在 `src/data/music.ts`);与 `sidebarConfig` 的 music 条目共同控制 `MusicSidebar`,详见下文与 `docs/sidebar-widgets.md` |123| `postListConfig.ts` | 文章列表:分页大小 + 布局(list/grid 模式、封面位置、grid 卡片宽度档位) |124| `articleConfig.ts` | 文章详情:最后更新提示、延伸阅读(相关/随机文章抽样)、以及文章尾部分享区块(总开关、海报生成与封面配置) |125| `commentConfig.ts` | 评论系统:全局开关(默认关闭)、Provider 选择(Twikoo / Giscus)、视口懒加载与服务凭据配置;Giscus 基于 GitHub Discussions(需公开仓库 + 安装 giscus App + 从 giscus.app 取 repoId/categoryId),主题明暗双值跟随站点切换 |126| `contextMenuConfig.ts` | 桌面端右键增强:可选开关(当前默认开启);配置允许页面与操作顺序,关闭时零 DOM、零监听器、零客户端资源 |127| `umamiConfig.ts` | Umami 统计:全局开关(默认关闭)、公开分享统计读取,以及可选的官方访问采集脚本配置;支持内容仓 `config/umami.yaml` 覆盖(领域键 `umami`) |128| `integrationsConfig.ts` | 所有模式(源码仓 + npm 包项目)共用的集成选项,唯一装载入口是 `src/integration/index.ts`:swup / astro-icon / expressive-code / svelte / mdx 的选项、`vite.build` 共用部分、`trailingSlash` 与 `image.endpoint.route` 的配对、音乐侧栏虚拟模块 id。**本目录里唯一的例外**:不走 barrel、不经 `withUserConfig`、也不被 `loadConfigModule` 动态加载(包模式在构建期把它打进 `dist/index.js`),所以它没有用户覆盖层,用户项目里的那份拷贝是死的 |129| `sitemapFilter.ts` | 由 `*Config.enable === false` 推导被关闭的页面清单,供 `sitemap()` 的 `filter` 排除它们。包模式通过 `loadConfigModule` 加载,用户可自行覆盖 |130| `skillsConfig.ts` | 技能页行为控制:页面总开关、分类清单与单项禁用列表(技能内容维护在 `src/data/skills.ts`);关闭页面时导航入口同步隐藏 |131| `projectsConfig.ts` | 项目页行为控制:页面总开关、分类清单与单项禁用列表(项目内容维护在 `src/data/projects.ts`);关闭页面时导航入口同步隐藏 |132| `timelineConfig.ts` | 时间线页行为控制:页面总开关、分类清单、排序方向与单项禁用列表(时间线内容维护在 `src/data/timeline.ts`);关闭页面时导航入口同步隐藏 |133| `devicesConfig.ts` | 设备页行为控制:页面总开关、场景分类清单与单项禁用列表(设备清单维护在 `src/data/devices.ts`);关闭页面时导航入口同步隐藏 |134| `gamesConfig.ts` | 游戏页行为控制:页面总开关、游戏分类清单与单项禁用列表(游戏清单维护在 `src/data/games.ts`,封面支持 src/assets 相对路径、/public 绝对路径与远程 URL);关闭页面时导航入口同步隐藏 |135| `animeConfig.ts` | 番剧页与外部追番数据源:数据源选择(本地 / Bangumi 快照 / Bilibili 快照)、失败降级、提供方凭据环境配置与快照生命周期管理(本地番剧维护在 `src/data/anime.ts`) |136| `llmsConfig.ts` | 大语言模型与 AI 友好内容系统:`/llms.txt`(索引)与 `/llms-full.txt`(全量正文汇编)静态端点生成控制、加密文章过滤、排除标签与自定义章节配置;支持内容仓 `config/llms.yaml` 覆盖(领域键 `llms`) |137| `seriesConfig.ts` | 系列连载:`/series/` 索引与 `/series/<slug>/` 详情页总开关(默认开启)、索引页 `title`/`description`(`$t:` i18n 引用或字面量,描述留空用动态汇总)、文章内系列块位置 `cardPosition: "top" \| "bottom"`(top = 元信息区下方、正文之前;bottom = 正文之后、版权/分享/延伸阅读之前);支持内容仓 `config/series.yaml` 覆盖(领域键 `series`)。关闭时系列页 404、导航/侧栏/sitemap 入口裁剪、文章内系列卡消失(系列实体与文章 frontmatter 见 `shirone-writing` 与内容分离文档) |138
139非首页 Banner 的标题、说明和可选日期由各页面通过 `MainGridLayout` 提供,并在 Swup 导航后从被替换的主内容容器同步。该上下文默认显示、不设配置开关;说明为空或与标题相同时自动省略,移动端非首页仍沿用紧凑布局并隐藏 Banner。140
141## 侧栏编排与页框宽度142
143- `arrangement: "single"`(默认)——全部 widget 渲染进唯一侧栏,页框 85rem;144- `arrangement: "dual"`——`column: "secondary"` 的 widget 进入副栏(视口 ≥ 1280px 起三列),145 其余留在主栏;页框自动放宽到 96rem,TOC 悬浮 rail 自动让位(右侧余量被副栏占据)。146 1280px 以下自动退化为单栏(只显主栏),无需配置。147- `side: "left" | "right"` 决定主栏物理侧,dual 线下副栏落在对面。148- 页框宽度用 `resolvePageWidth()`(`src/utils/responsive-utils.ts`)按编排自动解析,149 常量在 `src/constants/constants.ts`(`PAGE_WIDTH` / `PAGE_WIDTH_DUAL`),不提供手动覆盖。150
151## 右下角悬浮控制流(FAB)配置契约152
153`fabConfig.ts` 控制右下角悬浮操作栏的呈现与交互:154
155```typescript156export const fabConfig: FabConfig = {157 enable: true,158 items: [159 {160 type: "top",161 enable: true,162 icon: "material-symbols:keyboard-arrow-up-rounded",163 // 未指定 devices 时全设备生效;滚过横幅高度阈值后平滑浮现164 },165 {166 type: "toc",167 enable: true,168 icon: "material-symbols:format-list-bulleted-rounded",169 pages: ["post"],170 // 桌面端侧栏已有粘性 TOC,故默认仅在移动端与平板端显示悬浮目录171 devices: ["mobile", "tablet"],172 },173 {174 type: "comment",175 enable: true,176 icon: "material-symbols:comment-outline-rounded",177 pages: ["post"],178 devices: ["mobile", "tablet"],179 },180 {181 type: "home",182 enable: false,183 icon: "material-symbols:home-outline-rounded",184 },185 ],186};187```188
189### 核心规则与受控特性190
1911. **细粒度设备受控(`devices`)**:192 - `"mobile"`(< 768px)193 - `"tablet"`(768px ~ 1023px)194 - `"desktop"`(≥ 1024px)195 - 省略时默认所有设备均允许;SSR 阶段直接输出 Tailwind CSS 响应式类(如 `flex lg:hidden`),零首屏闪烁,CLS = 0。1962. **页面范围过滤(`pages`)**:197 - 过滤逻辑与侧栏 `SidebarPage` 统一,通过 `#swup-container` 的 `data-current-page` 在 Swup 站内导航时联动隐藏/显示。1983. **评论按钮零额外负担**:199 - 全局未开启评论系统(`commentConfig.enable: false`)或当前文章关闭评论时,评论 FAB 按钮产物为 0 DOM,零多余外链请求与布局偏移。2004. **无音乐挂件(保持纯粹)**:201 - FAB 控制流不集成音乐播放器,避免与侧边栏 `MusicSidebar` 产生双重状态混乱及包体积膨胀。202
203## 侧栏音乐启用契约与四种模式204
205侧栏音乐是默认关闭的可选能力,必须同时满足以下三项才加载并渲染:206
2071. `musicConfig.enable` 为 `true`;2082. 对应数据源(本地 `src/data/music.ts`、自定义 `tracks` 或 `meting` 远端歌单)至少包含一首有效曲目/合法歌单 ID;2093. `sidebarConfig.components` 中 `type: "music"` 的条目存在且 `enable: true`(默认条目为 `false`)。210
211### 四种数据源工作模式(`provider`)212
213| 模式 | 配置名 | 数据源 | 特点与适用场景 |214|---|---|---|---|215| **本地模式** | `"local"` | `src/data/music.ts` | 默认模式。零外部 API 依赖,构建期静态打包,首屏毫秒级就绪,支持离线/断网播放。 |216| **自定义列表** | `"custom"` | `musicConfig.tracks` | 灵活自定义。直接在配置中传入曲目数组(支持外链音频与封面),无需修改通用数据文件。 |217| **云端歌单** | `"meting"` | `musicConfig.meting` | 接入 Meting API(网易云、QQ 音乐、酷狗等),客户端异步按需拉取,曲目与封面自动清洗加载。 |218| **混合增强模式** | `"mixed"` | 本地数据 + Meting API | **推荐**。首屏立即播放本地音乐,后台无感拉取远端歌单并在就绪后无缝追加合并;若遇断网或云端接口故障,自动静默降级为本地曲目,绝不破版。 |219
220任一条件不满足时,音乐功能不得输出 DOM 或样式,不得请求音频/封面等资源,也不得把播放器代码或依赖带入主 bundle。配置消费者应先完成三项校验,再动态加载 `MusicSidebar`;不能用隐藏空卡片代替短路。221
222`defaultVolume` 与 `defaultMode` 只定义播放器首次初始化的音量和播放模式。播放器挂载后由持久侧栏运行时持有当前曲目、播放位置、音量与模式,Swup 站内导航不应重新读取默认值或重建播放器。1import type { SeriesConfig } from "../types/seriesConfig.ts";2import { withUserConfig } from "../utils/config-overlay.ts";3
4export const seriesConfig: SeriesConfig = withUserConfig("series", {5 enable: true,6 title: "$t:series",7 // 空 = 使用动态汇总(「x 个系列 · y 篇文章」)8 description: "",9 cardPosition: "bottom",10});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" | "series" | "stats" | "calendar" | "toc");14 * - enable:是否启用该 widget;15 * - slot:"top"(固定在顶部)| "sticky"(页面滚动时吸顶跟随);16 * - column:"primary"(主栏,默认)| "secondary"(副栏,仅在 arrangement: "dual" 时生效);17 * - pages:仅在指定页面展示(如 ["home", "post"],省略时默认全页面展示);18 * - collapseAfter:折叠阈值(适用于 categories/tags/series,超出条数显示展开按钮)。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 "games",49 "timeline",50 "albums",51 "about",52 "post",53 ],54 },55 {56 type: "series",57 enable: true,58 slot: "sticky",59 collapseAfter: 5,60 pages: [61 "home",62 "archive",63 "friends",64 "moments",65 "anime",66 "compass",67 "skills",68 "projects",69 "devices",70 "games",71 "timeline",72 "albums",73 "about",74 "post",75 ],76 },77 {78 type: "tags",79 enable: true,80 slot: "sticky",81 collapseAfter: 6,82 pages: [83 "home",84 "archive",85 "friends",86 "moments",87 "anime",88 "compass",89 "skills",90 "projects",91 "devices",92 "games",93 "timeline",94 "albums",95 "about",96 "post",97 ],98 },99 {100 type: "stats",101 enable: true,102 slot: "top",103 column: "secondary",104 pages: ["home", "archive", "categories", "tags"],105 },106 { type: "calendar", enable: true, slot: "top", column: "secondary" },107 {108 type: "toc",109 enable: true,110 slot: "sticky",111 column: "secondary",112 pages: ["post"],113 },114 ],115});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 /*31 * lang 与 themeColor.hue 保持主题默认值:本站的实际取值由内容仓32 * config/site.yaml 覆盖(zh_CN / 345)。在这里再写一遍个人值是无效的33 * ——内容仓覆盖优先——而且会造成「改了这里却没生效」的假象。34 * 想改语言或主题色,改内容仓那份。35 */36 lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.37 // IANA time zone for precise post and moment timestamps. It is independent of lang.38 timeZone: "Asia/Shanghai",39 themeColor: {40 hue: 315, // Default hue 0-360. 站点设计默认粉紫(偏二次元);262 紫 / 345 粉 也可选41 fixed: false, // Hide the theme color picker for visitors42 // Dynamic Material 3 palette style (TonalSpot/Vibrant/Content/Expressive/Rainbow/FruitSalad/Monochrome/Neutral/Fidelity)43 style: "tonalSpot",44 // Design spec version: "2021" (MD3) or "2025" (M3 Expressive)。角色集一致,45 // 差异仅在调色板派生(库的 colorSpec 静态为 2025 委托)46 spec: "2025",47 },48 // 默认页面背景模式:"banner" 使用壁纸横幅,"none" 使用主题纯色。49 // 访客在“显示设置”中的选择会保存在浏览器中,并覆盖这里的默认值。50 wallpaperMode: {51 defaultMode: "banner",52 },53 // 页面背景纹理系统配置(5 大精美预设 + 零开销 HCT 动态取色)54 texture: {55 enable: true, // 是否启用背景纹理系统56 defaultPreset: "starlight", // 默认纹理预设:"none" | "starlight" | "cyber-dots" | "topography" | "geometric" | "sakura"57 defaultOpacity: 0.12, // 默认纹理浓度 (0.05 ~ 0.25)58 allowMotion: true, // 是否允许背景微动效(开启 reduced-motion 时自动静止)59 },60 banner: {61 // 推荐将图片放入 src/assets,并填写相对 src 的路径,以启用构建期 AVIF/WebP 响应式优化。62 // 以 "/" 开头的 public 路径与远程 URL 仍可用,但会保留原图、不生成候选。63 // desktop 用于 >= 1024px;mobile 仅用于 < 1024px 的首页,手机非首页不显示壁纸。64 // 数组顺序就是轮播顺序;只需要静态 Banner 时,每组保留一张图片即可。65 //66 // 本站的实际壁纸由内容仓 config/site.yaml 覆盖,见该文件的 banner.src。67 src: {68 desktop: ["assets/images/banner/desktop/1.webp"],69 mobile: ["assets/images/banner/mobile/1.webp"],70 },71 // 图片裁切焦点:"top"、"center" 或 "bottom"。72 position: "center",73 dim: {74 // 在图片上覆盖黑色遮罩以提高标题和顶部栏的对比度;opacity 范围为 0-1。75 enable: true,76 opacity: 0.24,77 },78 homeText: {79 // 仅在首页 Banner 中显示,标题与副标题会上下居中排列。80 enable: true,81 title: "Shirone",82 subtitle: [83 "特別なことはないけど、君がいると十分です",84 "今でもあなたは私の光",85 "君ってさ、知らないうちに私の毎日になってたよ",86 "君と話すと、なんか毎日がちょっと楽しくなるんだ",87 "今日はなんでもない日。でも、ちょっとだけいい日",88 ],89 typewriter: {90 // 副标题逐字显示;关闭后直接显示完整副标题。91 enable: true,92 // 打字速度(每个字符间隔,毫秒)。93 speed: 100,94 // 回退反向删除速度(每个字符间隔,毫秒)。95 deleteSpeed: 50,96 // 打字完成后停顿时间,单位为毫秒。97 pauseTime: 2000,98 // 完成后是否循环播放;关闭表示只播放一次。99 loop: true,100 },101 },102 carousel: {103 // 是否开启多张图片自动轮播;多张图片时生效,单张图片时自动降级为静态展示。104 enable: true,105 // 轮播切换间隔时间(毫秒),运行时最小值限制为 3000ms。106 interval: 6000,107 // 交叉淡入淡出(Crossfade)过渡时长(毫秒,默认 1200ms)。108 fadeDuration: 1200,109 // 运镜呼吸动画模式:"ken-burns"(默认,循环运镜)| "zoom-in"(推进)| "zoom-out"(拉远)| "pan-left"(左移)| "pan-right"(右移)| "none"(无运镜)。110 animation: "ken-burns",111 },112 waves: {113 // 在 Banner 底部渲染页面背景色水波纹;关闭后不输出波浪 DOM。114 enable: true,115 },116 },117 // Markdown 正文图片处理;仅匹配远程图片,不会产生额外网络请求或客户端代码。118 imageOptimization: {119 // 为需要防盗链兼容的图片 CDN 添加 referrerpolicy="no-referrer",支持通配符。120 noReferrerDomains: ["*.hdslb.com"],121 },122 toc: {123 enable: true, // Display the table of contents on the right side of the post124 depth: 2, // Maximum heading depth to show in the table, from 1 to 3125 },126 progressIndicator: {127 // 进度条预设样式:dual 双向扫描(官方默认双线)/ single 单向扫描(单线)128 style: "dual",129 },130 favicon: [131 // 浏览器标签页图标,路径相对于 public 目录。132 //133 // 本站使用图床图标,由内容仓 config/site.yaml 的 favicon 覆盖。134 { src: "/logo/icon.webp" },135 ],136});137
138/**139 * 解析并返回背景纹理配置选项(包含关闭短路与 0 开销优化判定)140 */141export function resolveTextureOptions(142 config: boolean | TextureConfig | undefined = siteConfig.texture,143 displaySettingsTexture: boolean = siteConfig.displaySettings?.texture ?? true,144): ResolvedTextureOptions {145 if (config === false || config === undefined) {146 return {147 enable: false,148 defaultPreset: "none",149 defaultOpacity: 0.12,150 allowMotion: false,151 };152 }153
154 if (config === true) {155 return {156 enable: true,157 defaultPreset: "starlight",158 defaultOpacity: 0.12,159 allowMotion: true,160 };161 }162
163 const enable = config.enable ?? true;164 const defaultPreset = config.defaultPreset ?? "starlight";165 const defaultOpacity = config.defaultOpacity ?? 0.12;166 const allowMotion = config.allowMotion ?? true;167
168 // 性能短路优化:169 // 如果配置 enable: false,或者 defaultPreset: "none" 且显示设置面板未允许切换(访客也无法开启),170 // 则自动视为完全关闭以达成零 DOM、零 CSS、零运行时代价。171 const effectiveEnable =172 enable && (defaultPreset !== "none" || displaySettingsTexture);173
174 return {175 enable: effectiveEnable,176 defaultPreset,177 defaultOpacity,178 allowMotion,179 };180}181
182/** 站点默认配色风格(访客未做选择时的回退值) */183export function getDefaultStyle(): string {184 return siteConfig.themeColor.style;185}186
187/** 站点默认 Color Spec(2021 / 2025) */188export function getDefaultSpec(): string {189 return siteConfig.themeColor.spec;190}191
192/** 解析并返回显示设置面板各项开关(未配置时默认 true) */193export function resolveDisplaySettings(): {194 colorStyle: boolean;195 colorSpec: boolean;196 wallpaperMode: boolean;197 layoutMode: boolean;198 reduceMotion: boolean;199 texture: boolean;200} {201 const cfg = siteConfig.displaySettings;202 const textureOpts = resolveTextureOptions(203 siteConfig.texture,204 cfg?.texture ?? true,205 );206 return {207 colorStyle: cfg?.colorStyle ?? true,208 colorSpec: cfg?.colorSpec ?? true,209 wallpaperMode: cfg?.wallpaperMode ?? true,210 layoutMode: cfg?.layoutMode ?? true,211 reduceMotion: cfg?.reduceMotion ?? true,212 texture: textureOpts.enable && (cfg?.texture ?? true),213 };214}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 { gamesConfig } from "./gamesConfig.ts";8import { momentsConfig } from "./momentsConfig.ts";9import { projectsConfig } from "./projectsConfig.ts";10import { seriesConfig } from "./seriesConfig.ts";11import { skillsConfig } from "./skillsConfig.ts";12import { timelineConfig } from "./timelineConfig.ts";13
14/** 获取所有被配置 enable: false 关闭的页面标识清单 */15export function getDisabledPages(): string[] {16 const disabled: string[] = [];17 if (skillsConfig.enable === false) disabled.push("skills");18 if (projectsConfig.enable === false) disabled.push("projects");19 if (timelineConfig.enable === false) disabled.push("timeline");20 if (devicesConfig.enable === false) disabled.push("devices");21 if (gamesConfig.enable === false) disabled.push("games");22 if (animeConfig.enable === false) disabled.push("anime");23 if (aboutConfig.enable === false) disabled.push("about");24 if (friendsConfig.enable === false) disabled.push("friends");25 if (momentsConfig.enable === false) disabled.push("moments");26 if (albumsConfig.enable === false) disabled.push("albums");27 if (compassConfig.enable === false) disabled.push("compass");28 if (seriesConfig.enable === false) disabled.push("series");29 return disabled;30}31
32/**33 * 校验页面路径是否应当被收录进 sitemap。34 * 排除被关闭页面(例如 /skills/, /skills/index.html 等)以及 404 跳转存根。35 */36export function isSitemapPageAllowed(pageUrl: string): boolean {37 const disabled = getDisabledPages();38 for (const p of disabled) {39 if (40 pageUrl.endsWith(`/${p}/`) ||41 pageUrl.endsWith(`/${p}`) ||42 pageUrl.includes(`/${p}/`)43 ) {44 return false;45 }46 }47 return true;48}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 title: "$t:skills",17 description: "$t:skillsBanner",18 categories: [19 {20 key: "frontend",21 label: "Frontend",22 icon: "material-symbols:web-rounded",23 },24 {25 key: "backend",26 label: "Backend",27 icon: "material-symbols:dns-rounded",28 },29 {30 key: "tooling",31 label: "Tooling",32 icon: "material-symbols:construction-rounded",33 },34 ],35 // disabledNames: [],36});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 title: "$t:timeline",18 description: "$t:timelineBanner",19 categories: [20 {21 key: "milestone",22 label: "Milestones",23 icon: "material-symbols:flag-rounded",24 },25 {26 key: "project",27 label: "Projects",28 icon: "material-symbols:code-rounded",29 },30 {31 key: "career",32 label: "Career",33 icon: "material-symbols:work-rounded",34 },35 {36 key: "education",37 label: "Education",38 icon: "material-symbols:school-rounded",39 },40 {41 key: "life",42 label: "Life",43 icon: "material-symbols:favorite-rounded",44 },45 ],46 order: "desc",47 // disabledTitles: [],48});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 *26 * ## 为什么不能因为 `shareUrl` 缺失就返回 null27 *28 * 早先这里在 `shareUrl` 为空时直接返回 null,于是「只采集、不读取」这个组合29 * 静默失效:`Layout.astro` 的采集脚本挂在 `umamiOptions?.websiteId` 之下,30 * 而 `umamiOptions` 恒为 null,脚本永远不注入——配了 Tracking code 却没有任何31 * 数据上报,且没有任何报错。这既与文档描述的「两层独立」矛盾,也是采集层32 * 事实上无法启用的根因。33 *34 * 现在两层各自判断:35 * - 读取层:`shareUrl` 非空36 * - 采集层:`websiteId` 与 `scriptUrl` **同时**非空37 *38 * 任何一层配齐就返回配置对象;两层都空才返回 null(此时真的无事可做)。39 */40export function resolveUmamiOptions(config: UmamiConfig): ResolvedUmamiOptions {41 if (!config.enable) {42 return null;43 }44
45 const shareUrl = config.shareUrl?.trim() || undefined;46 const websiteId = config.websiteId?.trim() || undefined;47 const scriptUrl = config.scriptUrl?.trim() || undefined;48
49 // 采集层要求成对出现:只有 id 没有脚本地址(或反之)都无法上报50 const collect = Boolean(websiteId && scriptUrl);51
52 // 两层都没配齐:保持「零加载」承诺——不注入运行时,也不渲染任何占位53 if (!shareUrl && !collect) {54 return null;55 }56
57 return {58 ...(shareUrl ? { shareUrl } : {}),59 ...(collect ? { websiteId, scriptUrl } : {}),60 };61}62
63export type { ResolvedUmamiOptions };源码
1@[code-tree title="站点配置目录" entry="siteConfig.ts"](/src/config)导入写法是 @[code-tree 属性...](目录路径),路径必须以 / 开头、指向站点仓内的目录。构建期会以 UTF-8 读取文件,并忽略隐藏目录、依赖目录(如 node_modules)、构建目录与大于 256KB 的文件。
交互特性#
- 双栏联动:点击左侧文件名即时切换右侧代码与高亮;
- 默认入口:
entry="文件名"决定首次加载展示哪个文件; - 自适应滚动:高度与滚动条经无障碍与移动端优化,窄屏下改为上下布局。
第 15 篇,共 23 篇·本文属于系列
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
沿着主题读
基于共同的标签与分类
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 301 天
部分内容可能已过时
评论