From Jekyll to VitePress: The Progressive Evolution of This Blog
This blog is eleven years old. It has survived Octopress, Jekyll 2, Jekyll 3, a
couple of domain moves, and now — a rebuild on VitePress.
What I want to share today isn't "here's my new blog". It's the process: how
a legacy static site gets modernized step by step, without breaking a single old
URL, and where I'm planning to take it next — including turning the blog layer
into a reusable, Jekyll-like logging system powered by VitePress.
The legacy: eleven years of Jekyll
The original site was built on Octopress 2, which was itself a layer over
Jekyll 2. Later I migrated to Jekyll 3 (I wrote about it back then).
timeline
title The blog's journey
2015 : Octopress 2 + Jekyll 2
: greyshade theme, Disqus, MathJax
2016 : Migrate to Jekyll 3
: cleaner plugin model, gem ecosystem
2018 : Content grows — tools, slides, tags
2025 : Static pages feel dated
: JS-heavy tools need modern tooling
2026 : Rebuild on VitePress
: same URLs, same look, modern DXThe good news: after ten years, the URL structure had become part of my
identity — /blog/2016/05/25/... links are scattered across forums, bookmarks
and other posts. Whatever I did next, those URLs had to keep working.
The not-so-good news: maintaining a Jekyll theme means patching Liquid,
managing gem versions, and hand-editing stubs for every tag, category and
pagination page. It's mechanical, repetitive work that a computer should do.
The decision: rebuild, don't rewrite
"Rebuild" means: keep the content, keep the URLs, keep the visual identity —
and replace the machinery underneath.
VitePress was the right fit because it gives me a modern Vue 3 + Vite pipeline
while staying a static site generator: content stays in Markdown, output
stays static HTML, and the whole thing deploys to GitHub Pages unchanged.
flowchart LR
subgraph Old["Jekyll era"]
A1["Markdown posts"]
A2["Liquid templates"]
A3["Hand-written stubs<br/>tags / categories / pagination"]
end
subgraph New["VitePress era"]
B1["Markdown posts"]
B2["Vue components"]
B3["Auto-generated dynamic routes<br/>data-driven pages"]
end
A1 --> B1
A2 --> B2
A3 --> B3
Old -- "same content, same URLs" --> NewPhase 1: a faithful migration
The first pass had exactly one goal: feature parity. The greyshade layout
became a custom Layout.vue;
Disqus comments, MathJax and Google Analytics were wired back in; the sidebar,
tag clouds and archives all made the jump.
One subtle trick: VitePress renders pages at build time, but I wanted the
original greyshade theme class on <body> before any JavaScript runs, to
avoid a flash of unstyled content. That's done with a tiny transformHtml hook
in the config — the static HTML ships with the theme class already applied.
Phase 2: content becomes data
This is where the real architecture work started. Instead of hand-writing a stub
page for every tag, category, pagination page and tool page, I wrote VitePress
plugins that treat content as data:
- Scan a source directory (
docs/_posts,docs/_slides,docs/_tools)
and parse frontmatter. - Expose the results as Vite virtual modules (
virtual:w3cub:posts,virtual:w3cub:slides, …) that Vue components import directly. - Emit dynamic-route templates (
docs/blog/tags/[slug].md+[slug].paths.ts) so VitePress enumerates data-driven pages at build time. - Clean up automatically when a source disappears.
The payoff: to add a tag or category, I just write posts. The pages, feeds and
pagination appear by themselves.
flowchart TD
S["docs/_posts/*.md"] --> P["w3cub-blog plugin<br/>scan + parse"]
P --> V["virtual:w3cub:posts"]
V --> C["Blog components<br/>(index / tags / archives)"]
P --> D["dynamic routes<br/>[slug].md + paths.ts"]
D --> O["/blog/tags/<slug>/<br/>/posts/<n>/"]
P --> F["atom.xml + sitemap.xml"]Phase 3: configurable URLs (permalink templates)
One thing Jekyll gets right is the permalink template — the URL scheme lives
in config, not in the code. I ported that idea into a shared
permalink.ts
used by all three content plugins:
blog: '/blog/:year/:month/:day/:title/' # directory-style
slides: '/slides/:name/'
tools: '/tools/:path/'Trailing / gives directory-style URLs, .html gives file-style — so the whole
site can switch schemes by editing one string. Old links keep working because
the defaults reproduce the original Jekyll URLs exactly.
Phase 4: richer content — Mermaid in Markdown
The latest addition is Mermaid support. Now a fenced
block tagged with the mermaid language — in a post or a slide deck — becomes
a real diagram.
The rendering is intentionally split in two:
- Build time: a small markdown-it plugin
(markdown-mermaid.ts)
turnsmermaidfences into a<pre class="mermaid">placeholder. - Browser time: the theme's mermaid.ts
dynamically imports mermaid and renders each placeholder to SVG on mount and
on every route change / slide flip.
sequenceDiagram
participant MD as markdown-it (build)
participant P as Page HTML
participant M as mermaid (browser)
Note over MD: mermaid fence detected
MD->>P: <pre class="mermaid">source</pre>
Note over P: page loads
P->>M: renderMermaid() on mount / route change
M->>M: render to SVG
M-->>P: inject <svg>This keeps the build fully static (no server-side rendering of diagrams) and
works everywhere the theme renders — including the in-page PPT player, which
uses its own markdown-it instance and needs the same fence handling.
The next step: a Jekyll-like logging system on VitePress
Everything above is already organized as self-contained plugins. The blog layer
(w3cub-blog.ts) doesn't know about the theme — it just scans posts and exposes
data. That's the property I want to cash in next.
My plan is to extract the blog layer into a reusable package: a
"Jekyll-for-VitePress" that gives any project posts, tags, categories, archives,
pagination and atom feeds out of the box, configured through permalink-style
settings — exactly like Jekyll, but powered by VitePress under the hood.
flowchart LR
subgraph Today["Today — in-repo plugins"]
B1["w3cub-blog.ts"]
B2["permalink.ts"]
B3["theme components"]
end
subgraph Tomorrow["Tomorrow — extractable package"]
P1["@w3cub/blog plugin<br/>(scan + data + routes + feeds)"]
P2["@w3cub/theme<br/>(list / taxonomy / archive views)"]
P3["drop-in config:<br/>permalink templates"]
end
B1 --> P1
B2 --> P2
B3 --> P3
P1 --> ANY["Any VitePress project"]Want to try it? The current state already works as a reference implementation —
the repo is open, and the
architecture docs
describe exactly how each piece fits together. I also made a short
slide deck summarizing this whole story.
Takeaways
- Preserve what matters. URLs and content outlive any framework. Rebuild
the machinery, keep the identity. - Make content data, not pages. If your toolchain generates repetitive
pages by hand, a plugin that treats content as data will delete whole classes
of busywork. - Enhance incrementally. Permalink config, dynamic routes, Mermaid — each
step shipped on its own, without a big-bang rewrite. - Design for extraction. Organize features as independent plugins from day
one, and "turn it into a reusable system later" becomes a packaging task, not
a rewrite.
The blog you're reading right now is built from all of the above. Eleven years
in, it feels more maintainable than ever.
原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
Comments