
圆十年前的一个梦,SAO 主题博客的制作和博文语法记录
前言
还记得我真正意义上写的第一行代码就是一个单 html 文件的个人博客,仅仅是标题叫“个人博客”,然后后面经历了:
- 2023 Hexo + Github Pages
- 2024 - 2025 xLog
由于从 25 年年初开始 xLog 的服务越来越不稳定,我便提前导出了全部文件,随时准备迁移到别处。等我想到并下定决心“自己写一个”的时候已经还差两天就到 2026 了,这对于一个三分钟热度的 ADHD 来说在之前是真的无法完成的任务,还好 2025 是“Agent 元年”(最好是),Code Agent 比去年年底强大了太多,我已经从“trust 对话式编程 only”成功过渡到了接纳 Agent 接管编程的心态。
那还说什么,Antigravity + Cursor + Codex 启动!这些工具在 Gemini 3.0 Pro/Flash, Claude 4.5 Opus/Sonnet, GPT-5.2-Extra-High 几元大将的加持下已经到了几个月前的我无法想象的水平。
设计思路
打开 Antigravity 很突然地脑海里出现了这幅图:

那就做一个刀剑神域主题的博客吧!
想到这儿大脑里就开始渲染各种设计,然后尝试靠 AI 把它的原型做出来。
技术栈
核心是 SvelteKit + MDsveX,这个组合能让我在 Markdown 里直接写 Svelte 组件,非常灵活。
代码高亮用的是 rehype-pretty-code + Shiki,支持行号、行高亮、代码折叠等功能。表格和任务列表这些 GitHub 风格的 Markdown 扩展也都支持,通过 remark-gfm 插件实现。
样式方面用的是 SCSS,没有引入 Tailwind 之类的框架。好吧其实是写 Plan 的时候忘了跟 LLM 说,那就正好试试重归刚学前端时对着 live server 一行行敲过的 css 吧
写在最后
在自己写博客框架的过程中,有无数次推倒某一个设计重来或者临时增加一个组件的念头,但我都…选择了执行那些念头,这对于立下年前迁移完博客 flag 的我来说相当痛苦,但是回过头来看也趣味无穷。本来想说这辈子应该很难有第二次类似的经历了,但又想了下,其实这都是说明我还对这些充满 passion 的证据,我肯定还会经历很多这样有趣的事!
Just for fun.
后面就全都是给我以后看的了,因为没有专门做一个写文章的入口,后面得手敲各种格式,很容易忘记。
写作指南(给未来的我)
这里是本博客(SvelteKit + MDsveX)的“写作速查表”:路径约定、frontmatter、Markdown 扩展、数学公式、代码块增强、媒体与组件,以及常见坑。
0. 快速开始(Checklist)
- 新建文章:
src/lib/content/blog/<YYYY>/<slug>.md - 英文版:
src/lib/content/blog/<YYYY>/<slug>.en.md(同 slug,后缀是语言) - 资源目录:
static/assets/...会映射到站点根路径/assets/... - 访问地址:
/<slug>(slug=文件名;全站唯一,别在不同年份重名) - 本地预览:
bun run dev→http://localhost:5173/<slug>
1. Frontmatter(元数据)
约定字段(按页面展示/列表排序的实际使用情况整理):
| 字段 | 必填 | 类型 | 作用 |
|---|---|---|---|
title | ✅ | string | 文章标题 |
excerpt | ✅ | string | 列表卡片 + SEO 描述(建议 ≤ 180 字符) |
date | ✅ | string | 排序依据(建议 ISO:YYYY-MM-DD 或带时区的完整时间) |
tags | ✅ | string[] | 标签筛选 |
readTime | ✅ | number | 阅读时间(分钟,手填) |
cover | ⭕ | string | 封面图路径(/assets/...) |
creationMethod | ✅ | independent / ai-assisted | 文章创作方式标签 |
lastUpdated | ⭕ | string | 最近更新日期(显示“Last updated”) |
translated | ⭕ | string | 仅翻译稿填写:会弹窗提示“该文章由 xxx 翻译” |
模板:
---
title: "文章标题"
excerpt: "简短摘要(建议 ≤ 180 字符)"
date: "2026-01-01"
tags:
- 标签一
- 标签二
readTime: 10
cover: "/assets/images/posts/2026/my-post/cover.png"
creationMethod: independent
lastUpdated: "2026-01-01"
# translated: "GPT-5.2" # 仅翻译稿需要
---2. Markdown 基础 + GFM 扩展
本项目启用了 remark-gfm,除了基础 Markdown 以外,还支持表格/任务列表/删除线/自动链接等 GitHub 风格语法。
标题(自动生成锚点)
标题会自动生成 id,并且整段标题可点击跳转(rehype-slug + rehype-autolink-headings)。要引用某一小节,直接复制浏览器里带 #xxx 的 URL 即可。
列表与任务列表
- 无序列表、嵌套列表、以及有序列表都正常支持
- 任务列表(GFM):
- [ ] TODO
- [x] Done
链接
- 普通链接:
[Svelte](https://svelte.dev) - 想强制新标签页打开:用 HTML(记得加
rel):
<a target="_blank" rel="noopener noreferrer" href="https://svelte.dev">Svelte</a>表格
| 功能 | 用途 | 备注 |
|---|---|---|
| 表格 | 数据对比 | 来自 GFM |
| 任务列表 | TODO | 来自 GFM |
| 数学公式 | 技术写作 | KaTeX 渲染 |
脚注(可选)
这里是一句话[^1]
[^1]: 这里是脚注内容。
3. 数学公式(KaTeX)
已启用 remark-math + rehype-katex-svelte,支持行内与行间公式:
- 行内:
$E = mc^2$→ - 行间:
4. 代码块增强(Shiki + rehype-pretty-code)
代码块自带语法高亮 + 复制按钮,并支持标题、行号、高亮行、折叠行:
const items = [
{ name: "Item A", price: 100 },
{ name: "Item B", price: 200 },
{ name: "Item C", price: 300 },
];
const total = items.reduce((sum, item) => sum + item.price, 0);
console.log(`Total: ${total}`);title="example.js":代码块标题(文件名)showLineNumbers:显示行号{2,5}:高亮第 2 行与第 5 行(支持区间:{2-6,10})- 复制按钮:悬浮到代码块右上角即可看到
代码折叠(按行)
对“长配置/不重要片段”可以折叠,支持逗号与区间:
:root {
/* 主色调 */
--sao-orange: #f5a623;
--sao-blue: #0ea5e9;
--blog-code-bg: #1c1b2f;
/* 折叠的部分 */
--blog-code-border: rgba(245, 166, 35, 0.32);
--blog-inline-code-bg: rgba(245, 166, 35, 0.18);
--blog-code-highlight: rgba(245, 166, 35, 0.16);
--blog-code-lineno: #b5b7cd;
--blog-code-shell: linear-gradient(135deg, rgba(28, 27, 47, 0.96), rgba(21, 23, 39, 0.9));
--blog-code-line: rgba(255, 255, 255, 0.08);
--blog-code-text: #eaeaf6;
}fold={8-15} 会把第 8 到 15 行默认隐藏,读者可点击代码块左下角的提示展开/收起。
5. 图片(自动 WebP + Lazyload)
推荐把图片放在:static/assets/images/posts/<YYYY>/<slug>/...,引用时用绝对路径:
本项目会在构建阶段把本地 .png/.jpg/.jpeg 图片自动包一层 <picture>,并尝试加载同名 .webp(有就用,没有就回退原图)。批量生成 WebP:
bun run convert-webp另外,图片默认会补上 loading="lazy" 与 decoding="async"(你手动写 <img loading="eager" ...> 可以覆盖)。
想控制居中/宽度,用 HTML 更省事:
<p align="center">
<img src="/assets/images/posts/2026/my-post/demo.png" alt="demo" width="70%" />
</p>6. 视频嵌入(YouTube 示例)
建议用“自适应比例”的写法,移动端不容易溢出:
只需要把视频 ID 替换到 embed/ 后面即可。
7. 音乐播放器(GeoMusicPlayer)
博客内置了一个音乐播放器组件(QQ 音乐 + YouTube 双源,会根据网络环境自动选择):
使用方法:
- 在文章开头的
<script>标签里导入组件:import GeoMusicPlayer from '$lib/components/GeoMusicPlayer.svelte'; - 在需要的位置插入组件标签,填入相应参数
参数说明:
qqMusicSongId: QQ 音乐的歌曲 IDyoutubeVideoId: YouTube 视频 IDsongTitle: 歌曲标题(同时作为全局播放器的 key)coverExtension: 封面图扩展名(对应static/music-covers/<songTitle>.<ext>)autoplay: 是否自动播放(布尔值)
8. 在 Markdown 里写 Svelte(MDsveX)
基本规则
<script>建议放在 frontmatter 下面(越靠上越稳)- 正文里可以直接写组件标签/HTML(会被当成 Svelte 渲染)
- 普通 Markdown 文本里尽量不要写
{count}这种插值(会被当作文本转义);要用插值就包进一个节点里,例如<span>{count}</span>
一个最小交互例子
这是一个实时计数器演示
一个坑:不要用 HTML 的 onclick
构建阶段会移除 onclick/onClick 属性(主要是为了代码块复制按钮的安全处理)。如果要交互,优先用项目里通用的 onmousedown={...}(或 Svelte 的 on:click={...})。
9. 分割线
使用三个连字符创建分割线:
10. 多语言(中文/英文)
同一篇文章的多语言版本通过文件名后缀识别:
- 中文:
<slug>.md(默认) - 英文:
<slug>.en.md
英文版如果是“翻译稿”,在 frontmatter 里加 translated: "xxx" 会自动弹出翻译提示。