Life is a Mystery
photo of the woods

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

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.