Writing Jekyll Plugins

Hi! It looks like I have since continued, updated, or rethought this post in some ways, so you may want to look at this after you're done reading here.

It took me more than eighteen months and more than four hundred posts, here on Entropy Arbitrage, but I finally backed myself into a corner with the blog.

A door labeled Pull, in marker

I should note that, if you want to skip the storytelling and background, and just get to the concepts, I’ll meet you at Jekyll Plugins. If you want to skip the talk about structure and just want a simple example like I wanted when I started, we’ll see each other at The Final Code. And if you’re not interested in working with Jekyll, you can probably skip the post without missing anything.

Anyway, I’ve never quite been happy with how quotes looked in posts, so let’s start the story there.

The old quote style, shaded background with dark bars to the left and right

The style did the job, but it’s so…brutal, where just about everything else is relatively smooth. So, I decided to finally change it, using a shadow and some curves, instead of spending week after week saying “yeah, that could be better” like I’ve been doing.

…And that is when I realized that, when converting from a format with minimal semantic structure—like Markdown, which only headings, quotes, and tables—to a format with slightly more semantic structure (HTML), we sometimes hack our way into adding semantic structure in less-than-elegant ways. The big example for this blog has been the “pull quotes” that I (increasingly rarely) use in posts to call out the key ideas of a section. Because I only saw this as pure syntax, I identified pull quotes as nested quotes, creating the styles based on that assumption.

It was always an ugly solution, but it was better than the most obvious alternative, which was to hand-code the HTML for the pull quotes and drop that unreadable mess into the Markdown file. I already do this for some things, like embedding audio and video from off-site, so it wouldn’t be completely out of character.

Technical Debt

Because my pull quotes were quotes-inside-of-quotes, trying to change the appearance of quotes made a mess of those. The browser tried to render a quote inside a quote, exactly what I asked it to do, but not what I wanted, and basically useless.

That was annoying, but fine. This is what we call technical debt. My expedient solution saved me time then, with the expectation that the time would need to be “paid back” at some point in the future. That future was Monday, and that meant finally learning how Jekyll—the software that generates this blog—works, on some level.

If you’ll pardon the digression, the term is awkward, but “debt” is a great metaphor for so many aspects of life. Deciding to take “shortcuts” to get something done—speeding to get to work on time, saying something unkind to a friend to get out of a conversation, skipping an event where you’re expected, ignoring a health problem, or anything else—you’re implicitly accepting that you may need to pay that time back with interest. You might get pulled over immediately. The health condition might need more-aggressive treatment. You might need to work to regain your friend’s trust. And maybe, occasionally, your creditor forgets about you.

Even looking at things on a societal level, we have many problems today that are a result of making an expedient choice in the distant past—think of colonialism or slavery—and often outright refusing to pay that “societal technical debt” whenever the bill comes due again.

Alternatives

Back to styling quotes.

I already knew most of what I needed to know about Jekyll from working with it, really. Before Jekyll converts the Markdown files into HTML to be posted, it expands its own “inline tags,” plus any templates written in a simple language from Shopify called Liquid. I’ve used the templates before, letting me include the word count for a post that doesn’t need me to update it or even count, or listing all posts with a certain tag. I’ve also used Jekyll’s inline tags, for example referring to earlier posts by calling post_url, so that I don’t need to know the actual URL for a post.

Reviewing the Liquid documentation, templates weren’t going to help me. Instead, I needed a new tag—I call it pull—which will generate the HTML for the pull quote.

Jekyll Plugins

After poking around for a while—I don’t know why this isn’t just linked somewhere obvious on the Jekyll website—I found enough information to build a Jekyll plugin.

Structure

The bare minimum to know follows.

  • We write the plugins in Ruby.
  • Plugins sit in the _plugins folder.
  • When serving the “development” version of the blog locally, Jekyll isn’t interested in changes to plugins—though it definitely recognizes the changes and triggers a rebuild—so its server needs to be restarted to test a new idea.
  • A plugin class has two pieces—an initialize() method to handle any setup, and a render() method to return the generated HTML—and then must get attached to an inline tag name.

