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