Make developing a website easier
Jekyll enhancements for development
Jekyll is nice if you like the defaults, but as soon as you start customizing you run into walls. Here are some tips to make it less painful.
Table of Contents
Continous Development
I’m sure you’ve met this line if you started jekyll serve at least once:
Regenerating: 1 file(s) changed at 2015-06-09 12:34:56 ... done in 3.14 seconds. output from watcher.rb in jekyll-watch gem
Now, if you’re like me, you immediate ask the question: What are those files?
, especially when it says multiple files changed and you only changed one.
removed.each { |file| Jekyll.logger.info("Changes:", "Removed #{file.slice(site.source.length + 1, file.length)}"); }
added.each { |file| Jekyll.logger.info("Changes:", "Added #{file.slice(site.source.length + 1, file.length)}"); }
modified.each { |file| Jekyll.logger.info("Changes:", "Modified #{file.slice(site.source.length + 1, file.length)}"); }
print Jekyll.logger.message("Regenerating:", "#{n} file#{n>1?"s":""} changed at #{t.strftime("%Y-%m-%d %H:%M:%S")} ")
...
Changes: Added _posts/2015-06-09-Jekyll-hacks-for-development
Regenerating: 1 file changed at 2015-06-09 15:18:56 ...done in 3.220606 seconds.
Changes: Removed _posts/2015-06-09-Jekyll-hacks-for-development
Changes: Added _posts/2015-06-09-Jekyll-hacks-for-development.md
Regenerating: 2 files changed at 2015-06-09 15:19:00 ...done in 3.286006 seconds.
Changes: Modified _sass/_09_elements.scss
Regenerating: 1 file changed at 2015-06-09 15:20:27 ...done in 3.239607 seconds.
Changes: Modified _posts/2015-06-09-Jekyll-hacks-for-development.md
Regenerating: 1 file changed at 2015-06-09 15:20:53 ...done in 3.246334 seconds.
The paths are relative to Auto-regeneration: enabled for '...'.
Debugging Liquid
When copy-pasting-building a site from articles found on the internet you’ll most likely find yourself wondering: Why is it not working?
. There’s a built-in | inspect
, but it just dumps the text without any escaping or formatting, messing up HTML pages. Luckily Jade Dominguez included a little plugin in his bootstrap which would come to the rescue in this case. The following is a more advanced version of his plugin.
This is compatible with GitHub Pages! The default mode of Jekyll is unsafe, but GitHub starts it with --safe
. So plugins can be used during local development; just don’t forget to remove | debug
s before commit.
# A simple way to inspect liquid template variables.
# Based on: https://github.com/plusjade/jekyll-bootstrap/blob/master/_plugins/debug.rb
# The filters below can be used anywhere liquid syntax is parsed (templates, includes, posts/pages/collections)
require 'pp' # obj.pretty_inspect
require 'cgi' # CGI.escape_html
module Jekyll
# Need to overwrite the inspect methods, because the original uses a strange format
# and we're trying to output JSON. <> also conflicts with HTML code if output literally.
class Post
# Replace original #<Jekyll:Post @id="self.id">
def inspect
"{ \"type\": \"Jekyll:Post\", \"id\": #{self.id.inspect} }"
end
end
class Page
# Replace original #<Jekyll:Page @name="self.name">
def inspect
"{ \"type\": \"Jekyll:Page\", \"name\": #{self.name.inspect} }"
end
end
module DebugFilter
# Returns a highlighted HTML code block displaying the received object.
# Example usages:
# * <tt>{{ site.pages | debug }}</tt>
# * <tt>{{ site.pages | debug: 'pages' }}</tt>
def debug(obj, label = nil)
pretty = obj.pretty_inspect
pretty = pretty.gsub(/\=\>/, ': ') # approximate JSON syntax
pretty = "#{prefix(obj, label)}\n#{pretty}" # prefix with type
highlight = Jekyll::Tags::HighlightBlock.new('highlight', 'json', [ pretty, "{% endhighlight %}" ])
pretty = highlight.render_pygments(pretty, true)
pretty = highlight.add_code_tag(pretty)
pretty = pretty.sub(/<div class="highlight">/, "<div class=\"highlight debug\" title=\"#{prefix(obj, label)}\">")
return pretty
end
# Returns a non-highlighted HTML code block displaying the received object.
# Example usages:
# * <tt>{{ site.pages | dump_html }}</tt>
# * <tt>{{ site.pages | dump_html: 'pages' }}</tt>
def dump_html(obj, label = nil)
pretty = obj.pretty_inspect
pretty = CGI.escape_html(pretty)
pretty = "#{prefix(obj, label)}\n#{pretty}" # prefix with type
pretty = "<pre class=\"debug\" title=\"#{prefix(obj, label)}\">#{pretty}</pre>"
return pretty
end
# Returns pretty-printed plain text displaying the received object.
# Example usages:
# * <tt>{{ site.pages | dump_text }}</tt>
# * <tt>{{ site.pages | dump_text: 'pages' }}</tt>
def dump_text(obj, label = nil)
pretty = obj.pretty_inspect
return "#{prefix(obj, label)}#{pretty.strip}"
end
# Prints pretty-printed plain text displaying the received object to the console.
# Returns the original object, making it chainable.
# Example usages:
# * <tt>{% assign upperTitle = page.title | dump_console | upcase | dump_console %}</tt>
# * <tt>{% assign upperTitle = page.title | dump_console: 'original' | upcase | dump_console: 'upcased' %}</tt>
def dump_console(obj, label = nil)
pretty = obj.pretty_inspect
puts "#{prefix(obj, label)}#{pretty.strip}"
return obj
end
private
def prefix(obj, label)
clazz = "(#{obj.class})" if obj
label = "#{label}: " if label
return "#{label}#{clazz}"
end
end # DebugFilter
end # Jekyll
Liquid::Template.register_filter(Jekyll::DebugFilter)
Debugging Example Walkthrough
Suppose there’s a tags data file in the format I suggested:
tag1:
name: "Tag 1 Long Name"
tag2:
name: "Tag 2 Long Name"
… and you’re trying to list all the tags on the site, like LovesTha did:
<ul>
{% for tag in site.data.tags %}
<li><a href='/blog/tag/{{ tag }}/'>{{ tag.name }}</a></li>
{% endfor %}
</ul>
<ul>
<li><a href='/blog/tag/tag1{"name"=>"Tag 1 Long Name"}/'></a></li>
<li><a href='/blog/tag/tag2{"name"=>"Tag 2 Long Name"}/'></a></li>
</ul>
For some reason {{tag}}
comes up as tag1{"name"=>"Tag 1 Long Name"} instead of the expected tag1 and {{tag.name}}
is empty. Let’s augment the code to see what’s going wrong (the argument to debug
and dump_*
is optional):
{{ site.data.tags | debug: "data.tags" }}
<ul>
{% for tag in site.data.tags %}
<li><a href='/blog/tag/{{ tag }}/'>{{ tag.name | dump_text }}</a>{{ tag | dump_html: forloop.index }}</li>
{% endfor %}
</ul>
… and here’s how it looks like on the page:
data.tags:(Hash)
{"tag1": {"name": "Tag 1 Long Name"}, "tag2": {"name": "Tag 2 Long Name"}}
From the above the following is revealed:
- the iteration was correctly going through the Hash (map) as expected
{{tag.name}}
is nil displayed as an empty string- at each iteration tag is an array and not the key of the hash
In Liquid a hash entry is stored as [key, value]; compare to for..in loop in JavaScript.
Based on the above it’s clear that the code must be changed to correctly read the value from the entry:
{% for tag_entry in site.data.tags %}
{% assign tag_key = tag_entry[0] %}
{% assign tag_data = tag_entry[1] %}
<li><a href="/blog/tag/{{ tag_key }}/">{{ tag_data.name }}</a></li>
{% endfor %}
More Debugging options
In case you’re generating something other than HTML, dump_text
and dump_console
come in handy.
Different languages have different rules, make sure you escape everything accordingly.
For example XML comments cannot contain -- so it’s required to strip (| remove: '--'
) or collapse them (| replace: '--', '-'
) to make parsers happy.
<url>
<!-- {{ link | dump_text | remove: '--' }} -->
<loc>{{ site.url }}{{ site.baseurl }}{{ link.url | dump_console: "original" | remove: 'index.html' | dump_console: "stripped" }}</loc>
...
</url>
<url>
<!-- (Hash){...
"dir"=>"/blog",
"name"=>"index.html",
"path"=>"blog/index.html",
"url"=>"/blog/index.html"} -->
<loc>http://localhost:4000/dev/blog/</loc>
Regenerating: 1 file changed at 2015-07-29 14:16:06
original: (String)"/blog/index.html"
stripped: (String)"/blog/"
original: (String)"/blog/tags/"
stripped: (String)"/blog/tags/"
...
It’s also worth looking at Octopress Debugger which offers different features.
Hidden Gems
Filter Documentation
Not all the filters documented are available when using an older version of either Jekyll or Liquid. The most relevant documentation and list of filters can be found in your local Ruby installation:
- Jekyll: lib/ruby/gems/2.2.0/gems/jekyll-2.4.0/lib/jekyll/filters.rb
- Liquid lib/ruby/gems/2.2.0/gems/liquid-2.6.2/lib/liquid/standardfilters.rb
Working With Arrays
By default Liquid doesn’t have array manipulation, but our friends at Jekyll were kind enough to implement it, and we can even create new arrays with the split
trick.
{% assign r = "," | split: "," | dump_console: "new array" %}
{% assign r = r | push: "c" | dump_console: "insert last" %}
{% assign r = r | push: "d" | dump_console: "insert last" %}
{% assign r = r | unshift: "b" | dump_console: "insert first" %}
{% assign r = r | unshift: "a" | dump_console: "insert first" %}
{% assign r = r | shift: 2 | dump_console: "remove first 2" %}
{% assign r = r | pop: 1 | dump_console: "remove last 1" %}
{% assign r = r | unshift: "z" | dump_console: "insert first" %}
{% assign r = r | push: "a" | dump_console: "insert last" %}
{% assign r = r | sort | dump_console: "order by contents" %}
insert last: (Array)["c"]
insert last: (Array)["c", "d"]
insert first: (Array)["b", "c", "d"]
insert first: (Array)["a", "b", "c", "d"]
remove first 2: (Array)["c", "d"]
remove last 1: (Array)["c"]
insert first: (Array)["z", "c"]
insert last: (Array)["z", "c", "a"]
order by contents: (Array)["a", "c", "z"]
Liquid code highlight
Luckily the page’s code is first run through Liquid and then kramdown, so it’s possible to use liquid tags to generate the markdown code. Based on this it’s also possible to nest them weirdly, notice that the ```
pair is interleaving with the raw
/endraw
tags; I prefer this because the markdown indentation is natural, only the end of the line is a little different.
```liquid{% raw %}
{% assign var = other_var | sort | split: ' ' %}
```{% endraw %}
{: title="caption of code"}
Liquid Error Messages
Maybe https://saimonmoore.com/tumblog/200612/debugging-liquid-templates.html can help with this.
Among other modifications made to my tags display, I wanted to sort tags case insensitively and I got the following error message:
Liquid Exception: no implicit conversion from nil to integer in tags.html
Not knowing which line gave the error, I tried to fix and remove parts of the file which were dealing with numbers like 0 < post.size. After many minutes of trying I found out that the above line gives the message and the problem is that I missed the quotes on downcase
: quotes are required for map as method are called via reflection.
{% assign tag_words = site_tags | split: ',' | map: downcase | sort %}