And that’s basically everything, so let’s break down how the new pull quotes work, at least as of yesterday, when I checked everything in.

Initialization

All things considered, this is the boring part.

def initialize(tag_name, text, parse_context)
  super
  @text = text
end

Like I said, it’s boring. It calls super, the initialization code that this is otherwise replacing for the class, and stores the input string—the text of the pull quote, in this case—in a global variable for use elsewhere in the object.

I’m admittedly not sure what parse_context is, even after poking at it and searching, so I’m ignoring it until I realize that I need it for something. You might say that I’m refinancing my technical debt, rather than just paying it. Maybe it’ll turn out to be the solution to letting me render Markdown from inside a tag. Or maybe it won’t.

Rendering

Modern HTML has an aside tag, meant for content that isn’t relevant to the main body of the page. That seems like the best bet, here. Not only does that describe most of the semantics of a pull quote, it has also been supported by every major browser since 2015, with most supporting it since 2011. If anybody is still using a browser that old, they’re doing it on purpose, so I’m not really concerned about them…

So, for the first pass, our rendering is simple.

def render(context)
  return "<aside class=\"pull-quote\">#{@text}</aside>"
end

I take the text and stuff it into an aside, and the CSS class .pull-quote gets all my old, broken “blockquote blockquote” pull quote styling.

The context parameter appears to hold information about the blog posts. Specifically, context.environments.first['page'] is a hash table that includes previous/next elements, the front-matter elements (title, date, and so forth), most other obvious metadata, and a content element. So, don’t try to print it, since it’ll try to render the content inside the post, which will recursively work until it crashes the process. In other words, the entire blog is accessible from the plugin, and that includes modifying elements, though I honestly can’t imagine many ways to make that useful.

Based on this Stack Overflow answer, it looks like context can also give me access to the Markdown-to-HTML converter, which would let me convert my pull quotes. Maybe I’ll look at that some other time.

Register

Once the class has been defined—initialization and rendering—we finally register the tag name as attached to the code.

Liquid::Template.register_tag('pull', PullInlineTag)

I assume that part is self-explanatory.

Power-Mad

That could be the entire story, but it isn’t. One unfortunate problem is that, as I hinted at in the Initialization section, we render the input text literally. Because of that, I need to clear the various Markdown bits from the quotes. And the times that I’ve used a pull quote in order to—more technical debt from avoiding semantics—shove a small image over to the right of the page now just show the Markdown string pointing to the image, so that won’t do.

Easily enough, in the latter case, I can just replace those fake pull quotes with a different tag, that I’ll unimaginatively but cryptically call imgr, for “image to the right.” That has the advantage that the dark red border will no longer be associated with those small images.

I won’t include that code, because it’s boring after seeing pull, except that an image takes multiple parameters, not just a single string. There’s the path to the image, the “alt” text that shows when the browser can’t display the image, and the “title” text that shows up as a “tooltip” when you hover over the image with the mouse. Normally, I set both texts to be the same, out of laziness, but I usually want alt to be descriptive to the extent that it’s relevant to the post—in case the image isn’t displayed—and either the same description or any smart-ass remark that I might occasionally come up with goes in alt.

However, Liquid’s inline tags do only take one blob of text—unless I missed something important—so I can just delimit the string with something that isn’t likely to be used in a pull quote or image caption, like the pipe (|) character, and call split '|' on the string to return the components.

As long as I’m doing that for imgr, though, I can do the same thing for pull, because there’s one additional problem with the old pull quotes: They always show up to the right of the page. In older posts where I used pull quotes more frequently, the consistency is acceptable, but I can do better, now. In this case, I can add an optional part of the input text that chooses whether to put the pull quote on the left- or right-hand side. So, let’s revisit the rendering code.

def render(context)
  text, place = params @text
  return "<aside class=\"pull-quote pull-quote-#{place}\">" \
    "#{text}</aside>"
end

def params(input)
  parts = input.split '|'
  return parts[0].strip, parts.length > 1 ? parts[1].strip : 'right'
