Using WebC In 11ty

This week I decided to make a concerted effort to add WebC to some of my 11ty projects (not, ironically, this one ...)

What?

WebC is one (of several) templating languages that is supported in 11ty, and is actually the brainchild of Zach Leatherman, the developer behind 11ty.

Why?

The key attraction of WebC over my preferred Nunjucks is that it enables easy bundling of JavaScript and CSS, alongside HTML — which means:

  • JS and CSS will only be built on a page if it uses the relevant WebC component; and
  • WebC components can easily be re-used between projects, in theory without needing to copy across separate JS, CSS and any 11ty shortcodes/filters that are referenced

What's the catch?

The drawbacks of WebC are that it's (at least to my eye) a little less elegant than Nunjucks; I don't think it's as flexible as Nunjucks; and (at the moment at least) the documentation is quite sparse and there's not that many articles/posts etc out on the internet when you get stuck in the minutiae of implementation.

Getting started

WebC isn't included in the base bundle of 11ty (at least with 11ty 2.x, don't know whether that will change when 3.0 is released). That means there's a few hoops to jump through

1. Add a dependency

Install the plugin:

npm install @11ty/eleventy-plugin-webc

2. Add configuration

Make some changes to the eleventy.js config file:

// eleventy.js

// add a new import
const pluginWebc = require("@11ty/eleventy-plugin-webc");

// add config inside module.exports
module.exports = function(eleventyConfig) {

    ...

    // add the plugin and tell 11ty where to look for components ...
	eleventyConfig.addPlugin(pluginWebc, {
        components: [
            // assuming that you are keeping webc components in src/_includes/components
            "src/_includes/components/**/*.webc",
            // if you're using the eleventy-image plugin and want the <eleventy-img> component
            "npm:@11ty/eleventy-img/*.webc",
        ]
    });

    ...
}

With that configuration, basic WebC should be working.

What have I learnt?

1. WebC first ...

The 11ty documentation shows how to use a plugin to render WebC inside other templating languages.

Don't.

I've had lots of problems trying to transfer data from Nunjucks into a WebC components, and my considered opinion is that it's just too hard. Start with WebC (for the layout as well as the page).

2. ... other templates second

Which isn't to say we can't use other templating languages, just that I'd recommend rendering them inside WebC rather than the other way around. That's definitely how it's gone for me when trying to mix WebC and Nunjucks.

A simple example of how to do this is in the <head> tag of my layout. Originally the layout was written in Nunjucks to generate both the <title> and (if appropriate) <meta> tags:

// src/_includes/layouts/base.njk - the old way

---
title: Default
description: Default description
metacontent: []
---

    ...

    {# allow for additional meta tags, defined in a pages frontmatter as
       metacontent; this will be an array and inside it will be objects of shape
       {type: str, content: str} #}

    {% for item in metacontent %}
        <meta name="{{ item.type }}" content="{{ item.content }}">
    {% endfor %}

    {# title will be whatever is defined in a pages frontmatter, appended with the
       website name #}

    <title>{{ title }} | Name of website</title>

    ...

Now the layout is in WebC and wraps the Nunjucks elements as follows:

 <!-- src/_includes/layouts/base.webc - the new way -->

---
title: Default
description: Default description
metacontent: []
---

    ...

    <!-- wrap the old nunjucks code in a template specifying it should be rendered
         as nunjucks -->
    <template webc:type="11ty" 11ty:type="njk">
        {% for item in metacontent %}
            <meta name="{{ item.type }}" content="{{ item.content }}">
        {% endfor %}
    </template>

    <!-- and again -->
    <template webc:type="11ty" 11ty:type="njk">
        <title>{{ title }} | Name of website</title>
    </template>

    ...

Nothing more required, nice and simple (note that the data here is all contained in the frontmatter and cascaded in from individual pages ...)

3. Passing complex data is the gotcha

My main reason for using WebC is so that the relevant JS and CSS will only be bundled into a page when required, reducing unnecessary bloat, with a helping of "code reusability" on the side.

A lot of the components that warrant this treatment are going to be partly or entirely data-driven. For instance, I have a website that shows testimonials on the homepage — these are driven by a file src/_data/testimonials.js which contains an array of testimonial objects and randomly returns two of them to show on the page each time it's built.

In Nunjucks using this data is simple; we can iterate over the objects in testimonials (no further code needed to make it available) and properties/key-value pairs are available to use at will.

    {% for item in testimonials %}
        {# some Nunjucks code to use properties of the item, eg #}
        <h3>{{ item.name }}</h3>
    {% endfor %}

In WebC that's a little different; every example that I found referred either to data set in frontmatter, or data defined within the WebC component. After a LOT of scratching around (with thanks to vrugtehagel and trovster on the 11ty Discord, and really wishing that I'd found this post much earlier than I did) I've determined that the following three options work:

    <!-- OPTION ONE: iterate through code with just WebC -->
    <div webc:for="(index,item) in $data.testimonials">
        <!-- do something in here, for instance use item.name, in WebC -->
        <h3 @text="item.name"></h3>
    </div>

    <!-- OPTION TWO: iterate through code with WebC but pass to Nunjucks -->
     <div webc:for="(index,item) in $data.testimonials">
        <template webc:type="11ty" 11ty:type="njk">
            {# do something in here using Nunjucks; item will be available
               inside the template; eg #}
            <h3>{{ item.name }}</h3>
        </template>
    </div>

    <!-- OPTION THREE: pass data into a prop and use that within Nunjucks (iterate in Nunjucks) -->
     <div :testimonials="$data.testimonials">
        <template webc:type="11ty" 11ty:type="njk">
            {# do something in here using Nunjucks; testimonials (the array) will
               be available inside the template to iterate over; eg #}
            {% for item in testimonials %}
                <h3>{{ item.name }}</h3>
            {% endfor %}

        </template>
    </div>

Those aren't all strictly equivalent because the order of outside div's changes — that's not the point!

It may seem a bit odd messing around wrapping Nunjucks inside WebC, but as I said at the start — Nunjucks is more flexible than WebC (or maybe I just know it better) and so long as I can bundle any CSS or JS associated with the component by using the WebC wrapper, I'm still getting benefits from WebC.

(Also, to be clear, I am writing some of the elements purely in WebC!)