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)1/**2 * 番剧收藏数据(本地数据源)。3 * 用于番剧页:src/pages/anime.astro → organisms/AnimeSection → molecules/AnimeCard。4 *5 * 添加条目:在 animeData 中追加一项即可,状态筛选 chips 与计数自动生成。6 * - cover 省略时卡片显示主题色渐变占位(补图前不破版);7 * - link 省略时封面不可点;rating 为 0-10 个人评分;8 * - progress 是结构化追番进度,watching 状态在卡片上渲染进度条。9 * JSON 数据源(外部收藏服务拉取)见 utils/anime-data.ts 的 AnimeSource 分发。10 */11
12import type { AnimeIdentity } from "@/types/animeConfig.ts";13
14/** 收藏状态(Bangumi 领域通行五态) */15export type AnimeStatus =16 | "watching"17 | "completed"18 | "planned"19 | "onHold"20 | "dropped";21
22export interface AnimeItem {23 title: string;24 /** 封面图地址(相对 /public 或绝对 URL);省略 = 渐变占位 */25 cover?: string;26 /** 条目外链(Bangumi/官方站等);省略则封面不可点 */27 link?: string;28 status: AnimeStatus;29 /** 个人评分 0-10 */30 rating: number;31 /** 追番进度:已看 / 总集数(未知时可选省略) */32 progress?: { watched: number; total: number };33 /** 一句话感想 */34 description?: string;35 /** 放送年份(展示用) */36 year: string;37 /** 制作公司 */38 studio?: string;39 /** 题材标签 */40 genres: string[];41 /** 观看时间段(年-月) */42 period?: { start: string; end: string };43 /** 条目来源身份标识(可选,用于跨源去重与归档) */44 identity?: AnimeIdentity;45}46
47export const animeData: AnimeItem[] = [48 {49 title: "Lycoris Recoil",50 cover: "/assets/anime/lkls.webp",51 link: "https://www.bilibili.com/bangumi/media/md28338623",52 status: "completed",53 rating: 9.8,54 progress: { watched: 12, total: 12 },55 description: "Girl's gunfight",56 year: "2022",57 studio: "A-1 Pictures",58 genres: ["Action", "Slice of Life"],59 period: { start: "2022-07", end: "2022-09" },60 },61 {62 title: "Yowamushi Pedal",63 cover: "/assets/anime/rynh.webp",64 link: "https://www.bilibili.com/bangumi/media/md2590",65 status: "watching",66 rating: 9.5,67 progress: { watched: 8, total: 12 },68 description: "Girl's daily life, sweet and healing",69 year: "2015",70 studio: "Nexus",71 genres: ["Daily life", "Healing"],72 period: { start: "2015-07", end: "2015-09" },73 },74 {75 title: "Asteroid in Love",76 cover: "/assets/anime/laxxx.webp",77 link: "https://www.bilibili.com/bangumi/media/md28224128",78 status: "watching",79 rating: 9.2,80 progress: { watched: 5, total: 12 },81 description: "Meeting girls among the stars, pure love and healing",82 year: "2020",83 studio: "Doga Kobo",84 genres: ["Romance", "Healing"],85 period: { start: "2020-01", end: "2020-03" },86 },87 {88 title: "Is the Order a Rabbit?",89 cover: "/assets/anime/tz1.webp",90 link: "https://www.bilibili.com/bangumi/media/md2762",91 status: "planned",92 rating: 9.0,93 progress: { watched: 12, total: 12 },94 description: "A group of girls' warm daily life",95 year: "2014",96 studio: "White Fox",97 genres: ["Daily life", "Healing"],98 period: { start: "2014-04", end: "2014-06" },99 },100 {101 title: "The Secret of the Magic Girl",102 cover: "/assets/anime/cmmn.webp",103 link: "https://www.bilibili.com/bangumi/media/md26625039",104 status: "watching",105 rating: 9.0,106 progress: { watched: 8, total: 12 },107 description: "Muli, Muli!",108 year: "2024",109 studio: "C2C",110 genres: ["Daily life", "Healing", "Magic"],111 period: { start: "2025-07", end: "2025-10" },112 },113];1/**2 * 站点罗盘数据(本地数据源)。3 * 用途:src/pages/compass.astro → organisms/CompassSection → molecules/CompassTile。4 * 添加站点:往对应 Shelf.entries 追加一项;数组顺序即展示顺序。5 * - icon:Iconify 名(material-symbols:xxx)或图片 URL(http(s)/绝对路径);6 * 省略时瓷砖显示 label 首字母 tonal 块(不自动抓取 favicon)。7 * - image:用户自定义图片 URL(http(s)/绝对路径),优先于 icon 渲染;8 * 加载失败自动降级为首字母块。9 */10
11/** 单条站点记录 */12export interface CompassEntry {13 /** 站点名(瓷砖标题) */14 label: string;15 /** 外链地址 */16 href: string;17 /** 一句话说明(瓷砖副行;省略则显示域名) */18 note?: string;19 /** 图标:Iconify 名或图片 URL;省略 = 首字母兜底 */20 icon?: string;21 /** 用户自定义图片(http(s)/绝对路径):优先于 icon 渲染;省略则走 icon/首字母 */22 image?: string;23}24
25/** 分组(Shelf = 罗盘上的收纳格) */26export interface CompassShelf {27 /** 锚点 id(字母数字,作分组定位与跳转) */28 key: string;29 /** 分组名 */30 name: string;31 /** 分组图标(Iconify 名,SectionTitle 行首) */32 icon?: string;33 /** 分组副文案(标题下弱文本,可选) */34 blurb?: string;35 entries: CompassEntry[];36}37
38export const compassData: CompassShelf[] = [39 {40 key: "dev",41 name: "Development",42 icon: "material-symbols:code-rounded",43 blurb: "Sites I keep open while writing code",44 entries: [45 {46 label: "GitHub",47 href: "https://github.com",48 note: "Code hosting & collaboration",49 icon: "fa6-brands:github",50 },51 {52 label: "MDN",53 href: "https://developer.mozilla.org",54 note: "Authoritative web docs",55 icon: "material-symbols:menu-book-rounded",56 },57 {58 label: "Stack Overflow",59 href: "https://stackoverflow.com",60 note: "Q&A and debugging",61 },62 ],63 },64 {65 key: "design",66 name: "Design",67 icon: "material-symbols:palette-outline-rounded",68 blurb: "Colors, icons and inspiration",69 entries: [70 {71 label: "Iconify",72 href: "https://icon-sets.iconify.design",73 note: "Searchable open-source icon sets",74 },75 {76 label: "Material Symbols",77 href: "https://fonts.google.com/icons",78 note: "Official M3 icon set",79 icon: "material-symbols:star-rounded",80 },81 {82 label: "Excalidraw",83 href: "https://excalidraw.com",84 note: "Hand-drawn whiteboard collaboration",85 },86 ],87 },88 {89 key: "tools",90 name: "Tools",91 icon: "material-symbols:build-outline-rounded",92 entries: [93 {94 label: "Squoosh",95 href: "https://squoosh.app",96 note: "Image compression & conversion",97 },98 {99 label: "Regex101",100 href: "https://regex101.com",101 note: "Regex testing & debugging",102 },103 ],104 },105 {106 key: "reads",107 name: "Reading",108 icon: "material-symbols:auto-stories-outline-rounded",109 entries: [110 { label: "Hacker News", href: "https://news.ycombinator.com" },111 { label: "V2EX", href: "https://www.v2ex.com" },112 {113 label: "Solidot",114 href: "https://www.solidot.org",115 note: "Tech and culture news",116 },117 ],118 },119];1/**2 * 设备展示页数据源(纯内容)。3 * 页面展示与筛选规则由 src/config/devicesConfig.ts 控制。4 */5import type { DeviceItem } from "@/types/devicesConfig";6
7export const devicesData: DeviceItem[] = [8 {9 id: "macbook-pro-16",10 name: 'MacBook Pro 16"',11 brand: "Apple",12 category: "desk",13 status: "active",14 specs: "M3 Max / 64GB / 2TB",15 description:16 "Primary workstation for development, design, and heavy rendering workloads.",17 icon: "material-symbols:laptop-mac-rounded",18 featured: true,19 year: "2024",20 link: "https://www.apple.com/macbook-pro/",21 },22 {23 id: "iphone-16-pro",24 name: "iPhone 16 Pro",25 brand: "Apple",26 category: "mobile",27 status: "active",28 specs: "Natural Titanium / 256GB",29 description:30 "Daily driver smartphone with outstanding cameras and a smooth 120Hz ProMotion display.",31 icon: "material-symbols:phone-iphone",32 featured: true,33 year: "2024",34 },35 {36 id: "sony-wh1000xm5",37 name: "Sony WH-1000XM5",38 brand: "Sony",39 category: "audio",40 status: "active",41 specs: "Silver / ANC / LDAC",42 description:43 "Industry-leading noise-canceling headphones for immersive coding sessions and travels.",44 icon: "material-symbols:headphones-rounded",45 year: "2023",46 },47 {48 id: "custom-keyboard-75",49 name: "Custom 75% Mechanical Keyboard",50 brand: "Custom",51 category: "peripheral",52 status: "active",53 specs: "Anodized Aluminum / Linear Switches",54 description:55 "Custom gasket-mounted keyboard tuned for deep, quiet typing acoustics.",56 icon: "material-symbols:keyboard-outline-rounded",57 year: "2025",58 },59 {60 id: "ipad-pro-11",61 name: 'iPad Pro 11"',62 brand: "Apple",63 category: "mobile",64 status: "backup",65 specs: "Space Gray / 128GB",66 description:67 "Secondary mobile screen and digital notepad for sketching ideas and reading papers.",68 icon: "material-symbols:tablet-mac-rounded",69 year: "2021",70 },71];72
73/** 获取所有设备数据列表 */74export function getDevicesList(): DeviceItem[] {75 return devicesData;76}1/**2 * 友情链接数据配置(结构与 Mizuki 同款,便于互相迁移)。3 * 用于管理友情链接页面的数据:src/pages/friends.astro → organisms/FriendSection。4 *5 * 添加友链:在 friendsData 中追加一项即可,页面 / 筛选标签自动生成。6 * tags 会聚合为页面顶部的筛选 chip(OR 命中:选中多个标签时命中任一即显示)。7 */8export interface FriendItem {9 id: number;10 title: string;11 imgurl: string;12 desc: string;13 siteurl: string;14 tags: string[];15}16
17// 友情链接数据18export const friendsData: FriendItem[] = [19 {20 id: 1,21 title: "Mizuki",22 imgurl: "https://avatars.githubusercontent.com/u/225602409?v=4&s=640",23 desc: "Another Fuwari-based blog theme with docs",24 siteurl: "https://mizuki.mysqil.com",25 tags: ["Blog", "Theme"],26 },27 {28 id: 2,29 title: "Astro",30 imgurl: "https://avatars.githubusercontent.com/u/44914786?v=4&s=640",31 desc: "The web framework for content-driven websites",32 siteurl: "https://astro.build",33 tags: ["Framework"],34 },35 {36 id: 3,37 title: "Material 3",38 imgurl: "https://avatars.githubusercontent.com/u/19478152?v=4&s=640",39 desc: "Material Design 3 — the next generation of Material Design",40 siteurl: "https://m3.material.io",41 tags: ["Design"],42 },43];44
45// 获取所有友情链接数据(稳定顺序,测试可复现)46export function getFriendsList(): FriendItem[] {47 return friendsData;48}49
50// 获取随机排序的友情链接数据(避免固定排序,按需使用)51export function getShuffledFriendsList(): FriendItem[] {52 const shuffled = [...friendsData];53 for (let i = shuffled.length - 1; i > 0; i--) {54 const j = Math.floor(Math.random() * (i + 1));55 [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];56 }57 return shuffled;58}1/**2 * 游戏展示页数据源(纯内容)。3 * 页面展示与筛选规则由 src/config/gamesConfig.ts 控制。4 *5 * 封面支持三种写法:6 * - src/assets 相对路径(如本文件所用,走 Astro 图片管线自动优化为 webp/avif);7 * - /public 绝对路径(如 "/assets/games/xxx.webp",原样输出);8 * - 远程 URL(https://…)。9 *10 * 注:以下为演示条目——评分 / 时长 / 状态是占位数值,请按自己的实际情况调整;11 * 封面取自各游戏官方商店页或官网主视觉。12 */13import type { GameItem } from "@/types/gamesConfig";14
15export const gamesData: GameItem[] = [16 {17 id: "nte-neverness-to-everness",18 name: "NTE: Neverness to Everness",19 developer: "Hotta Studio",20 category: "open-world",21 status: "playing",22 cover: "assets/games/yihuan-hero.jpg",23 icon: "material-symbols:explore-outline-rounded",24 rating: 4.5,25 hours: 86,26 platform: "PC",27 year: "2026",28 tags: ["Open World", "Urban", "Supernatural"],29 description:30 "A supernatural urban open-world RPG. As an anomaly-user who senses the “waves” of people and anomalies, you join E.T.D Squad Six and investigate the city's paranormal events.",31 link: "https://yh.wanmei.com/main.html",32 featured: true,33 },34 {35 id: "minecraft",36 name: "Minecraft",37 developer: "Mojang Studios",38 category: "sandbox",39 status: "playing",40 cover: "assets/games/minecraft-hero.jpg",41 icon: "material-symbols:widgets-rounded",42 rating: 5,43 hours: 420,44 platform: "PC",45 year: "2011",46 tags: ["Sandbox", "Survival", "Building"],47 description:48 "A blocky sandbox where you mine, craft and build across procedurally generated worlds. Survive the night, or just keep building — alone or with friends.",49 link: "https://www.minecraft.net/",50 },51];1import type { TrackDescriptor } from "@/types/musicConfig";2
3/**4 * 侧栏音乐本地曲目数据源。5 * 遵循「零额外负担」原则:配置与数据解耦,此处专用于管理本地曲目列表。6 *7 * 添加曲目:在 musicTracks 中追加一项即可:8 * - id: 唯一标识9 * - title: 曲目标题10 * - artist: 艺术家(可选)11 * - cover: 封面图地址(可选;推荐相对 /src,亦支持 /public 或绝对 URL)12 * - source: 音频文件地址(相对 /public 或绝对 URL)13 * - duration: 曲目时长(秒,可选)14 */15export const musicTracks: readonly TrackDescriptor[] = [16 {17 id: "dazbee",18 title: "口笛で愛は歌えない",19 artist: "Dazbee",20 cover: "assets/images/music/dazbee.webp",21 source: "/assets/music/url/dazbee.mp3",22 duration: 241,23 },24 {25 id: "hitori",26 title: "ひとり上手",27 artist: "Kaya",28 cover: "assets/images/music/hitori.webp",29 source: "/assets/music/url/hitori.mp3",30 duration: 253,31 },32 {33 id: "xryx",34 title: "眩耀夜行",35 artist: "スリーズブーケ",36 cover: "assets/images/music/xryx.webp",37 source: "/assets/music/url/xryx.mp3",38 duration: 245,39 },40 {41 id: "cl",42 title: "春雷の頃",43 artist: "22/7",44 cover: "assets/images/music/cl.webp",45 source: "/assets/music/url/cl.mp3",46 duration: 242,47 },48];1/**2 * 项目页数据源(纯内容)。3 * 页面展示与筛选规则由 src/config/projectsConfig.ts 控制。4 */5import type { ProjectItem } from "@/types/projectsConfig";6
7export const projectsData: ProjectItem[] = [8 {9 key: "shirone",10 title: "Shirone",11 summary:12 "An Astro blog theme shaped around an M3E component system, expressive content, and resilient client navigation.",13 category: "theme",14 phase: "building",15 technologies: ["Astro", "Svelte", "TypeScript", "Tailwind CSS"],16 icon: "material-symbols:deployed-code-outline-rounded",17 cover: "/assets/projects/shirone.webp",18 coverAlt: "Shirone theme homepage preview",19 featured: true,20 repository: "https://github.com/LyraVoid/Shirone",21 year: "2026",22 },23 {24 key: "folkpatch",25 title: "FolkPatch",26 summary: "A kernel-level root solution for Android, built on APatch.",27 category: "android",28 phase: "building",29 technologies: ["Kotlin", "APatch", "Android"],30 icon: "material-symbols:terminal-rounded",31 repository: "https://github.com/LyraVoid/FolkPatch",32 },33 {34 key: "kernelpatch",35 title: "KernelPatch",36 summary:37 "A kernel patch framework that powers APatch-style root on Android by loading code into the running kernel.",38 category: "android",39 phase: "shipped",40 technologies: ["C", "Linux Kernel", "Android"],41 icon: "material-symbols:extension-outline-rounded",42 repository: "https://github.com/lyravoid/KernelPatch",43 },44];45
46/** 获取所有项目数据列表 */47export function getProjectsList(): ProjectItem[] {48 return projectsData;49}1/**2 * 技能页数据源(纯内容)。3 * 页面展示与筛选规则由 src/config/skillsConfig.ts 控制。4 */5import type { SkillItem } from "@/types/skillsConfig";6
7export const skillsData: SkillItem[] = [8 {9 name: "JavaScript",10 description:11 "ES2020+ syntax, async plumbing, and event-driven browser code.",12 icon: "simple-icons:javascript",13 category: "frontend",14 level: "advanced",15 },16 {17 name: "TypeScript",18 description: "Typed application code and maintainable contracts.",19 icon: "simple-icons:typescript",20 category: "frontend",21 level: "expert",22 },23 {24 name: "Astro",25 description: "Content-focused sites with fast server-rendered output.",26 icon: "simple-icons:astro",27 category: "frontend",28 level: "advanced",29 },30 {31 name: "Svelte",32 description: "Focused interactive islands and component systems.",33 icon: "simple-icons:svelte",34 category: "frontend",35 level: "advanced",36 },37 {38 name: "React",39 description: "Composable component trees with hooks and client state.",40 icon: "simple-icons:react",41 category: "frontend",42 level: "intermediate",43 },44 {45 name: "Vue",46 description: "Progressive component authoring for rapid single-page apps.",47 icon: "simple-icons:vuedotjs",48 category: "frontend",49 level: "intermediate",50 },51 {52 name: "Tailwind CSS",53 description: "Utility-first styling for rapidly composed interfaces.",54 icon: "simple-icons:tailwindcss",55 category: "frontend",56 level: "advanced",57 },58 {59 name: "Sass",60 description: "Nesting, variables, and mixins for maintainable stylesheets.",61 icon: "simple-icons:sass",62 category: "frontend",63 level: "intermediate",64 },65 {66 name: "Node.js",67 description: "Build tooling, services, and content pipelines.",68 icon: "simple-icons:nodedotjs",69 category: "backend",70 level: "advanced",71 },72 {73 name: "Python",74 description: "Scripting, data wrangling, and service automation.",75 icon: "simple-icons:python",76 category: "backend",77 level: "intermediate",78 },79 {80 name: "Java",81 description: "Typed OO code for larger service and tooling layers.",82 icon: "simple-icons:openjdk",83 category: "backend",84 level: "intermediate",85 },86 {87 name: "Go",88 description: "Concurrent services and small high-performance tools.",89 icon: "simple-icons:go",90 category: "backend",91 level: "beginner",92 },93 {94 name: "Rust",95 description: "Memory-safe systems code and performance-critical paths.",96 icon: "simple-icons:rust",97 category: "backend",98 level: "beginner",99 },100 {101 name: "C++",102 description: "Native modules and performance-sensitive components.",103 icon: "simple-icons:cplusplus",104 category: "backend",105 level: "beginner",106 },107 {108 name: "C",109 description: "Low-level systems work close to the runtime.",110 icon: "simple-icons:c",111 category: "backend",112 level: "beginner",113 },114 {115 name: "Kotlin",116 description: "Concise JVM/Android code with modern null safety.",117 icon: "simple-icons:kotlin",118 category: "backend",119 level: "beginner",120 },121 {122 name: "Swift",123 description: "Native Apple-platform code and small CLIs.",124 icon: "simple-icons:swift",125 category: "backend",126 level: "beginner",127 },128 {129 name: "Ruby",130 description: "Readable scripting and quick automation.",131 icon: "simple-icons:ruby",132 category: "backend",133 level: "beginner",134 },135 {136 name: "PHP",137 description: "Server-rendered web code and content platforms.",138 icon: "simple-icons:php",139 category: "backend",140 level: "beginner",141 },142 {143 name: "PostgreSQL",144 description: "Relational data modeling and application queries.",145 icon: "simple-icons:postgresql",146 category: "backend",147 level: "intermediate",148 },149 {150 name: "Playwright",151 description: "User-facing regression and accessibility testing.",152 icon: "simple-icons:playwright",153 category: "tooling",154 level: "advanced",155 },156];157
158/** 获取所有技能数据列表 */159export function getSkillsList(): SkillItem[] {160 return skillsData;161}1/**2 * 时间线页数据源(纯内容)。3 * 页面展示与筛选规则由 src/config/timelineConfig.ts 控制。4 */5import type { TimelineItem } from "@/types/timelineConfig";6
7export const timelineData: TimelineItem[] = [8 {9 title: "Shirone Theme M3E Major Architecture Upgrade",10 date: "2026.08",11 category: "milestone",12 subtitle: "Open Source Project",13 description:14 "Refactored the entire blog theme into a Material 3 Expressive atomic component system with token-driven styling, complete keyboard navigation, and full accessibility compliance.",15 highlights: [16 "Implemented dynamic HCT palette calculation and state layer tokens",17 "Added multi-page capabilities: Timeline, Skills, Projects, and Protected Albums",18 "Zero-error strict type-checking and automated visual regression locks",19 ],20 tags: ["Astro", "Svelte 5", "M3E", "Tailwind 4"],21 links: [22 {23 label: "GitHub Repository",24 url: "https://github.com/LyraVoid/Shirone",25 icon: "fa6-brands:github",26 },27 ],28 icon: "material-symbols:rocket-launch-rounded",29 featured: true,30 },31 {32 title: "Senior Frontend Engineer",33 date: "2025.03 – Present",34 category: "career",35 subtitle: "Technology Lab",36 location: "Tokyo, Japan",37 description:38 "Leading frontend architecture, web performance optimization, and interactive design system development for modern web platforms.",39 highlights: [40 "Spearheaded design system unification across web products",41 "Reduced core bundle load times by 40% using modern SSR and asset pipelines",42 ],43 tags: ["TypeScript", "Architecture", "Performance", "Design System"],44 icon: "material-symbols:work-rounded",45 featured: true,46 },47 {48 title: "Full-Stack Web Application Launch",49 date: "2024.11",50 category: "project",51 subtitle: "Independent Creation",52 description:53 "Designed and built an end-to-end creative workflow application with real-time collaboration and cloud synchronization.",54 highlights: [55 "Designed intuitive fluid canvas interface with low-latency interaction",56 "Built serverless backend APIs with edge caching and relational persistence",57 ],58 tags: ["Svelte", "Node.js", "PostgreSQL", "Cloudflare"],59 icon: "material-symbols:deployed-code-outline-rounded",60 },61 {62 title: "Computer Science & Engineering Degree",63 date: "2020.09 – 2024.06",64 category: "education",65 subtitle: "University of Technology",66 location: "Hangzhou, China",67 description:68 "Focused on computer systems, software engineering, human-computer interaction, and distributed architectures.",69 highlights: [70 "Graduated with honors and outstanding graduate thesis award",71 "Led university open source student community and hackathons",72 ],73 tags: ["Computer Science", "Algorithms", "Software Engineering"],74 icon: "material-symbols:school-rounded",75 },76 {77 title: "Started Personal Blog & Tech Notes",78 date: "2022.04",79 category: "life",80 subtitle: "First Step into Tech Writing",81 description:82 "Published my first article online and began documenting frontend exploration, creative coding, and personal reflections.",83 tags: ["Blogging", "Writing", "Open Web"],84 icon: "material-symbols:edit-note-rounded",85 },86];87
88/** 获取所有时间线数据列表 */89export function getTimelineList(): TimelineItem[] {90 return timelineData;91}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 快照(`shirones/config/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: "shirones/config/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 : "shirones/config/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: "source-han-sans-sc",87 family: "Source Han Sans SC",88 role: "cjk",89 source: "local",90 variants: [91 {92 file: "src/assets/fonts/SourceHanSansSC-Regular.otf",93 weight: 400,94 style: "normal",95 },96 {97 file: "src/assets/fonts/SourceHanSansSC-Medium.otf",98 weight: 500,99 style: "normal",100 },101 {102 file: "src/assets/fonts/SourceHanSansSC-Bold.otf",103 weight: 700,104 style: "normal",105 },106 ],107 fallback: ["system-ui", "sans-serif"],108 display: "swap",109 preload: false,110},111
112 // ---------------------------------------------------------------------113 // 3. 代码等宽字体(渲染代码块与终端文本,对应 CSS 变量 --font-mono)114 // ---------------------------------------------------------------------115 {116 id: "jetbrains-mono",117 family: "JetBrains Mono",118 role: "mono",119 source: "fontsource",120 variants: [121 {122 file: "@fontsource-variable/jetbrains-mono/index.css",123 weight: "100 800",124 style: "normal",125 },126 {127 file: "@fontsource-variable/jetbrains-mono/wght-italic.css",128 weight: "100 800",129 style: "italic",130 },131 ],132 fallback: [133 "ui-monospace",134 "SFMono-Regular",135 "Menlo",136 "Monaco",137 "Consolas",138 "monospace",139 ],140 display: "swap",141 preload: false,142 },143 ],144
145 /**146 * 字体子集化配置(生产构建时自动从文章、i18n、配置及 Meting 歌曲中提取字符,生成极速精简版 .woff2)147 * - Dev 开发环境:自动加载完整原字体,任意输入新汉字实时可见,极速 HMR 零等待;148 * - Build 生产构建:自动执行子集裁剪,将几十兆大字体压缩为几百 KB 的专属子集,秒开加载。149 */150 subsetting: {151 enable: true, // 启用自动化子集裁剪152 includeContent: true, // 扫描 src/content/ 下所有文章153 includeI18n: true, // 扫描全部 10 种语言词典154 includeConfig: true, // 扫描站点配置与导航155 includeCommon: true, // 包含常用标点与基础字符156 allowRemoteText: true, // 允许抓取 Meting 云端歌单曲目文本参与字形提取157 },158
159 /**160 * 字体打包体积预算限制(子集化后通常仅 300KB ~ 1MB)161 */162 budget: {163 maxTotalBytes: 6 * 1024 * 1024, // 全站引用自定义字体总大小上限:6MB164 maxFamilyBytes: 4 * 1024 * 1024, // 单个字体族文件大小上限:4MB165 },166});167
168/** 经过校验与标准化处理后的字体配置对象,由 Astro 模板与 CSS 消费 */169export const resolvedFontOptions: ResolvedFontOptions = resolve(fontConfig);170
171/** 字体配置解析与校验函数 */172export const resolveFontOptions: (config: FontConfig) => ResolvedFontOptions =173 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}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: "mixed",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: "14164869977",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});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 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 { 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 */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 };第 3 篇,共 3 篇·本文属于系列
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 41 天
部分内容可能已过时