My to-do list for this morning says “Work on Taxes.” Instead, I’m writing tools to make updates here.

A few things had been bugging me:

  • Posts were sorted by date but not time, so posts on the same day were sorted alphabetically instead of chronologically.
  • My permalinks were overly simplistic. All posts used the format /journal/title-slug. While unique titles avoided namespace issues, as I added categories and date-based index pages, I wanted the URLs to reflect some of that data.
  • Jekyll’s method of including categories in permalinks adds all categories like /category1/category2. I would like to only show a “main” category in the url.

I solved these issues with a combination of plugins to handle things dynamically at build time and scripts to bake some data into the posts.

Sorting

Jekyll sorts posts by date from the filename. If you have a properly formatted datetime value in your posts’ front matter, you can sort by date + time. I don’t. I use a simple time value in 12-hour format because I’m lazy and don’t want to think about date formats.

I solved this with a custom plugin to calculate an ISO 8601 datetime value at build time from the filename date and post time.

# _plugins/add_datetime_field.rb
require 'time'

Jekyll::Hooks.register :site, :post_read do |site|
  site.posts.docs.each do |post|
    if post.data['time']
      post_date = post.date.strftime('%Y-%m-%d')
      post_time = post.data['time']

      begin
        time_obj = Time.parse(post_time)
        combined = Time.parse("#{post_date} #{time_obj.strftime('%H:%M')}")
        post.data['datetime'] = combined.iso8601
      rescue ArgumentError => e
        Jekyll.logger.warn "Datetime Plugin:", "Could not parse time '#{post_time}' in #{post.path}: #{e.message}"
      end
    end
  end
end

With the datetime value added, sorting posts by date + time is as easy as:

{% assign sorted_posts = site.posts | sort: "datetime" | reverse %}
{% for post in sorted_posts limit:10 %}
  <div class="post">
    <h1 class="post-title"><a href="{{ post.url }}">{{ post.title }}</a></h1>
    <p class="meta">{{ post.date | date_to_string }}{% if post.place %} &#8212; {{ post.place }}{% endif %}</p>
    <div class="post-content text">
      {{ post.content }}
    </div>
  </div>
{% endfor %}

For paginated pages, I used the jekyll-pagination-v2 plugin, which supports sorting.

The v2 plugin is drop-in compatible with jekyll-pagination, all I had to do was update my _config.yml from:

paginate: 10
paginate_path: /journal/page:num/

To ↓

pagination:
  enabled: true
  per_page: 10
  permalink: /page:num/
  sort_field: datetime
  sort_reverse: true

Now posts are sorted by date + time on the main index and paginated /journal pages. Yay!

Before updating permalinks, I ensured existing links would still work. The jekyll-redirect-plugin maps old permalinks to new ones.

Redirects are performed by serving an HTML file with an HTTP-REFRESH meta tag pointing to your destination. No .htaccess file, nginx conf, xml file, or anything else is generated. It simply creates HTML files.

Not as great as a proper 301 or 302, but fine for my needs.

After installing the plugin, I added this to _config.yml to avoid generating a redirects.json that I don’t need:

redirect_from:
  json: false

Since all existing posts needed redirects, I used a Python script to write the old permalink into each post’s front matter.

See add_redirects.py and its README section.

After running ↑ that script ↑ every existing post had an entry in it’s front matter like:

redirect_from:
- /journal/voice-memo-manager/

To handle requests to the old url.

Once redirects were set up, I updated the default post permalink in _config.yaml:

defaults:
  - scope:
      path: ""
      type: "posts"
    values:
      permalink: /journal/:year/:month/:slug/

Now posts have permalinks with year and month, which will be useful for future index pages. Old /journal/:title links redirect nicely. Huzzah.

Jekyll supports :categories in permalinks, but I dislike how it handles posts in multiple categories. For example, this post is in both tools and field-notes. It will appear on /tools and /field-notes category pages.

Using /:categories/:year/:month/:slug/ would result in /tools/field-notes/2025/04/jekyll-tools. I dislike this because /tools/field-notes will never be a valid category URL.

I prefer specifying a link_category in the post front matter to specify the primary category in the permalink.

categories:
  - tools
  - field-notes
link_category: tools

So the post permalink becomes /tools/2025/04/jekyll-tools.

Jekyll doesn’t support custom front matter variables in permalinks, so I created a plugin to set a permalink for posts with link_category or categories.

  • If link_category is set, it is used as the primary category in the permalink.
  • If link_category is not set but the post has categories, the first category is used.
# _plugins/add_custom_permalink.rb
require 'time'

Jekyll::Hooks.register :site, :post_read do |site|
  site.posts.docs.each do |post|
    # Determine the link_category
    link_category = post.data['link_category']
    if !link_category && post.data['categories'] && post.data['categories'].any?
      link_category = post.data['categories'].first
    end

    # Skip this post if no link_category or categories are available
    next unless link_category

    # Extract year and month from the post date
    year = post.date.strftime('%Y')
    month = post.date.strftime('%m')
    slug = post.data['slug'] || post.data['title'].downcase.strip.gsub(" ", "-").gsub(/[^\w-]/, "")

    # Generate the custom permalink
    custom_permalink = "/#{link_category}/#{year}/#{month}/#{slug}/"

    # Set the custom permalink
    post.data['permalink'] = custom_permalink
  end
end

Now posts with categories have permalinks including one primary category instead of the default /journal. Hooray!

I also added a script to set link_category for all posts with existing categories. See set_link_category.py and its README section.

This script scans posts and sets the first category as link_category in their front matter. While the plugin fallback handles this, the script ensures permalinks won’t break if I add categories to old posts.

Now I have sorted posts with permalinks that will work well with future index pages. Not bad for a morning of not getting my taxes done!