
Fulfilling a decade-old dream: building an SAO-themed blog and documenting the MDsveX writing syntax
Preface
The first line of code I ever wrote (in any meaningful sense) was a single-file HTML personal blog. The title was literally “Personal Blog”. After that I went through:
- 2023: Hexo + GitHub Pages
- 2024–2025: xLog
Since early 2025, xLog had become more and more unstable, so I exported everything in advance and kept it ready to migrate. By the time I finally decided to “build my own”, there were only two days left before 2026. For a three-minute-interest ADHD brain, this used to be an impossible mission. Luckily, 2025 turned into the “Year of Agents” (hopefully), and code agents became dramatically stronger than what we had at the end of last year. I’ve successfully moved from “trust conversational programming only” to being okay with letting agents take the wheel.
So… Antigravity + Cursor + Codex, let’s go. With Gemini 3.0 Pro/Flash, Claude 4.5 Opus/Sonnet, and GPT-5.2-Extra-High backing them up, these tools have reached a level that would’ve been unimaginable to me just a few months ago.
Design Direction
When I opened Antigravity, this image suddenly popped into my head:

So I thought: why not build a Sword Art Online–themed blog?
From there my brain started rendering a whole UI in real time, and I tried to use AI to prototype it.
Tech Stack
At the core it’s SvelteKit + MDsveX — a combo that lets me write Svelte components directly inside Markdown, which is extremely flexible.
For code highlighting I use rehype-pretty-code + Shiki, supporting line numbers, line highlights, and code folding. GitHub-style Markdown extensions (tables, task lists, etc.) are enabled via remark-gfm.
Styling-wise it’s pure SCSS — no Tailwind or other frameworks. Well it’s partly because I forgot to tell the LLM in the planning stage, but also because Tailwind can genuinely become a maintenance nightmare. So let’s just go back to the “learn frontend by typing CSS line by line in front of a live server” era.
Writing Guide (Notes to Future Me)
Everything below is a cheat sheet for writing posts in this blog (SvelteKit + MDsveX): file conventions, frontmatter, supported Markdown extensions, math, enhanced code blocks, media embeds, Svelte-in-Markdown, and a few gotchas.
0. Quick Start (Checklist)
- New post:
src/lib/content/blog/<YYYY>/<slug>.md - English version:
src/lib/content/blog/<YYYY>/<slug>.en.md(same slug; locale suffix) - Static assets:
static/assets/...maps to/assets/... - URL:
/<slug>(slug = filename; must be globally unique) - Local preview:
bun run dev→http://localhost:5173/<slug>
1. Frontmatter (Metadata)
Fields used by the list page and the post page:
| Field | Required | Type | Notes |
|---|---|---|---|
title | ✅ | string | Post title |
excerpt | ✅ | string | Card text + SEO description (keep it short, ~≤ 180 chars) |
date | ✅ | string | Sort key (use ISO YYYY-MM-DD or full ISO datetime) |
tags | ✅ | string[] | Tag filters |
readTime | ✅ | number | Minutes (manual) |
cover | ⭕ | string | Cover image path (/assets/...) |
creationMethod | ✅ | independent / ai-assisted | Shows the creation badge |
lastUpdated | ⭕ | string | Shown as “Last updated” |
translated | ⭕ | string | For translated posts: triggers a “translated by …” popup |
Template:
---
title: "Post Title"
excerpt: "Short summary (aim for ≤ 180 characters)"
date: "2026-01-01"
tags:
- tag-one
- tag-two
readTime: 10
cover: "/assets/images/posts/2026/my-post/cover.png"
creationMethod: independent
lastUpdated: "2026-01-01"
# translated: "GPT-5.2" # Only for translated versions
---2. Markdown + GFM Extensions
This project enables remark-gfm, so you get GitHub-style tables, task lists, strikethrough, and autolinks on top of basic Markdown.
Headings (Auto Anchors)
Headings automatically get ids, and the entire heading is clickable (via rehype-slug + rehype-autolink-headings). To link to a section, just copy the URL with #....
Lists & Task Lists
- Regular ordered/unordered lists work as usual
- Task lists (GFM):
- [ ] TODO
- [x] Done
Links
- Normal links:
[Svelte](https://svelte.dev) - Force “open in new tab”: use HTML (and keep
rel):
<a target="_blank" rel="noopener noreferrer" href="https://svelte.dev">Svelte</a>Tables
| Feature | Use | Notes |
|---|---|---|
| Tables | Compare data | via GFM |
| Task lists | Track TODOs | via GFM |
| Math | Technical writing | rendered by KaTeX |
Footnotes (Optional)
This is a sentence.[^1]
[^1]: This is the footnote text.
3. Math (KaTeX)
Math is enabled via remark-math + rehype-katex-svelte:
- Inline:
$E = mc^2$→ - Display:
4. Code Blocks (Shiki + rehype-pretty-code)
Code blocks come with syntax highlighting + a copy button, and support titles, line numbers, highlighted lines, and folding:
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": code block title (filename)showLineNumbers: enable line numbers{2,5}: highlight lines 2 and 5 (ranges supported:{2-6,10})- Copy button: appears at the top-right on hover
Folding (By Line)
Fold long/boring ranges:
:root {
/* Primary palette */
--sao-orange: #f5a623;
--sao-blue: #0ea5e9;
--blog-code-bg: #1c1b2f;
/* Folded section */
--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} hides lines 8–15 by default; readers can toggle it from the bottom-left of the code block.
5. Images (Auto WebP + Lazyload)
Recommended location: static/assets/images/posts/<YYYY>/<slug>/..., and reference them with absolute paths:
During build, local .png/.jpg/.jpeg images are wrapped in a <picture> with a .webp source (if the sibling .webp exists). To batch-generate WebP files:
bun run convert-webpImages also default to loading="lazy" and decoding="async" (you can override by writing your own <img loading="eager" ...>).
For alignment/width control, HTML is the easiest:
<p align="center">
<img src="/assets/images/posts/2026/my-post/demo.png" alt="demo" width="70%" />
</p>6. Video Embeds (YouTube Example)
Use a responsive wrapper so it doesn’t overflow on mobile:
Replace the video ID after embed/.
7. Music Player (GeoMusicPlayer)
This blog includes a built-in music player component (QQ Music + YouTube; it picks a source based on the viewer’s network):
How to use:
- Import it at the top of the post:
import GeoMusicPlayer from '$lib/components/GeoMusicPlayer.svelte'; - Place the component tag and fill in props
Props:
qqMusicSongId: QQ Music song IDyoutubeVideoId: YouTube video IDsongTitle: song title (also used as the global player key)coverExtension: cover extension (expectsstatic/music-covers/<songTitle>.<ext>)autoplay: whether to autoplay (boolean)
8. Svelte in Markdown (MDsveX)
Rules of thumb
- Keep the
<script>block right after frontmatter (the earlier, the safer) - You can write components/HTML directly in the body (it’s compiled as Svelte)
- Avoid writing
{count}interpolation in plain Markdown text (it gets escaped as text); wrap it in an element instead, e.g.<span>{count}</span>
Minimal interactive example
This is a live counter demo
Gotcha: don’t use HTML onclick
The build pipeline strips onclick/onClick attributes (mainly to harden the code-block copy button). For interactions, prefer the project’s onmousedown={...} style (or Svelte’s on:click={...}).
9. Horizontal Rules
Use three hyphens:
10. Bilingual Posts (ZH/EN)
Locale variants are resolved by filename suffix:
- Chinese:
<slug>.md(default) - English:
<slug>.en.md
If the English post is a translation, add translated: "xxx" in frontmatter to show the translation notice.
Closing Thoughts
While building this blog framework, there were countless moments where I wanted to scrap a design and start over, or sneak in a new component at the last minute — and I did… exactly that. For someone who promised to “finish migrating before the new year”, it was pretty painful, but looking back it was also a ton of fun. I thought I probably wouldn’t have a second experience like this in my life, but then I realized: all of this is proof that I still have the passion. I’m sure I’ll end up doing many more interesting things like this.
Just for fun.