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 DX

The 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" --> New

Phase 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:

  1. Scan a source directory (docs/_posts, docs/_slides, docs/_tools)
    and parse frontmatter.
  2. Expose the results as Vite virtual modules (virtual:w3cub:posts,
    virtual:w3cub:slides, …) that Vue components import directly.
  3. Emit dynamic-route templates (docs/blog/tags/[slug].md +
    [slug].paths.ts) so VitePress enumerates data-driven pages at build time.
  4. 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"]

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:

yaml
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)
    turns mermaid fences 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

  1. Preserve what matters. URLs and content outlive any framework. Rebuild
    the machinery, keep the identity.
  2. 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.
  3. Enhance incrementally. Permalink config, dynamic routes, Mermaid — each
    step shipped on its own, without a big-bang rewrite.
  4. 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