Optimize your img tags with Eleventy Image and WebC

October 23, 2022 - 10 min

I am very excited about WebC and couldn’t wait to try out the corresponding Eleventy plugin.

The first use-case I had in mind was to use Eleventy Image for the native HTML img tag, that can be generated from Markdown and without shortcode. I had done this before, for my unpublished blog rewrite that’s been waiting to be released for ever now, through hooking up Eleventy Image into a markdown-it plugin. It wasn’t pretty as markdown-it plugins are pretty alien to me.

But now, this can be done “natively”, à-la-Cloudflare Workers for your native HTML elements!


UPDATE (04/11/2022): After sharing this article on Twitter, Zach Leatherman pointed out there should be an easier solution. It didn’t work as expected at first, which put me on a journey to fix the underlying issue in @11ty/webc. I’m very pleased to share that my first contribution to the project was accepted and landed in v0.6.0. As a result, the content of the last section was updated to remove the dirty data file hack with _data/Image.js: your image optimization logic can now truly live in a single file.


The documentation for WebC mentioned this use-case exactly, in the shape of this innocent puzzle:

screenshot of the Eleventy documentation page for 'JavaScript render functions' from which the quote below is taken

“Free idea: use the Eleventy Image plugin to return optimized markup”

So it should be easy, right?

Remember this lad?

I have discovered a truly remarkable proof of this theorem which this margin is too small to contain. Pierre de Fermat, cca. 1637

No need to wait 3 centuries for the answer this time, because it was ultimately not so hard… But I did struggle and wrestle with it for a few hours :D

Anyway, hope that can be useful to someone.

If you want to see the finished implementation, you can get it on GitHub: RobinCsl/11ty-webc-img-demo.

Set up a WebC-powered Eleventy project

or take a shortcut by cloning this repo and going to the 01-initial folder.

Initialise the project

Let’s start with an empty folder and add a basic package.json file:

mkdir 11ty-webc-img
cd 11ty-webc-img
npm init -y

We need to install the latest canary release (2.0.0-canary.16 at the time of writing) of Eleventy as well as the WebC plugin:

 npm install @11ty/eleventy@2.0.0-canary.16 @11ty/eleventy-plugin-webc

Configure Eleventy

We will now configure Eleventy to use the WebC plugin by creating a .eleventy.js file with the following content:

const webc = require("@11ty/eleventy-plugin-webc");

module.exports = function(eleventyConfig) {
	eleventyConfig.addPlugin(webc, {
		components: "_includes/webc/*.webc"
	});
}

In particular, components in the _includes/webc folder will be available in the global scope, no need to import them explicitly!

Scaffold a minimal project

To finish our setup, we create a layout file and a simple WebC component which we will use in a basic page, to make sure our setup is working correctly.

We create the layout file under _includes/layouts/base.webc. It is a simple HTML template with the @html="content" WebC directive set on the body tag. (See my-layout.webc in the docs)

<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>Eleventy Images + WebC rocks!</title>
	</head>
	<body @html="content"></body>
</html>

We now create a simple WebC component and write the following content, taken from the promising website 11ty.webc.fun, in _includes/webc/site-footer.webc :

<footer>
  <p>&copy; 2022 Yours Truly.</p>
</footer>

Finally, we create our basic page at the root of the project in index.webc:

---
layout: layouts/base.webc
---

Fun with WebC

<site-footer></site-footer>

If all went according to plan, we should now be able to run

npx @11ty/eleventy --serve

in the terminal and see the following output in our browser at http://localhost:8080:

initial output of the scaffold in the browser; it reads 'Fun with WebC, 2022 Yours Truly'

Create an img.webc component

or check the code on GitHub in the 02-img folder if you want to see the code right away.

In this section, we will create our img.webc override for the native img HTML tag and use Eleventy Image within it.

As a starting point, we will use the example from the documentation from which the screenshot at the start of this post is taken. In a new file _includes/webc/img.webc, add the following:

<script webc:type="render" webc:is="template">
function() {
	if(!this.alt) {
		throw new Error("[img.webc] the alt text is missing, no fun");
	}
	return `<img src="${this.src}" alt="${this.alt}">`;
}
</script>

This essentially verifies at build-time that all your images have a non-empty alt text, neat!

Let’s add a “broken” image to our index page, in index.webc, to verify that this works correctly:

---
layout: layouts/base.webc
---

Fun with WebC

<img src="https://images.unsplash.com/photo-1627224286457-923944e5b159?auto=format&fit=crop&w=387&q=80">

<site-footer></site-footer>

And indeed, running npx @11ty/eleventy --serve yields an expected error:

$ npx @11ty/eleventy --serve
[11ty] Problem writing Eleventy templates: (more in DEBUG output)
[11ty] 1. Having trouble rendering webc template ./index.webc (via TemplateContentRenderError)
[11ty] 2. [img.webc] the alt text is missing, no fun (via Error)

Add an alt text (“possum hanging from a palm leaf” should work) and try again. Oh no! Another error:

$ npx @11ty/eleventy --serve  
[11ty] Problem writing Eleventy templates: (more in DEBUG output)
[11ty] 1. Having trouble rendering webc template ./index.webc (via TemplateContentRenderError)
[11ty] 2. Circular dependency error: You cannot use <img> inside the definition for ./_includes/webc/img.webc (via Error)

That makes sense, when you think about it! All our .webc files are processed by WebC, so if we return an img tag in a img.webc component, the compiler would work indefinitely to get to the bottom of it, because it’s turtles img all the way down!

