Skip to content

Rendering my posts as man pages

Some time back I remember reading on one of the blogs that I follow that you could read their blog posts as man pages by changing the .html suffix on the URL to .7. I thought this was a really fun idea. It’s a great example of how programming doesn’t always have to be super practical. Sometimes it can be about making things just because they’re fun. This is something I’d wanted to implement on my own blog ever since, and so one sunny Sunday afternoon, I set out to do just that.

I figured it should be relatively straightforward as my website is built with Jekyll which is very extensible. It turns out that, combined with Ruby’s inherent hackability, it was.

Jekyll lets you build generator plugins which can programmatically create additional content for you. So all I needed to make this work is a generator that iterates through my blog posts and:

  1. creates a new page for each post;
  2. changes the layout from the prescribed HTML layout to a new man page layout; and
  3. somehow converts the content of my blog posts to a format supported by man(1).

This is what I ended up with.

module Man
  class Generator < Jekyll::Generator
    def generate(site)
      @site = site

      site.posts.docs.each do |post|
        @site.pages << ManPage.new(site, post)
      end
    end
  end

  class ManPage < Jekyll::Page
    def initialize(site, post, section = 7)
      @site = site
      @base = site.source
      @dir = File.dirname(post.url)

      @basename = post.data["slug"]
      @ext = ".#{section}"
      @name = @basename + @ext

      @data = {
        **post.data,
        "layout" => replace_ext(post.data["layout"], section),
        "command" => site.config["url"].sub("https://", ""),
        "section" => section
      }

      # TODO: convert post content to a format supported by man(1)
      # and set post content...
      # @content =
    end

    private

    def replace_ext(filename, ext)
      "#{File.basename(filename, File.extname(filename))}.#{ext}"
    end
  end
end

Most of this is just boilerplate taken straight from the Jekyll documentation. There are a few interesting bits here. The first is setting the basename to the slug of the post and the extension to .7. This ensures that the man page representation has the same URL as the HTML representation but with the man extension. The other interesting bit is the fields in the @data hash. They correspond the page metadata that you would normally set in the frontmatter. The layout is set to a new layout with the same name as the one specified in the post, but with the .7 file extension. The rest of the fields are variables for rendering in the layout - the man page “command”, and the man section. I’m also copying over all the metadata from the post’s frontmatter with **post.data. The layout looks like this:

.TH {{ page.command | upcase }} {{ page.section }} {{ page.date | date: "%Y-%m-%d" }}
.SH NAME
{{ page.title }}
.SH SYNOPSIS
{{ site.description }}
.SH TEXT
{{ page.content }}
.SH AUTHOR
{{ site.author.name }}
.SH COPYRIGHT
Copyright {{ page.date | date: "%Y" }} {{ site.author.name }}
.MT {{ site.author.email }}

Now the only thing left is to figure out how to convert the post content to a format supported by man(1). It turns out that Kramdown—the Markdown parser which is built into Jekyll—already has a built-in method that can convert Markdown to groff format. All you need to do is call to_man instead of to_html on the parsed document.

doc = Kramdown::Document.new(post.content)
@content = doc.to_man

I put it all together, and it was all going rather smoothly until…

Build Warning: Layout 'article.7' requested in ... does not exist.

Now we’re coding.

I did some digging through the Jekyll source code and found the culprit in lib/jekyll/readers/layout_reader.rb:

def layout_name(file)
  file.split(".")[0..-2].join(".")
end

I had two layouts for each post - an article.html layout which renders the default HTML representation of the post, and an article.7 layout which renders the man page representation. The method above is called by a read method which builds a hash table of the layouts as an instance variable in the LayoutReader class. It strips the file extension from the layout before adding it to the hash table, so when I have two layouts with the same name but different file extensions, the second layout it reads overrides the first.

A simple (and safe) solution to this would be to give the layout a different name. But they are different representations of the same layout, so they should have the same name. This is a personal project, and I wanted it done right, not safe. And Ruby is a very hackable language, so let’s hack it!

One of the really cool (and dangerous) things about Ruby is that you can monkey patch any class. It’s basically like having a patch which only exists in your codebase, and doesn’t need to be accepted upstream. The dangerous part is that if the Jekyll maintainers ever restructure the code, the patch might break. But I’m not too worried as worst case scenario I update my Jekyll dependency, my site doesn’t build, and it takes me about 10 minutes to resolve. And like I said, this is a personal project, and I’m allowed to break the rules if I want. So I built my patch:

module Jekyll
  class LayoutReader
    def layout_name(file)
      if File.extname(file) == ".html"
        file.split(".")[0..-2].join(".")
      else
        file
      end
    end
  end
end

To preserve backward compatibility, it first checks if the layout has an .html extension and if so, preserves the existing functionality. Otherwise, instead of adding just the basename of the file to the hash table, it adds the entire filename, extension and all. So now I can have multiple layouts for each file with the same name but different file extensions. Yay.

I ran rake, and lo and behold, it works! You can now view all of my blog posts as man pages by replacing the .html extension on the URL with .7. Give it a go!