Log in to GraphQL EditorGet started
Eleventy components woes

Michał Tyszkiewicz

10/3/2023

Eleventy components woes

So far so good, in part one we managed to do a very basic setup of a blog website sticking to just markdown and html (with a little bit of Nunjucks admittedly). If that's all you wanted from Eleventy then you're good to go and you'll find a lot of possible improvements and additions if needed. If you wanted to build a more complex website, well... this is where things get a bit tougher as modern websites use components and for Eleventy those are a bit of a touchy subject.

Components

Components are used almost universally now so it's hard to imagine web dev without them. At first glance everything looks really promising - not only does Eleventy support components but since it will compile them to static html and CSS we're getting a true server-side component system! While that sounds great in theory, sadly things aren't as simple as that. To get to know why first let's look at the numerous methods of creating components in Eleventy:

  • Liquid Render/Include
  • Nunjucks Include & Macro
  • Eleventy Render Plugin
  • Eleventy Vue Plugin (soon to be archived)
  • Eleventy Shortcodes
  • WebC Plugin (new!)

Templating

We've done a bit of that already with collections and this approach follows the same logic. Since we already have our blog posts now we need a bit of navigation, let's add a go-back button for users, letting them return to the index page after they're done reading a post. For that we need a button component in our _includes folder which will have typical button props like label and URL.

<a href="/">Go back</a>

Now we need to include it in our layout template:

{% include "button.html" %}

That works, but it isn't really fair, is it? We simply imported a static piece of html. If we want a real button component it should have props and to do that with Eleventy we need to use Nunjucks and create a button.njk component with those props:

<a href="{{ url }}">{{ label }}</a>

with that we can go to our template and specify what that url and label for the included button will be:

{% set url = "/" %}
{% set label = "Go Back" %}
{% include "button.njk" %}

No thanks

So we managed to create the simplest button component with props. The downside is that not only is this very similar to React and importing components - it's also more boilerplate compared to something like a simple import statement and <Button href="/">Go Back</Button>. Just to hammer this point home, this button was the simplest example. Let's imagine some standard website component which will have not only a button but also localized text, which will vary depending on which language the user selects and styling which will vary if a different theme is selected.

Liquid includes/render works basically the same - you can include a global reusable component like button.liquid in your _includes folder and render it in a template or page as {% render 'button' %}. It does have additional options but since we're looking for simplicity and this method has already soured us on that, I won't be going over them in more detail here.

Similarly there's also an option of using Nunjucks Macros for larger components like a pricing card:

{% macro pricingCard(title, description, price) %}
  <div class="pricing-card">
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
    <span class="price">{{ price }}</span>
  </div>
{% endmacro %}
{% import "pricingCard.njk" as pricingCard %}

{% block content %}
  <div class="container">
    {{ pricingCard.pricingCard('Basic Plan', 'Includes essential features', '$1.99/month') }}
  </div>
{% endblock %}

The issue with all these methods is they are doing something similar to React, but in a more complicated way. What about conditional props which in React are as simple as a single ?, well for those you'll have to do even more javascript boilerplate and use if/else conditions. It's pretty apparent that neither of these approaches fits our simplicity requirement, so that's 0-2 for Eleventy and let’s proceed to the others.

Custom Template Shortcodes

The Render Plugin I mentioned before is a little addon which gives you two shortcodes for quick and easy use, so let’s go over shortcodes in general. You might be wondering why there’s a plugin with just two shortcodes when Eleventy supports them out of the box. The answer is very simple - adding them is even more of a hassle and even more boilerplate code.

Going back to our button example, let’s say we want to get closer to React and use something like:

{% button url = "/", label = "Go Back" %}

That is doable, but for that we need to tell Eleventy what this button is and this is done via a function in the main eleventy config file. I don't think I need to go over this step by step because a quick look at the code will tell you everything you need to know:

const fs = require('fs');
const path = require('path');
const nunjucks = require('nunjucks');

module.exports = (eleventyConfig) => {
  eleventyConfig.addNunjucksShortcode('button', (props) => {
    const filePath = path.join(__dirname, '_includes/button.njk');
    if (!fs.existsSync(filePath)) {
      return 'missing file';
    }
    const content = fs.readFileSync(filePath).toString();
    return nunjucks.renderString(content, { component: props });
  });
};

Holy boilerplate, this just looks terrible and remember this is all for just one basic button component. I guess I get the idea, but I shudder to think how that config file would look for some of our websites. Now I have seen some example of getting around that using functions to loop over a directory with components, but that's just digging a deeper hole with even more js code. Even if you make that work it's just another makeshift solution which will then run into the fact that most websites use nested components as organisms, atoms, sections etc. which you'll have to do even more clunky workarounds to handle. Once again we were looking for simplicity - and we arrived at the opposite end of the spectrum - tedious boilerplate code shoved into a bloated config file. I mean just compare the atrocity above with a simple export like this:

const Button = ({ href, children }) => {
  return <Button href={href}>{children}</Button>;
};
export default Button;

Vue woes

So with that we're down to 0-4, not great to put it mildly. Eleventy has also cooperated with Vue on a specific Eleventy Vue Plugin. This approach had its benefits as it used Vue components out of the box rendering them to plain html providing you with a very nice base. The issue here is that after a while Eleventy ran into the same problem I mentioned in the first blogpost - maintenance. Maintaining a plugin like this requires cooperation on all the connected things and long story short that has broken down and the Eleventy Vue plugin is on its way to being archived after VUE shut it down on their end.

WebC to the rescue?

Now being completely honest I'm probably a bit too harsh on Eleventy as it did its job as a SSG wonderfully as I outlined in the first post. It's just a bummer that we got into this roadblock with components and looking at the various methods has put me off trying it for even a simple landing page. Just to make it perfectly clear I'm not comparing Eleventy with React because I want it to be React - far from it! We were looking for something that isn't React and thats why wrapping components in boilerplate js code to get them to work kinda like React is missing the point entirely.

But thankfully from what I've read there's a light in the tunnel in the shape of a new WebC Plugin. I have to say I am a bit skeptical after going through the sad tale of the Vue plugin, but from what I've seen this looks a lot more promising. Which is great because Eleventy really is promising and solves a lot of the basic problems. All that's missing is some less hacky way to do components and it can be truly great. So thanks for reading and see you in part three where we look at the new WebC plugin and if that solves our component issues.

Check out our other blogposts

GraphQL cache: using LRU cache with GraphQL Zeus
Michał Tyszkiewicz
Michał Tyszkiewicz
GraphQL cache: using LRU cache with GraphQL Zeus
1 min read
8 days ago
Unlocking the Power of React 19
Tomasz Gajda
Tomasz Gajda
Unlocking the Power of React 19
1 min read
about 2 months ago
Zeus update - GraphQL spread operator
Michał Tyszkiewicz
Michał Tyszkiewicz
Zeus update - GraphQL spread operator
1 min read
3 months ago

Ready for take-off?

Elevate your work with our editor that combines world-class visual graph, documentation and API console

Get Started with GraphQL Editor