end

The params() method separates out the text to render and the place, the side to drop the pull quote. In this case, if I don’t provide a position, the conditional code will default to 'right', which represents the behavior of the old pull quotes. The default saves me the trouble of going back through all the old posts a second time, to specify that everything should go on the right.

I can then hack out the side-dependent parts of my .pull-quote style, and create .pull-quote-right with those bits and .pull-quote-left with their opposites. And with that taken care of, I can now drop pull quotes on either side of the page, whenever I please, so that I can break up the visual layout more carefully. Or I can put them on both sides, in the case of this paragraph, which I already regret trying.

I’ve been driven wild with power, I guess. Mwah-ha-etc.

Anyway, I had to go through my old posts looking for nested quotes, to replace them with either a pull or imgr tag, and now I can stop wasting time with the hacked-together pull quotes.

Where Was I?

Oh, right, I did all this so that I can change the style of the normal quotes. And that is now possible, like so.

I’ve been driven wild with power, I guess.

Me, a few paragraphs up

I like the new style—it’s much clearer that they’re to be read as offset from the main text—so the work was probably worth it. It’s not perfect, mind you, but the new look is still better than the old look.

I’m using a heading for the credit, for example, which is definitely another legitimate semantic taboo, especially since the heading comes after the quote. So, I should probably eventually add a citation as a tag, generating a line with style that matches those headings; HTML has a cite element that would be ideal for it. I could also add a quote-with-citation tag, but that seems excessive.

The style also somehow interacts badly with the imgr tag. Maybe the pull quotes and normal quotes always had a conflict, and I just never noticed, because of the stark appearances and similar natures. Conveniently, the situation has only come up when I decided to add illustrations to a Twitter roundup post. Barely anybody ever refers to the old editions but me—I post the links for a reason, after all—as far as I can tell, so it’s not a high priority item to fix. Like I said, I’m refinancing some of this debt, not paying it off completely.

And it’s not a problem so much as a possible enhancement, but I’d like to find a way to push the pull quotes further down the height of the paragraph, so that they aren’t always sitting at the top. There’s probably some obvious CSS to do it, but it’ll take some research.

Regardless, notice how the new style for quotes does not affect the style for the pull quotes. They don’t have shadows, and the curvature of the box is less. Before pull quotes had their own semantic meaning, the pull quotes were placing empty quote boxes, in addition to overly styled pull quote boxes.

Note that I was tempted to fully unleash the semantic meaning of a pull quote, by structuring the HTML as follows.

<aside>
  <blockquote>
    The quoted text.
  </blockquote>
</aside>

That’s what a pull quote semantically is, after all, a quote meant as an aside. However, I quickly realized that was probably going to cause me more conflicts as I work to undo the existing quote styles. I may revisit this, however, if I decide to start using the aside tag for other purposes.

The Final Code

If you want to play with this idea and don’t feel like wandering around the Entropy Arbitrage Code repository to find it, here’s the final version of the plugin.

class PullInlineTag < Liquid::Tag
  def initialize(tag_name, text, parse_context)
    super
    @text = text
  end

  def render(context)
    text, place = params @text
    return "<aside class=\"pull-quote pull-quote-#{place}\">" \
      "#{text}</aside>"
  end

  def params(input)
    parts = input.split '|'
    return parts[0].strip, parts.length > 1 ? parts[1].strip : 'right'
  end
end

Liquid::Template.register_tag('pull', PullInlineTag)

About the only piece that I didn’t already mention is that the plugin is a subclass of Liquid::Tag.

Other Possibilities

Now that I have this process down, I can potentially take a look at other tags that have crossed my mind.

For example, readers might notice that, when I link to Twitter, I add Font Awesome’s Twitter icon to the link. Likewise, I have started trying to remember to indicate the article’s license at the other end of a link, if it’s not compatible with the blog. It might be nice to have a warn_url tag that lets me specify Twitter, non-commercial links, or anything else that might come up in the future, saving me the need to track down the code for the icons every time I need them.