Luckily for us, we can let WebC know when its job is done through the webc:raw attribute:

Use webc:raw to opt-out of WebC template processing for all child content of the current node. Notably, attributes on the current node will be processed.

Let’s fix it in _includes/webc/img.webc:

  <script webc:type="render" webc:is="template">
  function() {
	  if(!this.alt) {
	  	throw new Error("[img.webc] the alt text is missing, no fun");
	  }
-	  return `<img src="${this.src}" alt="${this.alt}">`;
+	  return `<img webc:raw src="${this.src}" alt="${this.alt}">`;
  }
  </script>

It compiles now! 🎉

Here’s the expected output:

screenshot of the output in the browser, same text as before, with a picture of a possum taking quite a bit of room on the page

We have effectively augmented our img tag to verify at build time that alt text is present.

Let’s now add Eleventy Image to the party!

Optimize your HTML img with Eleventy Image

or check the code on GitHub in the 03-final folder if you want to see my solution.

Let’s first install Eleventy Image in our project:

npm install @11ty/eleventy-img

We will more or less copy the example from the Eleventy Image docs in the Asynchronous Shortcode section, because our JavaScript render function can be async!

In the file _includes/webc/img.webc, let’s add the following:

  <script webc:type="render" webc:is="template">
-   function() {
-      if(!this.alt) {
-          throw new Error("[img.webc] the alt text is missing, no fun");
-      }
-      return `<img webc:raw src="${this.src}" alt="${this.alt}">`;
+    const Image = require("@11ty/eleventy-img");
+    module.exports = async function() {
+      let metadata = await Image(this.src, {
+        widths: [300],
+        formats: ["avif", "jpeg"],
+        outputDir: "_site/img/",
+      });

+      let imageAttributes = {
+        alt: this.alt,
+        sizes: "100vw",
+        loading: "lazy",
+        decoding: "async",
+      };

+      // You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
+      return Image.generateHTML(metadata, imageAttributes);
  }
  </script>

Note that we harcoded the value for sizes in the imageAttributes object, and prepended both src and alt by this to wire things up correctly. We also instruct the Image plugin to output optimized images in our _site/img/ folder directly for them to be available when using the following markup:

<img alt="a lovely image" src="/img/hash-width.jpeg">

Let’s run npx @11ty/eleventy --serve once more:

$  npx @11ty/eleventy --serve
[11ty] Problem writing Eleventy templates: (more in DEBUG output)
[11ty] 1. Having trouble rendering webc template ./index.webc (via TemplateContentRenderError)
[11ty] 2. Circular dependency error: You cannot use <img> inside the definition for ./_includes/webc/img.webc (via Error)

That error sounds familiar, right?

We need to tell WebC to ignore img tags generated by Eleventy Image, and it’s as simple as adding "webc:raw": true to the imageAttributes object:

  <script webc:type="render" webc:is="template">
  const Image = require("@11ty/eleventy-img");
  module.exports = async function() {
    let metadata = await Image(this.src, {
      widths: [300],
      formats: ["avif", "jpeg"],
      outputDir: "_site/img/",
    });
  
    let imageAttributes = {
      alt: this.alt,
      sizes: "100vw",
      loading: "lazy",
      decoding: "async",
+     "webc:raw": true,
    };
  
    // You bet we throw an error on missing alt in `imageAttributes` (alt="" works okay)
    return Image.generateHTML(metadata, imageAttributes);
  }
  </script>

And we’re done!! (Note how the image now aligns perfectly with localhost:8080 #IncontestableTruth)

screenshot of the output in the browser, the image now takes 300px

with corresponding markup:

<!doctype html>
<html lang="en">
<head>
        <meta charset="utf-8">
        <title>Eleventy Images + WebC rocks!</title>
    </head>
    <body>Fun with WebC

<picture><source type="image/avif" srcset="/img/SwdsuTpRbT-300.avif 300w"><img alt="possum hanging from a palm leaf" loading="lazy" decoding="async" src="/img/SwdsuTpRbT-300.jpeg" width="300" height="450"></picture>


<footer>
  <p>© 2022 Yours Truly.</p>
</footer>

</body>
</html>

Parting words

I am so excited to explore further the possibilities of using WebC to augment other native HTML elements.

This article only scratches the surface when it comes to the img tag. You could pass props down from the markup to Eleventy Image and control the behaviour of the image optimization. But we can now augment our native img tag without much hassle as it’s all in one file.

It even works with Markdown images (and yields the cutest webpage ever):

  ---
  layout: layouts/base.webc
  ---
  
  Fun with WebC
  
  <img alt="possum hanging from a palm leaf" src="https://images.unsplash.com/photo-1627224286457-923944e5b159?auto=format&fit=crop&w=387&q=80">
  
+  <template webc:type="11ty" 11ty:type="md">
+  
+   !['close-up of a baby possum among ferns'](https://images.pexels.com/photos/13960994/pexels-photo-13960994.jpeg)
+  
+  </template>
  
  <site-footer></site-footer>
  

same screenshot of the output in the browser, with an additional possum picture, rendered from the Markdown template above

All the more reasons to optimize your images now!

Find the code supporting this article on the RobinCsl/11ty-webc-img-demo repository.

Join Robin's Gazette

Receive

every second Friday

my favourite links and findings

on frontend technologies, writing and more!

46 issues and counting since December, 2020

    I hate spam, don't expect any from me. Unsubscribe at any time.



    Personal blog written by Robin Cussol
    I like math and I like code. Oh, and writing too.