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}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 {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 /** 是否启用番剧页(仅控制页面渲染,不发起任何外部网络连接) */43 enable: true,44
45 /** 主数据源选择 */46 source: {47 kind: "local",48 // provider: "bangumi",49 // file: "bangumi.json",50 // fetchOnDev: true,51 },52
53 /** 异常降级策略(快照丢失或解析失败时回退本地数据) */54 fallback: {55 kind: "local",56 },57
58 /** 外部提供方配置 */59 providers: {60 bangumi: {61 enable: false,62 userId: "", // 填入你的 Bangumi 数字 UID 或公开用户名(测试可填 "sai")63 request: {64 pageSize: 50,65 maxItems: 300,66 minDelayMs: 200,67 },68 },69 bilibili: {70 enable: false,71 vmid: "", // 填入你的 B 站公开 UID72 sessdataEnv: "BILI_SESSDATA",73 cover: {74 mode: "local", // "local" 站内下载缓存(推荐)| "remote" 远程链接 | "none"75 useWebp: true,76 },77 request: {78 pageSize: 30,79 maxItems: 300,80 minDelayMs: 300,81 },82 },83 },84
85 /** 快照存储管理 */86 snapshot: {87 directory: "shirones/config/data/anime-snapshots",88 staleAfterDays: 30,89 keepLastValid: true,90 },91});92
93const SAFE_FILENAME_PATTERN = /^[a-zA-Z0-9_-]+\.json$/;94
95/**96 * 校验并解析 Anime 配置,返回只读的标准选项97 */98export function resolveAnimeOptions(config: AnimeConfig): ResolvedAnimeOptions {99 const enable = Boolean(config.enable);100 const fallback: AnimeFallbackKind =101 config.fallback?.kind === "empty" ? "empty" : "local";102
103 const directory =104 typeof config.snapshot?.directory === "string" &&105 config.snapshot.directory.trim() &&106 !config.snapshot.directory.includes("..")107 ? config.snapshot.directory.trim().replace(/[\\/]+$/, "")108 : "shirones/config/data/anime-snapshots";109
110 const staleAfterDays =111 typeof config.snapshot?.staleAfterDays === "number" &&112 Number.isFinite(config.snapshot.staleAfterDays) &&113 config.snapshot.staleAfterDays > 0114 ? Math.floor(config.snapshot.staleAfterDays)115 : 30;116
117 const keepLastValid = config.snapshot?.keepLastValid ?? true;118
119 const rawKind = config.source?.kind;120 let kind: AnimeSourceKind = "local";121 let provider: AnimeProvider | undefined;122 let file: string | undefined;123
124 if (rawKind === "snapshot") {125 const rawProvider = config.source?.provider;126 if (rawProvider === "bangumi" || rawProvider === "bilibili") {127 provider = rawProvider;128 }129
130 const rawFile = config.source?.file?.trim();131 if (132 rawFile &&133 SAFE_FILENAME_PATTERN.test(rawFile) &&134 // 若指定了 provider 但 file 误填了另一 provider 的 json,自动校正为对应 provider 的 json 文件135 !(provider === "bilibili" && rawFile === "bangumi.json") &&136 !(provider === "bangumi" && rawFile === "bilibili.json")137 ) {138 file = rawFile;139 } else if (provider) {140 file = `${provider}.json`;141 }142
143 const fetchOnDev = config.source?.fetchOnDev ?? true;144
145 if (file) {146 kind = "snapshot";147 }148
149 return Object.freeze({150 enable,151 source: Object.freeze({152 kind,153 ...(provider ? { provider } : {}),154 ...(file ? { file } : {}),155 fetchOnDev,156 }),157 fallback,158 snapshot: Object.freeze({159 directory,160 staleAfterDays,161 keepLastValid,162 }),163 });164 }165
166 return Object.freeze({167 enable,168 source: Object.freeze({169 kind,170 ...(provider ? { provider } : {}),171 ...(file ? { file } : {}),172 fetchOnDev: config.source?.fetchOnDev ?? true,173 }),174 fallback,175 snapshot: Object.freeze({176 directory,177 staleAfterDays,178 keepLastValid,179 }),180 });181}182
183export const resolvedAnimeOptions: ResolvedAnimeOptions =184 resolveAnimeOptions(animeConfig);1import type { AnnouncementConfig } from "@/types/announcementConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 公告栏配置6 * 组件显示由 sidebarConfig 统一控制7 */8export const announcementConfig: AnnouncementConfig = withUserConfig(9 "announcement",10 {11 title: "", // 公告标题,填空使用 i18n 字符串 Key.announcement12 content: "The only way to do great work is to love what you do", // 公告内容13 closable: true, // 允许用户关闭公告14 link: {15 enable: true, // 启用链接16 text: "GitHub", // 链接文本17 url: "https://github.com", // 链接 URL18 external: true, // 外部链接19 },20 },21);1import type { ArticleConfig } from "@/types/articleConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 文章详情页配置。6 */7export const articleConfig: ArticleConfig = withUserConfig("article", {8 lastUpdated: {9 // 关闭后不渲染最后更新提示。10 enable: true,11 // 按 UTC 日历日计算;达到该天数当天开始显示,0 表示立即显示。12 minimumAgeDays: 90,13 },14 discovery: {15 // 总开关关闭后不计算、不渲染文章尾部的延伸阅读区域。16 enable: true,17 related: {18 // 只展示至少共享一个标签或分类的文章。19 enable: true,20 count: 3,21 },22 random: {23 // 按当前文章标识稳定抽样;同一构建中的结果不会随刷新变化。24 enable: true,25 count: 2,26 },27 },28 share: {29 // 关闭后不渲染文章尾部的分享区块,不引入客户端水合。30 enable: true,31 // 生成海报时是否默认包含文章封面(封面不可用时自动降级为无封面排版)。32 includeCover: true,33 },34});35
36const MAX_DISCOVERY_COUNT = 6;37
38export interface ArticleDiscoveryOptions {39 relatedCount: number;40 randomCount: number;41}42
43export interface ArticleShareOptions {44 includeCover: boolean;45}46
47export function normalizeDiscoveryCount(value: number): number {48 return Number.isFinite(value)49 ? Math.min(MAX_DISCOVERY_COUNT, Math.max(0, Math.floor(value)))50 : 0;51}52
53export function resolveArticleDiscoveryOptions(54 config: Pick<ArticleConfig, "discovery">,55): ArticleDiscoveryOptions | null {56 if (!config.discovery.enable) return null;57
58 const relatedCount = config.discovery.related.enable59 ? normalizeDiscoveryCount(config.discovery.related.count)60 : 0;61 const randomCount = config.discovery.random.enable62 ? normalizeDiscoveryCount(config.discovery.random.count)63 : 0;64
65 return relatedCount > 0 || randomCount > 066 ? { relatedCount, randomCount }67 : null;68}69
70export function resolveArticleShareOptions(71 config: Pick<ArticleConfig, "share">,72): ArticleShareOptions | null {73 if (!config.share.enable) return null;74 return { includeCover: config.share.includeCover };75}76
77export function resolveLastUpdatedNoticeOptions(78 config: Pick<ArticleConfig, "lastUpdated">,79): ArticleConfig["lastUpdated"] | null {80 return config.lastUpdated.enable ? config.lastUpdated : null;81}1import type { CommentConfig, TwikooConfig } from "@/types/commentConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 评论系统配置单一真源。6 *7 * 遵循「零额外负担」原则:默认全局关闭(enable: false),8 * 在未开启时不产生任何外部网络请求、零额外 DOM 占位与零包体积膨胀。9 *10 * 【开启 Twikoo 评论配置步骤】11 * 1. 部署 Twikoo 服务端并获取环境 ID(腾讯云 CloudBase / Vercel / Railway / 私有部署等);12 * 2. 将 `enable` 置为 `true`,并将 `provider` 设置为 `"twikoo"`;13 * 3. 填入你的 `twikoo.envId`;14 * 4. (可选)自定义 `scriptUrl`(如使用自建 CDN 或官方 unpkg/jsdelivr 源)。15 */16export const commentConfig: CommentConfig = withUserConfig("comment", {17 /** 全局评论总开关:false 时完全不加载评论脚本与 DOM */18 enable: false,19 /** 评论提供商类型:"none" | "twikoo" */20 provider: "none",21 /** 是否开启视口懒加载:滚动进入视口才动态加载评论组件(推荐 true) */22 lazy: true,23 /** Twikoo 专有配置 */24 twikoo: {25 /** Twikoo 环境 ID(如 "https://your-twikoo.vercel.app" 或腾讯云环境 ID) */26 envId: "",27 /** Twikoo 前端 JS 脚本 CDN 地址 */28 scriptUrl: "https://cdn.jsdelivr.net/npm/twikoo@1.7.19/dist/twikoo.min.js",29 /** 评论语言:"auto"(跟随站点)| "zh-CN" | "zh-TW" | "en" | "ja" 等 */30 lang: "auto",31 /** 评论输入框占位提示文本 */32 placeholder: "Share your thoughts...",33 },34});35
36export type ResolvedCommentOptions = {37 provider: "twikoo";38 lazy: boolean;39 twikoo: TwikooConfig;40} | null;41
42/**43 * 解析并校验评论配置。未启用、提供商为 none 或关键参数缺失时返回 null。44 */45export function resolveCommentOptions(46 config: CommentConfig,47): ResolvedCommentOptions {48 if (!config.enable || config.provider === "none") {49 return null;50 }51 if (config.provider === "twikoo") {52 const envId = config.twikoo.envId?.trim();53 const scriptUrl = config.twikoo.scriptUrl?.trim();54 if (!envId || !scriptUrl) {55 return null;56 }57 return {58 provider: "twikoo",59 lazy: config.lazy,60 twikoo: {61 ...config.twikoo,62 envId,63 scriptUrl,64 },65 };66 }67 return null;68}1import type { ContextMenuConfig } from "@/types/contextMenuConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/** Optional desktop context-menu enhancement. */5export const contextMenuConfig: ContextMenuConfig = withUserConfig(6 "contextMenu",7 {8 enable: true,9 actions: ["copySelection", "backToTop", "sharePageLink"],10 },11);1import type { DevicesConfig } from "@/types/devicesConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 设备展示页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /devices/ 跳转 404;9 * - categories:场景分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledIds:可选被禁用的设备 ID 列表;11 *12 * 注:设备的具体清单数据(设备名、品牌、规格、感受说明、图片等)请在 `src/data/devices.ts` 中维护。13 */14export const devicesConfig: DevicesConfig = withUserConfig("devices", {15 enable: true,16 categories: [17 {18 key: "desk",19 label: "Desk Setup",20 icon: "material-symbols:desktop-windows-outline-rounded",21 description: "Workstation & home office hardware",22 },23 {24 key: "mobile",25 label: "Mobile & EDC",26 icon: "material-symbols:phone-iphone",27 description: "Daily portable devices & smart gadgets",28 },29 {30 key: "audio",31 label: "Audio & Visual",32 icon: "material-symbols:headphones-rounded",33 description: "Headphones, speakers & monitoring gears",34 },35 {36 key: "peripheral",37 label: "Peripherals",38 icon: "material-symbols:keyboard-outline-rounded",39 description: "Keyboards, mice & desk accessories",40 },41 ],42 // disabledIds: [],43});1import type { ExpressiveCodeConfig } from "@/types/config";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * Expressive Code 代码块主题(astro.config.mjs 与 setting-utils 消费)。6 * 类型见 src/types/config.ts。7 */8export const expressiveCodeConfig: ExpressiveCodeConfig = withUserConfig(9 "expressiveCode",10 {11 // Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.12 // 代码块跟随明暗模式切换深浅主题13 theme: "github-dark",14 lightTheme: "github-light",15 darkTheme: "github-dark",16 },17);1import type { FabConfig } from "@/types/fabConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 右下角悬浮控制流(FAB)导航配置。6 *7 * 【核心配置项】8 * - enable:是否开启悬浮操作栏;9 * - align:"start"(靠左)| "end"(靠右,默认);10 * - size:"small" | "regular"(默认)| "large";11 * - offset:右下角边距(支持 CSS 变量或具体像素);12 * - items:操作按钮清单(按数组顺序渲染):13 * - type: "top" —— 平滑返回顶部按钮(滚过横幅后自动浮现);14 * - type: "toc" —— 悬浮文章目录面板(桌面端已有侧栏粘性 TOC,默认仅在 mobile/tablet 显示);15 * - type: "comment" —— 直达评论区按钮(评论系统关闭或文章关闭评论时零 DOM 产物);16 * - type: "home" —— 返回首页按钮(onlySubPages: true 表示仅在非首页展示);17 * - devices:受控设备矩阵("mobile" | "tablet" | "desktop"),省略表示全设备生效;18 * - pages:页面范围过滤(如 ["post"])。19 *20 * 架构规范见 docs/fab-system.md。21 */22export const fabConfig: FabConfig = withUserConfig("fab", {23 enable: true,24 align: "end",25 size: "regular",26 offset: {27 bottom: "var(--m3e-space-8)",28 right: "var(--m3e-space-6)",29 },30 items: [31 {32 type: "top",33 enable: true,34 devices: ["mobile", "tablet", "desktop"],35 },36 {37 type: "toc",38 enable: true,39 devices: ["mobile", "tablet"],40 pages: ["post"],41 depth: 3,42 closeOnSelect: true,43 },44 {45 type: "comment",46 enable: true,47 devices: ["mobile", "tablet"],48 pages: ["post"],49 },50 {51 type: "home",52 enable: true,53 devices: ["mobile", "tablet"],54 onlySubPages: true,55 },56 ],57});1import type { FontConfig, ResolvedFontOptions } from "@/types/fontConfig.ts";2import { withUserConfig } from "@/utils/config-overlay.ts";3import { resolveFontOptions as resolve } from "@/utils/font-options.ts";4
5/**6 * ─────────────────────────────────────────────────────────────────────────────7 * Shirone 全站字体配置指南8 * ─────────────────────────────────────────────────────────────────────────────9 *10 * 博客的字体分为 3 种角色(Role),每个角色各司其职:11 * 1. `body`:西文与默认基础正文字体(英文字母、数字、基础标点)12 * 2. `cjk` :中日韩字体(汉字、日文平假名/片假名、韩文)13 * 3. `mono`:等宽代码字体(文章代码块、行内代码、终端输出)14 *15 * ─────────────────────────────────────────────────────────────────────────────16 * 【常见修改场景】17 * ─────────────────────────────────────────────────────────────────────────────18 * 场景 A:完全使用系统默认字体(零字体打包,极速加载,最省流量)19 * - 将 `mode` 设置为 `"system"`,并将 `fontFamilies` 设为空数组 `[]`。20 *21 * 场景 B:更换本地中文字体或英文字体(.woff2 文件)22 * 1. 准备你的 `.woff2` 字体文件,放入项目 `src/assets/fonts/` 目录下;23 * 2. 找到对应角色的配置(如 `role: "cjk"` 或 `role: "body"`);24 * 3. 设置 `source: "local"`,并在 `file` 中填入你的字体路径(例如 `"src/assets/fonts/MyFont.woff2"`);25 * 4. 将 `family` 设为该字体的真实族名称。26 *27 * 场景 C:使用 npm 的 Fontsource 字体包28 * 1. 安装字体包(如 `pnpm.cmd add @fontsource/inter`);29 * 2. 设置 `source: "fontsource"`,并在 `file` 中填入对应的 CSS 路径(如 `"@fontsource/inter/400.css"`);30 * 3. 将 `family` 设为对应的字体名称(如 `"Inter"`)。31 *32 * ─────────────────────────────────────────────────────────────────────────────33 * 【修改后的验证命令】34 * 在终端依次执行:35 * 1. `npx.cmd astro check` -> 校验配置与页面语法36 * 2. `pnpm.cmd build` -> 执行生产构建与字体打包37 * 3. `pnpm.cmd fonts:check` -> 校验字体格式与体积预算38 * ─────────────────────────────────────────────────────────────────────────────39 */40export const fontConfig: FontConfig = withUserConfig("font", {41 /**42 * 构建模式:43 * - `"custom"`: 启用自定义字体(加载下方 fontFamilies 中配置的字体)44 * - `"system"`: 纯系统字体模式(不打包任何自定义字体文件,完全依赖访客设备)45 */46 mode: "custom",47
48 /**49 * 字体清单列表(按需配置 body、cjk、mono 角色)50 */51 fontFamilies: [52 // ---------------------------------------------------------------------53 // 1. 正文字体(现代几何圆润西文字体 Outfit,与 M3E 大圆角及悠哉圆体绝配)54 // ---------------------------------------------------------------------55 {56 id: "outfit-body",57 family: "Outfit",58 role: "body",59 source: "fontsource",60 variants: [61 {62 file: "@fontsource/outfit/400.css",63 weight: 400,64 style: "normal",65 },66 {67 file: "@fontsource/outfit/500.css",68 weight: 500,69 style: "normal",70 },71 {72 file: "@fontsource/outfit/700.css",73 weight: 700,74 style: "normal",75 },76 ],77 fallback: ["ui-sans-serif", "system-ui", "sans-serif"],78 display: "swap",79 preload: false,80 },81
82 // ---------------------------------------------------------------------83 // 2. 中文 / 日文 CJK 字体(悠哉圆体 Yozai Medium,全量简繁中日韩 100% 覆盖)84 // ---------------------------------------------------------------------85 {86 id: "yozai-cjk",87 family: "Yozai Medium",88 role: "cjk",89 source: "local",90 variants: [91 {92 file: "src/assets/fonts/Yozai-Medium.ttf",93 weight: 500,94 style: "normal",95 },96 ],97 fallback: ["system-ui", "sans-serif"],98 display: "swap",99 preload: false,100 },101
102 // ---------------------------------------------------------------------103 // 3. 代码等宽字体(渲染代码块与终端文本,对应 CSS 变量 --font-mono)104 // ---------------------------------------------------------------------105 {106 id: "jetbrains-mono",107 family: "JetBrains Mono",108 role: "mono",109 source: "fontsource",110 variants: [111 {112 file: "@fontsource-variable/jetbrains-mono/index.css",113 weight: "100 800",114 style: "normal",115 },116 {117 file: "@fontsource-variable/jetbrains-mono/wght-italic.css",118 weight: "100 800",119 style: "italic",120 },121 ],122 fallback: [123 "ui-monospace",124 "SFMono-Regular",125 "Menlo",126 "Monaco",127 "Consolas",128 "monospace",129 ],130 display: "swap",131 preload: false,132 },133 ],134
135 /**136 * 字体子集化配置(生产构建时自动从文章、i18n、配置及 Meting 歌曲中提取字符,生成极速精简版 .woff2)137 * - Dev 开发环境:自动加载完整原字体,任意输入新汉字实时可见,极速 HMR 零等待;138 * - Build 生产构建:自动执行子集裁剪,将几十兆大字体压缩为几百 KB 的专属子集,秒开加载。139 */140 subsetting: {141 enable: true, // 启用自动化子集裁剪142 includeContent: true, // 扫描 src/content/ 下所有文章143 includeI18n: true, // 扫描全部 10 种语言词典144 includeConfig: true, // 扫描站点配置与导航145 includeCommon: true, // 包含常用标点与基础字符146 allowRemoteText: true, // 允许抓取 Meting 云端歌单曲目文本参与字形提取147 },148
149 /**150 * 字体打包体积预算限制(子集化后通常仅 300KB ~ 1MB)151 */152 budget: {153 maxTotalBytes: 6 * 1024 * 1024, // 全站引用自定义字体总大小上限:6MB154 maxFamilyBytes: 4 * 1024 * 1024, // 单个字体族文件大小上限:4MB155 },156});157
158/** 经过校验与标准化处理后的字体配置对象,由 Astro 模板与 CSS 消费 */159export const resolvedFontOptions: ResolvedFontOptions = resolve(fontConfig);160
161/** 字体配置解析与校验函数 */162export const resolveFontOptions: (config: FontConfig) => ResolvedFontOptions =163 resolve;1<!--2 在这里添加自定义的页脚 HTML 内容(例如 ICP 备案号、公安备案图标与链接、自定义声明等)。3 需要在 src/config/footerConfig.ts 中保持 enable: true;4 内容将注入在页脚版权信息上方;若文件留空或仅保留注释,则不会产生任何额外的 DOM 结构。5-->1import type { FooterConfig } from "@/types/footerConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 页脚自定义 HTML 注入配置。6 * 开启后将读取 src/config/FooterConfig.html 文件内容并注入到页脚版权信息上方。7 * 关闭时(enable: false)零额外 DOM 占位、零文件读取开销。8 */9export const footerConfig: FooterConfig = withUserConfig("footer", {10 enable: false,11});1/**2 * Tonal Bloom(色调辉光占位)默认配置。3 * 与 M3E HCT 色彩系统同源,为全站图片提供防抖动尺寸占位与色彩过渡体验。4 */5import type { ImageBloomConfig } from "@/types/imageBloomConfig";6import { withUserConfig } from "@/utils/config-overlay.ts";7
8export const imageBloomConfig: ImageBloomConfig = withUserConfig("imageBloom", {9 enable: true,10 blurRadius: 20,11 opacity: 0.7,12 transitionDuration: 300,13});14
15export function resolveImageBloomOptions(16 config: Partial<ImageBloomConfig> = imageBloomConfig,17): ImageBloomConfig {18 return {19 enable: config.enable ?? true,20 blurRadius: config.blurRadius ?? 20,21 opacity: config.opacity ?? 0.7,22 transitionDuration: config.transitionDuration ?? 300,23 };24}1import type { LicenseConfig } from "@/types/config";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 文章版权声明(文章页 License 区块消费)。类型见 src/types/config.ts。6 */7export const licenseConfig: LicenseConfig = withUserConfig("license", {8 enable: true,9 name: "CC BY-NC-SA 4.0",10 url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",11});1import type { LlmsConfig } from "@/types/llmsConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * ─────────────────────────────────────────────────────────────────────────────6 * Shirone LLMs.txt 与 AI 友好内容系统配置指南7 * ─────────────────────────────────────────────────────────────────────────────8 *9 * 遵循「零额外负担」原则与 https://llmstxt.org/ 官方规范:10 * - 为大语言模型 (ChatGPT, Claude, Perplexity, Cursor 等) 提供结构化 Markdown 索引;11 * - 纯服务端静态生成 `/llms.txt`(精简索引)与 `/llms-full.txt`(全量正文汇编);12 * - 客户端 JS 主包增加 0 KB,前台读者浏览速度 0 影响;13 * - 安全隔离:自动过滤密码保护文章 (encrypted: true) 与草稿 (draft: true),绝不泄漏私密内容。14 *15 * ─────────────────────────────────────────────────────────────────────────────16 * 【自动化机制说明(平时写作无需维护本文件)】17 * ─────────────────────────────────────────────────────────────────────────────18 * 1. 公开博客文章(Articles):19 * - 构建期系统自动调用 `getSortedPosts()` 扫描全站 Markdown 文件;20 * - 自动提取每篇文章的标题、链接、简介与标签,无需手动登记!21 * 2. 站点基本信息:22 * - 站点标题、副标题与简介默认自动继承 `siteConfig` 与 `profileConfig`;23 * 3. 正文脱敏与清洗:24 * - `/llms-full.txt` 自动展开 `<llm-only>` AI 专属提示,自动剔除 `<llm-exclude>` 内容。25 *26 * ─────────────────────────────────────────────────────────────────────────────27 * 【常用配置场景】28 * ─────────────────────────────────────────────────────────────────────────────29 * 场景 A:使用默认配置(开箱即用,最推荐)30 * - 保持下方默认配置即可,全站文章自动收录并生成 `/llms.txt` 与 `/llms-full.txt`。31 *32 * 场景 B:完全关闭 AI 检索端点33 * - 将 `enable` 设置为 `false`(访问对应链接返回 404,不生成任何静态文件)。34 *35 * 场景 C:文章量极大时仅生成精简目录,不生成超长全文 dump36 * - 将 `generateFull` 设置为 `false`(只生成 `/llms.txt`,跳过 `/llms-full.txt`)。37 *38 * 场景 D:防止某些私密标签被大模型检索39 * - 在 `excludeTags` 中追加标签名,例如:`excludeTags: ["secret", "private", "diary"]`。40 *41 * 场景 E:内容仓(external 模式)覆盖42 * - 在内容仓 `config/llms.yaml` 里只写想改的键即可(如 `siteSummary`、`excludeTags`);43 * - 合并规则为「对象递归合并,数组整体替换」,因此改 `corePages` / `customSections`44 * 需要把整个清单写全。契约见 `docs/content-separation/config-overlay.md`。45 * ─────────────────────────────────────────────────────────────────────────────46 */47export const llmsConfig: LlmsConfig = withUserConfig("llms", {48 /**49 * 是否启用 /llms.txt 与 /llms-full.txt 静态端点生成50 * - true (默认): 构建期自动在 dist/ 输出纯文本 Markdown 文件;51 * - false: 彻底禁用此功能,访问返回 404,不产生任何构建文件。52 */53 enable: true,54
55 /**56 * 是否同时生成包含全站公开文章完整正文的 /llms-full.txt 文件57 * - true (默认): 将所有公开非加密文章的正文清洗后合并为一个文件,方便 AI 全量学习与 RAG 导入;58 * - false: 仅生成目录索引 /llms.txt,不生成全量正文。59 */60 generateFull: true,61
62 /**63 * 站点在大模型眼中的自我介绍(可选)64 * - 省略或留空时:自动回退使用 `siteConfig.subtitle` 或 `profileConfig.bio`;65 * - 填写字符串时:优先使用此处的自定义英文/中文介绍覆盖默认值。66 */67 siteSummary: "",68
69 /**70 * 单篇文章在 /llms.txt 目录索引中的摘要截断字数上限(默认 200 字)71 * - 超出长度时会自动在句尾添加省略号 "…";72 * - 不影响 /llms-full.txt 中的完整正文输出。73 */74 descriptionMaxLength: 200,75
76 /**77 * 敏感标签黑名单过滤(可选)78 * - 凡是包含此列表中任意标签的文章,将同时从 /llms.txt 与 /llms-full.txt 中剔除;79 * - 即使文章本身为公开状态(非加密),只要命中黑名单标签也绝不暴露给 AI 模型。80 */81 excludeTags: ["secret", "private"],82
83 /**84 * 敏感分类黑名单过滤(可选)85 * - 凡是属于此分类的文章,将彻底从 LLM 产物中排除。86 */87 excludeCategories: [],88
89 /**90 * 核心引导页面清单(Core Pages)91 * - 向大模型重点介绍站点的核心栏目与功能入口;92 * - 可填写站内相对路径(如 "/about/")或外部完整 URL;93 * - 省略或设为空数组 [] 时,系统会自动使用默认核心页面。94 */95 corePages: [96 {97 title: "Home",98 url: "/",99 description: "Main blog entrance and latest post stream.",100 },101 {102 title: "About",103 url: "/about/",104 description: "Author profile, technical stack, and background.",105 },106 {107 title: "Archive",108 url: "/archive/",109 description: "Chronological index of all published writings.",110 },111 ],112
113 /**114 * 自定义扩展章节(可选)115 * - 用于向 AI Agent 额外推荐外部开源项目、API 文档或衍生资源;116 * - 默认为空数组 [],不输出额外章节。117 *118 * 示例:119 * ```ts120 * customSections: [121 * {122 * title: "Open Source Projects",123 * description: "Featured open source repositories maintained by the author.",124 * items: [125 * { title: "Shirone Theme", url: "https://github.com/LyraVoid/Shirone", description: "M3E blog theme for Astro." },126 * ],127 * },128 * ]129 * ```130 */131 customSections: [],132});1import { musicTracks } from "./data/music.ts";2import type {3 MetingMusicConfig,4 MusicConfig,5 MusicProvider,6 PlaybackMode,7 TrackDescriptor,8} from "@/types/musicConfig.ts";9import { withUserConfig } from "@/utils/config-overlay.ts";10
11/**12 * 侧栏音乐配置单一真源。13 * 遵循「零额外负担」原则:禁用时不产生任何网络请求与额外 DOM。14 *15 * ─────────────────────────────────────────────────────────────────────────────16 * 【四种工作模式(Provider)使用指南】17 * ─────────────────────────────────────────────────────────────────────────────18 * 1. "local"(本地独立模式,默认):19 * - 数据源:src/data/music.ts20 * - 特点:零外部 API 依赖,首屏毫秒级就绪,静态打包直出,断网也能正常播放。21 * - 示例:22 * provider: "local"23 *24 * 2. "custom"(自定义列表模式):25 * - 数据源:直接在 tracks 字段显式传入曲目数组(支持外链音频与封面)26 * - 特点:灵活自定义,无需修改通用数据文件。27 * - 示例:28 * provider: "custom",29 * tracks: [30 * { id: "song-1", title: "Song", artist: "Artist", source: "https://.../a.mp3", cover: "https://.../c.jpg" }31 * ]32 *33 * 3. "meting"(云端歌单模式):34 * - 数据源:Meting API 远端歌单(网易云 / QQ音乐 / 酷狗等)35 * - 特点:客户端异步按需拉取,海量曲库与封面自动解析。36 * - 示例:37 * provider: "meting",38 * meting: { server: "netease", type: "playlist", id: "14164869977" }39 *40 * 4. "mixed"(混合增强模式,推荐):41 * - 数据源:本地曲目(src/data/music.ts)+ Meting API 远端歌单自动合并42 * - 特点:首屏立即可播本地音乐,后台无感拉取远端歌单并在就绪后无缝扩容;43 * 若遇断网或云端接口故障,自动静默降级为本地曲目播放,绝不报红破版。44 * - 示例:45 * provider: "mixed",46 * meting: { server: "netease", type: "playlist", id: "14164869977" }47 * ─────────────────────────────────────────────────────────────────────────────48 */49export const musicConfig: MusicConfig = withUserConfig("music", {50 enable: true,51 provider: "mixed",52 // tracks: [53 // {54 // id: "custom-1",55 // title: "示例曲目",56 // artist: "艺术家",57 // cover: "/assets/music/cover/example.webp",58 // source: "/assets/music/url/example.mp3",59 // duration: 240,60 // },61 // ],62 meting: {63 server: "netease",64 type: "playlist",65 id: "14164869977",66 },67 defaultVolume: 0.7,68 defaultMode: "sequence",69});70
71export interface ResolvedMusicOptions {72 readonly provider: MusicProvider;73 readonly playlist: readonly TrackDescriptor[];74 readonly meting?: MetingMusicConfig;75 readonly defaultVolume: number;76 readonly defaultMode: PlaybackMode;77}78
79const ABSOLUTE_MEDIA_SOURCE = /^(?:https?:)?\/\//i;80const UNSAFE_SCHEME = /^[a-z][a-z\d+.-]*:/i;81
82function normalizeMediaSource(value: string): string | null {83 const source = value.trim();84 if (!source) return null;85 if (ABSOLUTE_MEDIA_SOURCE.test(source) || source.startsWith("/")) {86 return source;87 }88 if (UNSAFE_SCHEME.test(source)) return null;89 return `/${source.replace(/^\.\//, "")}`;90}91
92function normalizeTrack(93 track: TrackDescriptor,94 usedIds: Set<string>,95): TrackDescriptor | null {96 const id = track.id.trim();97 const title = track.title.trim();98 const source = normalizeMediaSource(track.source);99 if (!id || !title || !source || usedIds.has(id)) return null;100
101 usedIds.add(id);102 const artist = track.artist?.trim() || undefined;103 const cover = track.cover104 ? (normalizeMediaSource(track.cover) ?? undefined)105 : undefined;106 const duration =107 typeof track.duration === "number" &&108 Number.isFinite(track.duration) &&109 track.duration > 0110 ? track.duration111 : undefined;112
113 return Object.freeze({ id, title, source, artist, cover, duration });114}115
116export function clampMusicVolume(value: number, fallback = 0.7): number {117 if (!Number.isFinite(value)) return fallback;118 return Math.min(1, Math.max(0, value));119}120
121export function resolveMusicOptions(122 config: MusicConfig,123): ResolvedMusicOptions | null {124 if (!config.enable) return null;125
126 const provider: MusicProvider = config.provider ?? "local";127
128 if (provider === "meting") {129 const id = config.meting?.id?.trim();130 if (!id) return null;131 return Object.freeze({132 provider: "meting",133 playlist: Object.freeze([]),134 meting: config.meting,135 defaultVolume: clampMusicVolume(config.defaultVolume),136 defaultMode: config.defaultMode,137 });138 }139
140 let rawTracks: readonly TrackDescriptor[] = [];141 if (provider === "local" || provider === "mixed") {142 rawTracks = config.tracks ?? musicTracks;143 } else if (provider === "custom") {144 rawTracks = config.tracks ?? [];145 }146
147 const usedIds = new Set<string>();148 const playlist = rawTracks149 .map((track) => normalizeTrack(track, usedIds))150 .filter((track): track is TrackDescriptor => track !== null);151
152 if (provider === "mixed") {153 const metingId = config.meting?.id?.trim();154 if (playlist.length === 0 && !metingId) return null;155 return Object.freeze({156 provider: "mixed",157 playlist: Object.freeze(playlist),158 meting: config.meting,159 defaultVolume: clampMusicVolume(config.defaultVolume),160 defaultMode: config.defaultMode,161 });162 }163
164 if (playlist.length === 0) return null;165
166 return Object.freeze({167 provider,168 playlist: Object.freeze(playlist),169 defaultVolume: clampMusicVolume(config.defaultVolume),170 defaultMode: config.defaultMode,171 });172}1import I18nKey from "@i18n/i18nKey";2import { i18n } from "@i18n/translation";3import { devicesConfig } from "@/config/devicesConfig";4import { projectsConfig } from "@/config/projectsConfig";5import { skillsConfig } from "@/config/skillsConfig";6import { timelineConfig } from "@/config/timelineConfig";7import type {8 NavBarConfig,9 NavBarConfigOverride,10 NavBarLink,11 NavBarLinkOverride,12} from "@/types/navBarConfig";13import { getUserConfig } from "@/utils/config-overlay.ts";14
15/**16 * 导航栏配置(统一单一来源)。17 * - LinkPresets:命名链接预设表 —— 名称 / 地址 / 图标单点维护,可整体复用;18 * - navBarConfig:导航结构 —— 顺序 + 分组(children 子菜单),19 * 同时驱动顶栏下拉菜单与全端导航抽屉。20 * 新增入口:先在 LinkPresets 登记预设,再在 navBarConfig.links 按序引用。21 *22 * 内容仓可用 `config/nav-bar.yaml` 整体替换 `links`,写法见 `NavBarLinkOverride`。23 */24export const LinkPresets: Record<string, NavBarLink> = {25 Home: {26 name: i18n(I18nKey.home),27 url: "/",28 icon: "material-symbols:home-outline-rounded",29 pageKey: "home",30 },31 Archive: {32 name: i18n(I18nKey.archive),33 url: "/archive/",34 icon: "material-symbols:archive-outline-rounded",35 pageKey: "archive",36 },37 Friends: {38 name: i18n(I18nKey.friends),39 url: "/friends/",40 icon: "material-symbols:handshake-outline-rounded",41 pageKey: "friends",42 },43 Moments: {44 name: i18n(I18nKey.moments),45 url: "/moments/",46 icon: "material-symbols:auto-awesome-outline-rounded",47 pageKey: "moments",48 },49 Anime: {50 name: i18n(I18nKey.anime),51 url: "/anime/",52 icon: "material-symbols:live-tv-outline-rounded",53 pageKey: "anime",54 },55 Compass: {56 name: i18n(I18nKey.compass),57 url: "/compass/",58 icon: "material-symbols:explore-rounded",59 pageKey: "compass",60 },61 Skills: {62 name: i18n(I18nKey.skills),63 url: "/skills/",64 icon: "material-symbols:workspaces-outline-rounded",65 pageKey: "skills",66 },67 Projects: {68 name: i18n(I18nKey.projects),69 url: "/projects/",70 icon: "material-symbols:deployed-code-outline-rounded",71 pageKey: "projects",72 },73 Devices: {74 name: i18n(I18nKey.devices),75 url: "/devices/",76 icon: "material-symbols:devices-rounded",77 pageKey: "devices",78 },79 Timeline: {80 name: i18n(I18nKey.timeline),81 url: "/timeline/",82 icon: "material-symbols:timeline-rounded",83 pageKey: "timeline",84 },85 Albums: {86 name: i18n(I18nKey.albums),87 url: "/albums/",88 icon: "material-symbols:photo-library-outline-rounded",89 pageKey: "albums",90 },91 Categories: {92 name: i18n(I18nKey.categories),93 url: "/categories/",94 icon: "material-symbols:folder-outline-rounded",95 pageKey: "categories",96 },97 Tags: {98 name: i18n(I18nKey.tags),99 url: "/tags/",100 icon: "material-symbols:tag-rounded",101 pageKey: "tags",102 },103 About: {104 name: i18n(I18nKey.about),105 url: "/about/",106 icon: "material-symbols:info-outline-rounded",107 pageKey: "about",108 },109 GitHub: {110 name: "GitHub",111 url: "https://github.com/LyraVoid/Shirone",112 icon: "fa6-brands:github",113 external: true,114 pageKey: "github",115 },116};117
118const defaultNavBarConfig: NavBarConfig = {119 links: [120 LinkPresets.Home,121 LinkPresets.Archive,122 LinkPresets.Friends,123 LinkPresets.Moments,124 LinkPresets.Anime,125 LinkPresets.Compass,126 LinkPresets.Albums,127 {128 name: i18n(I18nKey.more),129 icon: "material-symbols:apps-rounded",130 children: [131 ...(timelineConfig.enable ? [LinkPresets.Timeline] : []),132 ...(projectsConfig.enable ? [LinkPresets.Projects] : []),133 ...(devicesConfig.enable ? [LinkPresets.Devices] : []),134 ...(skillsConfig.enable ? [LinkPresets.Skills] : []),135 // 分类/标签入口不进导航菜单(避免菜单项过多),预设已登记指向独立页面,136 // 需要时取消注释即可137 // LinkPresets.Categories,138 // LinkPresets.Tags,139 LinkPresets.About,140 LinkPresets.GitHub,141 ],142 },143 ],144};145
146/** `$t:home` 形式的 i18n 引用前缀;不带前缀的 name 一律按字面量处理。 */147const I18N_REFERENCE_PREFIX = "$t:";148
149function fail(message: string): never {150 throw new Error(`[config] nav-bar:${message}`);151}152
153function resolveName(name: string): string {154 if (!name.startsWith(I18N_REFERENCE_PREFIX)) return name;155
156 const key = name.slice(I18N_REFERENCE_PREFIX.length);157 if (!Object.hasOwn(I18nKey, key)) {158 fail(159 `未知的 i18n 词条 "${key}"。可用词条见 src/i18n/i18nKey.ts;` +160 " 若本意是普通文本,去掉开头的 $t: 即可。",161 );162 }163 return i18n(I18nKey[key as keyof typeof I18nKey]);164}165
166/**167 * 把内容仓的声明式导航条目还原成 `NavBarLink`。168 *169 * 预设名与 i18n 词条只有在这里才能校验(`LinkPresets` 与 `I18nKey` 都住在代码仓,170 * 生成期的 Node 脚本受路径别名所限读不到),因此错误在构建加载配置时抛出。171 */172export function resolveNavBarLinks(173 entries: readonly NavBarLinkOverride[],174 presets: Record<string, NavBarLink> = LinkPresets,175): NavBarLink[] {176 return entries.map((entry) => {177 let base: NavBarLink | null = null;178 if (entry.preset !== undefined) {179 base = presets[entry.preset] ?? null;180 if (!base) {181 fail(182 `未知的预设 "${entry.preset}"。可用预设:${Object.keys(presets).join("、")}。`,183 );184 }185 }186
187 const name =188 entry.name !== undefined ? resolveName(entry.name) : base?.name;189 if (name === undefined) {190 fail("每个条目都需要 name,或用 preset 引用一个内置预设。");191 }192
193 // 未声明 children 时沿用预设自带的子菜单(已由 ...base 带入)。194 return {195 ...base,196 name,197 ...(entry.url !== undefined ? { url: entry.url } : {}),198 ...(entry.icon !== undefined ? { icon: entry.icon } : {}),199 ...(entry.pageKey !== undefined ? { pageKey: entry.pageKey } : {}),200 ...(entry.external !== undefined ? { external: entry.external } : {}),201 ...(entry.children202 ? { children: resolveNavBarLinks(entry.children, presets) }203 : {}),204 };205 });206}207
208const userNavBar = getUserConfig("navBar") as NavBarConfigOverride | undefined;209
210export const navBarConfig: NavBarConfig = userNavBar211 ? { links: resolveNavBarLinks(userNavBar.links) }212 : defaultNavBarConfig;1import type { PermalinkConfig } from "@/types/permalinkConfig.ts";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * Permalink 固定链接配置6 * 控制文章 URL 路由与生成模板7 */8export const permalinkConfig: PermalinkConfig = withUserConfig("permalink", {9 /** 是否启用全局 permalink 功能,关闭时使用默认的文件名作为链接 (/posts/<slug>/) */10 enable: false,11 /**12 * permalink 格式模板13 * 支持的占位符:14 * - %year% : 4 位年份 (如 2024)15 * - %monthnum% : 2 位月份 (01-12)16 * - %day% : 2 位日期 (01-31)17 * - %hour% : 2 位小时 (00-23)18 * - %minute% : 2 位分钟 (00-59)19 * - %second% : 2 位秒数 (00-59)20 * - %post_id% : 文章序号(按发布时间升序排列,最早的文章为 1)21 * - %postname% : 文章文件名(slug,通常为全小写)22 * - %raw_postname% : 文章原始文件名(保留大小写)23 * - %category% : 分类名(无分类时为 "uncategorized")24 *25 * 示例:26 * - "%year%-%monthnum%-%postname%" => "/2024-12-my-post/"27 * - "%post_id%-%postname%" => "/42-my-post/"28 * - "%category%-%postname%" => "/tech-my-post/"29 * - "%year%/%monthnum%/%day%/%postname%" => "/2024/12/01/my-post/"30 *31 * 注意:支持使用斜杠 "/" 构建嵌套路径。32 */33 format: "%postname%",34});1import type { PostCardWidth, PostListConfig } from "@/types/postListConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 文章列表页配置:分页大小与排版布局。6 *7 * 【核心配置项】8 * - pageSize:每页展示的文章数量(默认 8 篇);9 * - layout:10 * - mode:"list"(经典纵向列表)| "grid"(双列/三列卡片网格);11 * - cover:"left"(封面在左)| "right"(封面在右,默认);12 * - cardWidth(仅在 grid 模式下生效):13 * - "compact":紧凑卡片(最小宽 20rem,适合高密度展示);14 * - "regular":标准卡片(最小宽 24rem,默认推荐);15 * - "relaxed":宽松大卡(最小宽 28rem,突出大图)。16 *17 * 注意:访客可在前端显示设置面板中动态切换 list/grid,此处为站点初始默认值。18 * GridUI 仅在主内容容器至少能容纳两张所选宽度的卡片时生效;侧栏等因素压窄19 * 内容后会暂时回退 ListUI,但保留 grid 偏好,空间恢复后自动切回。20 */21export const postListConfig: PostListConfig = withUserConfig("postList", {22 pageSize: 8,23 layout: {24 mode: "list",25 cover: "right",26 cardWidth: "regular",27 },28});29
30/** grid 档位 → 卡片最小宽度(--post-card-min 预设,与 shape/type 分档哲学同构)。31 页面框架 90rem:regular 24rem 保证宽屏为 2 列大卡(3 列窄卡会让32 日期/分类/字数元信息行换行),compact 才给密排选项。 */33export const POST_CARD_MIN_WIDTH: Record<PostCardWidth, string> = {34 compact: "20rem",35 regular: "24rem",36 relaxed: "28rem",37};1import type { ProfileConfig } from "@/types/config";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 博主资料:头像 / 名称 / 简介 / 社交链接(侧栏 Profile 卡片、页脚、RSS 作者等消费)。6 * 类型见 src/types/config.ts。7 */8export const profileConfig: ProfileConfig = withUserConfig("profile", {9 avatar: "assets/images/demo-avatar.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'10 name: "Shirone",11 bio: "The rain remembers what the sky forgot to say.",12 links: [13 {14 name: "Twitter",15 icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes16 // You will need to install the corresponding icon set if it's not already included17 // `pnpm add @iconify-json/<icon-set-name>`18 url: "https://twitter.com",19 },20 {21 name: "Steam",22 icon: "fa6-brands:steam",23 url: "https://store.steampowered.com",24 },25 {26 name: "GitHub",27 icon: "fa6-brands:github",28 url: "https://github.com/LyraVoid/Shirone",29 },30 ],31});1import type { ProjectsConfig } from "@/types/projectsConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 项目页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /projects/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledKeys:可选被禁用的项目 key 列表(例如 ["folkpatch"]);11 *12 * 注:项目的具体内容数据(标题、描述、技术栈、链接、封面等)请在 `src/data/projects.ts` 中维护。13 */14export const projectsConfig: ProjectsConfig = withUserConfig("projects", {15 enable: true,16 categories: [17 {18 key: "theme",19 label: "Theme",20 icon: "material-symbols:palette-outline-rounded",21 },22 {23 key: "android",24 label: "Android",25 icon: "material-symbols:android-rounded",26 },27 ],28 // disabledKeys: [],29});1/**2 * 侧边栏布局配置(数据驱动编排)。3 *4 * 【核心概念】5 * 1. arrangement(侧栏编排模式):6 * - "single"(单栏,默认):所有 widget 放入主侧栏,适合紧凑布局(页框 85rem);7 * - "dual"(双栏):column: "secondary" 的 widget 放入副侧栏(视口 ≥ 1280px 展开三列,页框 96rem),8 * 在 1024px~1279px 之间会自动优雅退化为单栏,无需手动适配。9 * 2. side(主栏物理位置):10 * - "left":主侧栏在左侧(默认),dual 模式下副栏自动落右侧;11 * - "right":主侧栏在右侧,dual 模式下副栏落左侧。12 * 3. widget 属性:13 * - type:组件类型("profile" | "music" | "announcement" | "categories" | "tags" | "stats" | "calendar" | "toc");14 * - enable:是否启用该 widget;15 * - slot:"top"(固定在顶部)| "sticky"(页面滚动时吸顶跟随);16 * - column:"primary"(主栏,默认)| "secondary"(副栏,仅在 arrangement: "dual" 时生效);17 * - pages:仅在指定页面展示(如 ["home", "post"],省略时默认全页面展示);18 * - collapseAfter:折叠阈值(适用于 categories/tags,超出条数显示展开按钮)。19 *20 * 类型定义见 src/types/sidebarConfig.ts。21 */22import type { SidebarConfig } from "@/types/sidebarConfig";23import { withUserConfig } from "@/utils/config-overlay.ts";24
25export const sidebarConfig: SidebarConfig = withUserConfig("sidebar", {26 enable: true,27 arrangement: "dual",28 side: "left",29 components: [30 { type: "profile", enable: true, slot: "top" },31 { type: "music", enable: true, slot: "top" },32 { type: "announcement", enable: true, slot: "top", pages: ["home"] },33 {34 type: "categories",35 enable: true,36 slot: "sticky",37 collapseAfter: 5,38 pages: [39 "home",40 "archive",41 "friends",42 "moments",43 "anime",44 "compass",45 "skills",46 "projects",47 "devices",48 "timeline",49 "albums",50 "about",51 "post",52 "categories",53 "tags",54 ],55 },56 {57 type: "tags",58 enable: true,59 slot: "sticky",60 collapseAfter: 6,61 pages: [62 "home",63 "archive",64 "friends",65 "moments",66 "anime",67 "compass",68 "skills",69 "projects",70 "devices",71 "timeline",72 "albums",73 "about",74 "post",75 "categories",76 "tags",77 ],78 },79 {80 type: "stats",81 enable: true,82 slot: "top",83 column: "secondary",84 pages: ["home", "archive", "categories", "tags"],85 },86 { type: "calendar", enable: true, slot: "top", column: "secondary" },87 {88 type: "toc",89 enable: true,90 slot: "sticky",91 column: "secondary",92 pages: ["post"],93 },94 ],95});1import type { SiteConfig } from "@/types/config";2import type {3 ResolvedTextureOptions,4 TextureConfig,5} from "@/types/textureConfig";6import { withUserConfig } from "@/utils/config-overlay.ts";7
8/**9 * 站点核心配置:标题 / 语言 / 主题色(HCT 动态配色)/ 横幅 / 目录 / 进度条 / favicon。10 * 类型见 src/types/config.ts。11 */12export const siteConfig: SiteConfig = withUserConfig("site", {13 site: "https://shirone.vercel.app/",14 base: "/",15 title: "Shirone Demo",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: "en", // 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 Demo",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 // Leave this array empty to use the default favicon124 // {125 // src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory126 // theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode127 // sizes: '32x32', // (Optional) Only if you have favicons of different sizes128 // }129 ],130});131
132/**133 * 解析并返回背景纹理配置选项(包含关闭短路与 0 开销优化判定)134 */135export function resolveTextureOptions(136 config: boolean | TextureConfig | undefined = siteConfig.texture,137 displaySettingsTexture: boolean = siteConfig.displaySettings?.texture ?? true,138): ResolvedTextureOptions {139 if (config === false || config === undefined) {140 return {141 enable: false,142 defaultPreset: "none",143 defaultOpacity: 0.12,144 allowMotion: false,145 };146 }147
148 if (config === true) {149 return {150 enable: true,151 defaultPreset: "starlight",152 defaultOpacity: 0.12,153 allowMotion: true,154 };155 }156
157 const enable = config.enable ?? true;158 const defaultPreset = config.defaultPreset ?? "starlight";159 const defaultOpacity = config.defaultOpacity ?? 0.12;160 const allowMotion = config.allowMotion ?? true;161
162 // 性能短路优化:163 // 如果配置 enable: false,或者 defaultPreset: "none" 且显示设置面板未允许切换(访客也无法开启),164 // 则自动视为完全关闭以达成零 DOM、零 CSS、零运行时代价。165 const effectiveEnable =166 enable && (defaultPreset !== "none" || displaySettingsTexture);167
168 return {169 enable: effectiveEnable,170 defaultPreset,171 defaultOpacity,172 allowMotion,173 };174}175
176/** 站点默认配色风格(访客未做选择时的回退值) */177export function getDefaultStyle(): string {178 return siteConfig.themeColor.style;179}180
181/** 站点默认 Color Spec(2021 / 2025) */182export function getDefaultSpec(): string {183 return siteConfig.themeColor.spec;184}185
186/** 解析并返回显示设置面板各项开关(未配置时默认 true) */187export function resolveDisplaySettings(): {188 colorStyle: boolean;189 colorSpec: boolean;190 wallpaperMode: boolean;191 layoutMode: boolean;192 reduceMotion: boolean;193 texture: boolean;194} {195 const cfg = siteConfig.displaySettings;196 const textureOpts = resolveTextureOptions(197 siteConfig.texture,198 cfg?.texture ?? true,199 );200 return {201 colorStyle: cfg?.colorStyle ?? true,202 colorSpec: cfg?.colorSpec ?? true,203 wallpaperMode: cfg?.wallpaperMode ?? true,204 layoutMode: cfg?.layoutMode ?? true,205 reduceMotion: cfg?.reduceMotion ?? true,206 texture: textureOpts.enable && (cfg?.texture ?? true),207 };208}1import type { SkillsConfig } from "@/types/skillsConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 技能页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /skills/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - disabledNames:可选被禁用的技能名称列表(例如 ["PHP"]);11 *12 * 注:技能的具体内容数据(技能名称、熟练度等级、图标、描述等)请在 `src/data/skills.ts` 中维护。13 */14export const skillsConfig: SkillsConfig = withUserConfig("skills", {15 enable: true,16 categories: [17 {18 key: "frontend",19 label: "Frontend",20 icon: "material-symbols:web-rounded",21 },22 {23 key: "backend",24 label: "Backend",25 icon: "material-symbols:dns-rounded",26 },27 {28 key: "tooling",29 label: "Tooling",30 icon: "material-symbols:construction-rounded",31 },32 ],33 // disabledNames: [],34});1import type { TimelineConfig } from "@/types/timelineConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * 时间线页行为与展示配置。6 *7 * 遵循「配置管行为,数据管内容」原则:8 * - enable:页面总开关;false 时导航入口同步隐藏,访问 /timeline/ 跳转 404;9 * - categories:筛选分类清单(数组顺序即页面顶部 Chips 顺序);10 * - order:排序方向,默认为 "desc"(时间倒序,最新在前);可选 "asc"(正序);11 * - disabledTitles:可选被禁用的事件标题列表;12 *13 * 注:时间线的具体节点数据(标题、日期、经历描述、要点列表、关联链接等)请在 `src/data/timeline.ts` 中维护。14 */15export const timelineConfig: TimelineConfig = withUserConfig("timeline", {16 enable: true,17 categories: [18 {19 key: "milestone",20 label: "Milestones",21 icon: "material-symbols:flag-rounded",22 },23 {24 key: "project",25 label: "Projects",26 icon: "material-symbols:code-rounded",27 },28 {29 key: "career",30 label: "Career",31 icon: "material-symbols:work-rounded",32 },33 {34 key: "education",35 label: "Education",36 icon: "material-symbols:school-rounded",37 },38 {39 key: "life",40 label: "Life",41 icon: "material-symbols:favorite-rounded",42 },43 ],44 order: "desc",45 // disabledTitles: [],46});1import type { ResolvedUmamiOptions, UmamiConfig } from "@/types/umamiConfig";2import { withUserConfig } from "@/utils/config-overlay.ts";3
4/**5 * Umami 统计配置单一真源(由 oddmisc 提供)。6 *7 * 遵循「零额外负担」原则:默认全局关闭(enable: false),8 * 在未开启时不产生任何外部网络请求、零额外 DOM 占位与零包体积膨胀。9 *10 * 详细用法见:`docs/umami-guide.md`11 */12export const umamiConfig: UmamiConfig = withUserConfig("umami", {13 /** 全局 Umami 统计总开关:false 时完全不加载 oddmisc 运行时脚本与 DOM */14 enable: false,15 /** Umami 分享链接(必填) */16 shareUrl: "",17 /** Umami Website ID;与 scriptUrl 同时填写时启用访问采集 */18 websiteId: "",19 /** Umami 采集脚本 URL;与 websiteId 同时填写时启用访问采集 */20 scriptUrl: "",21});22
23/**24 * 解析并校验 Umami 配置。未启用或关键参数缺失时返回 null。25 */26export function resolveUmamiOptions(config: UmamiConfig): ResolvedUmamiOptions {27 if (!config.enable) {28 return null;29 }30 const shareUrl = config.shareUrl?.trim();31 if (!shareUrl) {32 return null;33 }34 return {35 shareUrl,36 websiteId: config.websiteId?.trim() || undefined,37 scriptUrl: config.scriptUrl?.trim() || undefined,38 };39}40
41export type { ResolvedUmamiOptions };Share Article
Generate a share poster or copy the link to share this article.
Continue reading
Related reading
Based on shared tags and categories
Take another route
A consistent pick from other articles
Last updated on , 17 days ago
Some content may be outdated