Similarly, I’ve considered adding my “JC” icon to internal links, though that might be too cluttered. More useful whenever I embed video or a book from the Internet Archive, I need to look up the syntax and rescale everything, whereas a plugin can do that automatically.

It probably doesn’t fit with the flow of my posts, where links are just inline, but given that the plugins are written in Ruby, I could easily have one download each given URL to extract a title and preview. Bicker does something like this, though I believe that it relies on a gem. A typical use for this that might fit well with Monday’s developer diary posts would be a github and/or gitlab tag that generates a link and preview, so that I don’t need to constantly twist my writing to fit the name of the project into each update.

I don’t do anything with data visualization, right now, but if I do in the future, it wouldn’t be hard to write a plugin to generate a vector image of a bar, line, or pie graph, given some input points. Likewise, I don’t think that I’d use such a feature, but as the audience slowly grows, generating an HTML form and adding some back-end code elsewhere would make it possible to run polls.

A possible alternative to or expansion of the original pull quote idea comes to mind, too. It’s not terrible, but the current (and prior) process is a little clumsy, in that I need to find a quote to pull out, copy it into the tag, and remember to update it, if the source paragraph ever changes. It might be possible to wrap the entire paragraph in a Liquid::Block tag, and add delimiters to the paragraph to mark the text to be pulled out, so that it only gets typed once. On the other hand, that wouldn’t allow for editing.

It doesn’t come up often, but I occasionally want to post some code that includes a Liquid tag. Unfortunately, Liquid processes first, meaning that, if I want to explain that “I can insert the page title by typing Writing Jekyll Plugins”—I didn’t type the title, there—I unfortunately get the title, instead of page.title inside double-braces ({ { ... } }, minus the spaces); it gets transformed before Markdown does. The problem where the contents don’t get processed could be useful, here, by letting the Liquid code pass through.

In other words, the ability to create new tags should make it possible to do pretty much anything, but do it in a way that’s semantically appropriate and doesn’t make the Markdown versions of the post significantly less readable.

This work seems to have created one minor problem, though: Now that I’ve added what I would call “trivial” plugins, it has nearly doubled the time that Jekyll takes to process and generate the updated files. I’ll need to see what’s going on with that and how to speed it up, at some point. It’s not exactly a disaster, but it’s a nuisance that I’d rather not deal with…

Anyway, for the handful of people still coming to or working with Jekyll, I hope that this gives you a start on creating plugins. I may pick up some of the ideas I listed and run with them, or this might be plenty for me. But I’m at least glad that I won’t—once I handle the citation tag—try to fake my way through semantic organization after this.

I should also mention—since I’m talking about the blog—that Jekyll has been a shockingly good experience, especially once I automated some of the more tedious or intricate tasks away. Every once in a while, it looks like I get the date in the URL wrong, but I have no plans of replacing it with another static site generator or blogging platform anytime in the foreseeable future.


Credits: The header image is If a door know needs a label… well its function is not clear by Alan Levine, made available under the terms of the Creative Commons Attribution 2.0 Generic license.


No webmentions were found.

By commenting, you agree to follow the blog's Code of Conduct and that your comment is released under the same license as the rest of the blog. Or do you not like comments sections? Continue the conversation in the #entropy-arbitrage chatroom on Matrix…

 Tags:   programming   blog   techtips   ruby

Sign up for My Newsletter!

Get monthly * updates on Entropy Arbitrage posts, additional reading of interest, thoughts that are too short/personal/trivial for a full post, and previews of upcoming projects, delivered right to your inbox. I won’t share your information or use it for anything else. But you might get an occasional discount on upcoming services.
Or… Mailchimp 🐒 seems less trustworthy every month, so you might prefer to head to my Buy Me a Coffee ☕ page and follow me there, which will get you the newsletter three days after Mailchimp, for now. Members receive previews, if you feel so inclined.
Email Format
* Each issue of the newsletter is released on the Saturday of the Sunday-to-Saturday week including the last day of the month.
Can’t decide? You can read previous issues to see what you’ll get.