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:
- creates a new page for each post;
- changes the layout from the prescribed HTML layout to a new man page layout; and
- 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!