Restructuring Jekyll Multilingual Blog URLs and Overhauling Pagination

URL restructuring diagram

The URLs Were Bothering Me

This blog supports three languages: Korean, English, and Japanese. But the URL structure was a bit off.

Korean:   /2026/02/21/claude-code-1m-cost-reality/
English:  /2026/02/21/claude-code-1m-cost-reality-en/
Japanese: /2026/02/21/claude-code-1m-cost-reality-ja/

The -en suffix tacked on to every English post URL — not great. Google also recommends using subdirectory structures like /en/ for language-specific content. And the longer I waited, the more posts would pile up, making it even harder to fix later.

Here’s what I wanted:

graph LR
    subgraph Before
        A["/date/slug/"] --- B["Korean"]
        C["/date/slug-en/"] --- D["English"]
        E["/date/slug-ja/"] --- F["Japanese"]
    end
    subgraph After
        G["/date/slug/"] --- H["Korean"]
        I["/en/date/slug/"] --- J["English"]
        K["/ja/date/slug/"] --- L["Japanese"]
    end

Jekyll lets you set a permalink for each post. I added this to every English and Japanese post’s front matter:

# English post front matter
permalink: /en/:year/:month/:day/:title/
redirect_from:
  - /2026/02/21/claude-code-1m-cost-reality-en/

permalink sets the new URL, and redirect_from ensures the old URL redirects automatically. This requires the jekyll-redirect-from plugin, which GitHub Pages officially supports — just one line in _config.yml.

# _config.yml
plugins:
  - jekyll-redirect-from

Next, I updated the hreflang tags in _layouts/default.html:

<!-- Before -->
<link rel="alternate" hreflang="en" href="https://realcoding.blog-en/" />

<!-- After -->
<link rel="alternate" hreflang="en" href="https://realcoding.blog/en-en/" />

The real grunt work, though, was updating 152 hardcoded links in the post bodies. Every English post referencing another English post was using the old URL format.

<!-- Before -->
[Escaping Compacting Hell](/2026/02/20/claude-code-1m-context-review-en/)

<!-- After -->
[Escaping Compacting Hell](/en/2026/02/20/claude-code-1m-context-review-en/)

Final tally: 140 files changed, 569 insertions, 154 deletions. Quite the operation.

Then Pagination Broke

After restructuring the URLs, pagination became an issue. Jekyll’s jekyll-paginate plugin only works in the root directory (/). This is a well-known limitation.

It worked fine on the Korean homepage (/), but on /en/ and /ja/, the paginator object simply doesn’t exist. No server-side Liquid solution was possible.

Rewrote It in JS

So I switched to client-side JavaScript pagination entirely. The concept is straightforward:

  1. Render all posts in the HTML via Liquid
  2. JS shows/hides them in groups of 10 using display: none/block
  3. Page state managed through ?page=2 URL parameters

Here’s the core logic:

(function() {
  var POSTS_PER_PAGE = 10;
  var posts = document.querySelectorAll('.posts-list .post-item');
  if (posts.length <= POSTS_PER_PAGE) return;

  var totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
  var params = new URLSearchParams(window.location.search);
  var currentPage = parseInt(params.get('page')) || 1;

  function showPage(page) {
    var start = (page - 1) * POSTS_PER_PAGE;
    var end = start + POSTS_PER_PAGE;
    for (var i = 0; i < posts.length; i++) {
      posts[i].style.display = (i >= start && i < end) ? '' : 'none';
    }
  }
  showPage(currentPage);
})();

This same script goes into index.html, en/index.html, and ja/index.html. Multilingual labels were added to _data/*.yml:

# _data/en.yml
posts:
  prev_page: "Previous"
  next_page: "Next"

Previously, only Korean had pagination via /page2/, /page3/. Now all three languages use the unified ?page=2 approach.

Wrap-up

Set your URL structure early, before posts accumulate. Changing it later means touching 140+ files. And if you’re running a multilingual Jekyll blog, skip jekyll-paginate for non-root pages — client-side JS is the pragmatic choice.