Skip to content

Instantly share code, notes, and snippets.

@imathis
Last active December 17, 2015 07:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save imathis/29750372b55cd164e367 to your computer and use it in GitHub Desktop.
Save imathis/29750372b55cd164e367 to your computer and use it in GitHub Desktop.
New tags in Octopress

New Liquid Tags for Octopress

Writing and customizing themes for Jekyll and Octopress is about to get a lot easier. This post will probably be most interesting to those who've written Liquid templating code or customized an Octopress theme. The coolest new Liquid tag is probably include. If you don't have time to read this whole post, at least jump down and check that out.

In this post first I'll illustrate some of the shortcomings of today's Liquid with a simple example. Then I'll proceed to demonstrate all the awesome stuff you'll be able to do with the new Liquid tags in Octopress 3.0.

You can dig around in the new Liquid code over here and watch the new classic theme shaping up here.

Using Liquid Today

Right now it's really challenging to create a flexible framework on top of Liquid templates.

For example to display the date for a post, this is what I'm doing with today's Liquid.

{% capture date %}{{ post.date }}{{ page.date }}{% endcapture %}
{% capture has_date %}{{ date | size }}{% endcapture %}

{% if has_date != '0' %}
  {% capture date %}
    <time datetime="{{ date | datetime | date_to_xmlschema }}" pubdate>{{ date | format_date, site.date_format }}</time>
  {% endcapture %}
{% endif %}

In the first line, I know that either post.date or page.date will render as an empty string so I capture both to save my self a step. Then because some pages won't have a date, I capture the size of the date string to a variable has_date. Next I do a string comparison and if the date isn't '0' I can finally capture the <time> tag and formatted date. Gross right?

After all that if I want to add a label to the date or wrap it in a paragraph I do this.

{% unless date == '' %}
  <p class='post-meta'>Published: {{ date }}</p>
{% endunless %}

I know that date will either be a <time> tag, or if the page has no date, it will be an empty string. Yuck.

Liquid has an assign tag which allows you to set the value of a variable directly (without using a capture block) but it only works with strings.

{% assign date = post.date %} # yields "post.date"

Unsatisfied with these tools, I decided to rewrite them. Here's what I came up with.

The New Assign Tag

The new assign tag is fully backwards compatible and it does some neat new tricks.

Assign strings or variables.

{% assign first = Ender %}     # "Ender"
{% assign last = "Wiggin" %}   # "Wiggin"
{% assign date = page.date %}  # "2013-04-27T22:56:58-05:00"
{% assign index = false %}     # false

The first example works like assign has always worked assigning the string "Ender" to the variable first. If you want to be unambiguous, simply quote your strings like the second example.

The astute observer may be wondering what will happen if Ender is a variable and why in the third example date isn't set to the string "page.date". Here's the catch. In order to be backwards compatible with the old assign tag, the new tag checks the context for the variable Ender, assigning its value if it is found. If there is no variable, the string "Ender" is returned instead. As it follows, in the third example, if the variable page.date cannot be found, date will be set to the string "page.date". In this case, that's definitely not what we want.

To resolve that unwanted case, I've added the option to do cascading assignment with the double pipe operator.

Cascading Assignment

Using the double pipe ||, we can assign a variable to the first truthy value. If none are truthy, the variable will be assigned to the value in the chain. Words like true, false and nil will behave as you'd expect. Have a look.

{% assign date = post.date || page.date || nil %}

In this case, date will be set to the value of the first real variable, reading from left to right. If no variable can be found, date will be set to nil.

Conditional assignment

The new assign tag can evaluate ternary expressions as well as post if and unless conditions.

{% assign date = (post ? post.date : page.date) || nil %}
{% assign author = page.author if page.author? %}
{% assign sidebar = page.sidebar unless page.sidebar == false %}

Well that's more like it. You can get really funky by combining ternary, || assignment and post conditionals, but I hope you'll never need to.

The New Return Tag

This is a brand new tag which functions exactly like the assign tag, but instead of assigning a variable, it simply returns the value as rendered text in the template. It looks like this.

{% return date if date %}
{% return marker_before || '' %}
{% return (post ? post.url : page.url) %}

It's nice to be able to avoid extra conditional blocks for simple things like this.

The New Capture Tag

The new capture block is backwards compatible, but it can use ternary expressions and post conditional logic too.

{% capture (position == 'before' ? mark_before : mark_after) if marker %}
  <span class='post-marker'>{{ marker }}</span>
{% endcapture %}

This rather involved example uses a ternary expression to choose which variable to set, mark_before or mark_after, then it uses a post condition, only capturing if a marker variable is present.

Revisiting the date example at the beginning of this post, I can now do this.

{% assign date = (post ? post.date : page.date) || nil %}
{% capture date if date %}
  <time datetime="{{ date | datetime | date_to_xmlschema }}" pubdate>{{ date | format_date, site.date_format }}</time>
{% endcapture %}

Now when I want to render the date in a template, I no longer have to compare a string.

{% if date %}<p class='post-meta'>Published: {{ date }}</p>{% endif %}

Now that I don't have to do unnecessary captures and string comparison, my liquid templates are simpler and more clearly express my intent.

The New Include Tag

This is probably the most useful of the new tags. The include tag can do everything you've seen so far and more. Here is what it looks like to use cascading with the include tag.

{% include custom/header.html || theme/header.html %}

This tag searches the Jekyll <source>/_includes directory for custom/header.html if it isn't found it looks for and renders theme/header.html. As a result in Octopress 3.0 it will be very easy for Octopress users to override a theme's templates without modifying them.

Ternary expressions and post conditionals work too; making general templating logic much simpler.

{% include (post ? theme/post.html : theme/page.html) %}
{% include custom/comments.html || theme/comments.html unless page.comments == false %}
{% include (post ? custom/post.html || theme/post.html : custom/page.html || theme/page.html) %}

In the new include tag we can even use variable names instead of referring to a template by name.

{% include page.sidebar || site.sidebar.default %}

Now users can use page and site YAML configuration to control which templates are included when the site is rendered. In this case though, if there is no template defined, Liquid will try to load <source>/_includes/site.sidebar.default which will through an error as it probably doesn't exist.

To allow the include tag to fail gracefully, we can simply end our cascade with none.

{% include some/template.html || none %}

If Jekyll can't find _includes/some/template.html it will silently fail instead of inserting the typical "Include file not found" message.

New Wrap Tag

The new wrap tag works just like the include, but it's a block which allows you to wrap an included template file with some html. This is helpful because it allows you to use post conditionals instead of wrapping include tags with conditional blocks.

{% wrap custom/sharing.html || theme/sharing.html unless page.sharing == false %}
<div class="sharing">{= yield }</div>
{% endwrap %}

The faux Liquid tag, {= yield } tells the wrap tag where to insert the template.


I hope you enjoyed this preview of things to come. All of the above is currently working and I've just finished refactoring the classic theme to use all of the new goodies. I still have some work to do before I have a version of this which you can test, but in the mean time, if you're writing themes for Octopress I hope this gets you excited for the future and I'd love to know what you think.

We have a new mailing list where you can discuss this or you can ping @octopress on App.net or Twitter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment