Introducing "needs": a Jekyll recipe for dependency-aware javascript inclusion

When I first moved a site from WordPress to Jekyll, one things I missed most was the way add-ons to WordPress could register JavaScript scripts so that they were included on the page with dependencies account for. For example, if I wrote a script that required jQuery, I could be assured jQuery was loaded before my script.

In WordPress this is accomplished via the wp_enqueue_script() function. This function inclues a way to declare other dependencies so that script tags are output in the right order to the rendered page.

I wanted something similar for Jekyll, and if possible I wanted to implement it without requiring any plugins. This page describes the needs.yml mechanism I created and shows how it can be used. I am especially pleased that I was able to accomplish this in Jekyll without a plugin. This recipe only uses YML and Liquid, so it should work anywhere you can use Jekyll.

The Jekyll recipe requires three files:

  • the _data/needs.yml file holds a structured list of all the scripts known to my Jekyll installation and their relationships to one another;
  • the _includes/walk_needs.html file holds Liquid scripting to start a walk of the needs on a given page and describes how a need should be rendered to the page;
  • the _includes/walk_needs_recursion.html file holds Liquid scripting to help recursively walk the needs described in the needs.yml file.

If you try to follow this recipe you will probably not have to change anything in either of the included HTML files, but let me describe how to use the YML file and call this all from your own layouts.

needs.yml

The _data/needs.yml file is the heart of this recipe. While this might have been included as an element of the _config.yml file, I decided to create a separate file because the needs get pretty wordy and felt like clutter in the config file.

The file contains a series of entries, each one beginning with an identifier (a name) you supply for a given need. These are the same identifiers that you will use when defining relationships with other needs or declaring the need on a given page’s YML frontmatter.

Under that identfier you can have a number of elements, all of which are optional:

  • needs is an array of other needs upon which this need is dependent;
  • javascript is either the URL of a script on the web somewhere or the filename of a script in your local Jekyll implmentation;
  • id is an identifier which should be associated with the <script> tag that loads this script;
  • async is a boolean which you can set to true if you want the <script> tag to specify ansyncronous loading of the javascript.

For example, if I want to use Disqus to add the option for commenting to some of my pages, but I also want to hide the Disqus elements of the page until the user clicks on a comment button, I might include the following in my needs.yml file:

comments:
    javascript: /js/disqus.js
    needs:
        - jquery
        - disqus_embed
        - disqus_count
disqus_embed:
    javascript: "//shortname.disqus.com/embed.js"
    async: true
disqus_count:
    javascript: "//shortname.disqus.com/count.js"
    id: dsq-count-scr
    async: true

This indictes that if I declare that a page needs comments, then I need to load jQuery and a couple scripts from Disqus before I load my own /js/disqus.js script to handle the revealing of the Disqus elements of the page. Althought it has no bearing on the recipe for needs, here is my /js/disqus.js script for completness:

jQuery(document).ready(function($) {
    $("#comment-button").click(function() {
        $("#comment-button").slideUp(400, function() {
            $("#disqus_thread").slideDown(800);
        }); 
    });
});

On pages and posts

To include the need on a page or post (or in any other collection item you may have), you just need to add a reference to it in the YML frontmatter. For example, if I want to include the Disqus comments defined above I would simply add this code to the YML frontmatter of a page:

needs:
    - comments

In layouts

In order for the needs to be rendered on the page, the Jekyll layout for a given page must have a bit of Liquid added. In my case I include this code just before the closing </body> tag on a defaults.html layout that is the basis for all my other layouts.

{% include walk_needs.html needs = page.needs %}

That’s all that is required to get the JavaScript loaded, but in the case of Disqus there is also a bit of HTML and page-specifc JavaScript that must be included. To manage this I have elsewhere on the layout included:

{% if page.needs contains "comments" %}
    <script>
        var disqus_config = function () {
            this.page.url = "{{ site.url }}{{ page.url }}";
            this.page.identifier = "{{ site.url }}{{ page.url }}";
            this.page.title = "{{ page.title | replace: '"', '\"' }}";
        };
    </script>
    <div id="comment-button" class="disqus-comment-count button" data-disqus-identifier="{{ site.url }}{{ page.url }}">Be the first to comment</div>
    <div id="disqus_thread"></div>
{% endif %}

The includes

While you don’t have to change these files, you will need to know what is in them!

_include/walk_needs.html:

{% capture list %}{% include walk_needs_recursion.html needs=include.needs %}{% endcapture %}
{% assign needs = list | strip_newlines | replace: ' ', '' | split: ',' | uniq %}
{% for need in needs %}
    {% assign printing = site.data.needs[need] %}
    {% assign javascript = printing.javascript %}
    {% assign id = printing.id %}
    {% if javascript %}
        <script {% if id %}id="{{ id }}"{% endif %} src="{{ javascript | reurl }}" {% if printing.async %}async{% endif %}></script>
    {% endif %}
{% endfor %}

_include/walk_needs_recursion.html:

{% for need in include.needs %}
    {% assign testing_need = site.data.needs[need] %}
    {% assign dependencies = testing_need.needs %}
    {% if dependencies %}
        {% include walk_needs_recursion.html needs = dependencies %}
    {% endif %}
{{ need }},
{% endfor %}

Bottom line

So all this boils down to the fact that I can include various features only on pages where I need them by including some needs YML in their frontmatter. I don’t have to worry about dependencies or special inclusions at that point. I also have a simple mechanism for documenting the JavaScript used on my site and its dependencies on other JavaScript tools or libraries in the _data/needs.yml file.

This has saved a lot of headaches and I like the result even more than the WordPress wp_enqueue_script that inspired it. I’d love comments if you see things I could improve.

In the future I plan to enhance the _include/walk_needs.html code so that it can accomodate CSS and JavaScript to rendered in the header as well as the current JavaScript at the end of the page. I plan to do that by introducing a position prameter to the call and a css element to the _data/needs.yml. Suggestions welcome.

Be the first to comment