Enforcing better HTML markup with Eleventy

by Yannick Nana published on

While what we mean is usually very clear to us, others may decode our messages differently from what we intended. This is especially true on the web, where there are many ways to consume content. The language, browser type, device model, using a screen reader, navigating with or without a mouse - all of these factors can influence how information is experienced on the web.

And the glue that can better aid all these factors might be developing with semantic HTML which helps in describing content more accurately. In this article, we will explore common issues related to incorrect HTML markup and how you can enforce good practices to fix them.


Web experiences should be fast, accessible, and tailored to the capabilities of users and the devices they own. A premise we could all agree on, as we have all have suffered from poorly designed web experiences. And if you are a web developer, you might have once followed practices that led to such experiences.

Still, building for a wide range of users you know nothing about is far from being a straightforward process. Fortunately, most of the research have extensively been done and made available sets of proven practices that when followed can ensure that what we build does not exclude groups of people arbitrarily. So how can we develop interfaces that create such experiences? To first understand that, we have to understand the role of semantics.

Semantics in spoken languages

Something we usually agree on in society is how important it is to define things. Without mutual understanding, we wouldn't be able to communicate effectively.

If I ask you to define what a cup of coffee is, you might provide an answer resembling: “a drink made from coffee beans”, whereas the internet might go for “the juice of a plant that gives life and suppresses the urge for murder”, or you might have your own definition not too far from these two. This mutual understanding of different concepts helps to communicate with each other. Whenever you buy a cup of coffee, you will expect what you receive to reflect this definition. You can even go a step further and adjust your words based on who you are talking to and how that person might feel at the moment.

For instance, you could find yourself speaking louder to someone hard of hearing, avoid technical words with a young person, or even adding gestures to illustrate what would otherwise be too abstract. Such adjustments are done unconsciously and aim to increase the likelihood that every exchange is done efficiently. The last thing you want is to have to persistently repeat something over and over to a person who simply does not understand what you are saying instead of adjusting once.

Semantics on the web

So what about on the web? Are semantics still as important and is it possible to adjust our message to the circumstances of others?

Communicating did indeed go past the face-to-face interactions and has reached the internet, in the form of blogs, websites, or social media. Even if the medium differs, I would argue that the idea of helping others better understand your content remains the same. Whether you are a company seeking profit through selling a product, a creator promoting his content or someone who occasionally post blog articles, there is surely a benefit in having a more reachable content. In that aspect, the need for semantics on the web is very close to that of spoken languages.

Specifically, for people with disabilities, they point assistive technology at what stuff is, so there is a bit of responsibility, a big responsibility that we have as Web developers, to make sure we point machines and assistive tech into the right direction and show them exactly what stuff is and what we mean by stuff in our user interfaces.

(Hidde de Vries, Semantics and How to Get Them Right)

Taking accountability as a web developer

Harry Potter and the Sorcerer's Stone (2001).
An orphaned boy enrolls in a school of wizardry, where he learns the truth about himself, his family and the terrible evil that haunts the magical world.

Without context you might have understood from this small excerpt that “Harry Potter and the Sorcerer's Stone” corresponds to the title of a media published in “2001”. That’s the kind of extra meaning you can explicitly attach to content on the web through HTML elements and attributes.

<h1>Harry Potter and the Sorcerer's Stone</h1>
<time datetime="2001">2001</time>
<p>An orphaned boy enrolls in a school of wizardry, where he learns the truth about himself, his family and the terrible evil that haunts the magical world.</p>

This would then be conveyed as if you were saying “Hey device, present ‘Harry Potter and the Sorcerer's Stone’ as the title, this number value as the year of a date, and the remaining text as a paragraph.” By understanding the inherent meaning of such content, devices are able to convey it more accurately to a wide number of people, regardless of how they decide to experience it.

As someone responsible for building interfaces, not only are you trying to make your content understandable, you also have to make sure that content is accurately conveyed to their devices.

Helping users create their own ideal experience

In reality how does this help? In the same way that someone hard of hearing would have difficulties understanding what you say, there are situations that can prevent users from effectively consuming content.

And because you can’t adjust in real time, the idea is to provide all the hints you can to help users help themselves. Maybe the person browsing has a limited monthly internet package, and would sometimes prefer a low fidelity version of your website with only text content? Or that person has low vision, and would benefit from reading bigger text sizes? Perhaps said person is currently browsing while engaging in a phone call and could benefit from only using the keyboard to navigate? Such circumstances are intrinsic to the person, but often are reflected to their devices through preferences or apps aimed at supporting them.

As a developer, the HTML markup you write leads to an interface that users can view on their devices. Not only are your users diverse, but so are the devices they use. You may write sentences that are semantically valid, but the information may not be correctly displayed by your interface if you do not develop semantically. By utilizing the appropriate HTML elements to wrap your content, you can provide cues to devices browsing your interface about the way your content should be described.

When you start from scratch, it can be a tedious process to write good semantic HTML. As the complexity of your project increases, you may end up writing the same HTML patterns over and over again.

The sooner you can identify these patterns and establish a system for reusing them, the easier the development process will be. Having highlighted the role of semantics, let's look at the most common problems related to HTML markup and how you can avoid them.

Common HTML markup issues

Here are common issues encountered on the web that stems from bad HTML markup.

  • Each form field is not associated with its own label.
  • The heading structure is not respected.
  • Missing alternative text on images.
  • The opening HTML tag does not specify the main language.
  • The HTML code is not valid.


Designing a system for writing better HTML in Eleventy

Solid, accessible HTML should be the base of any web stack. If everything fails (and it will), your users still get the core content.

(Trys Mudford, Unyfying with Jamstack)

We are trying to implement a low configuration system that outputs HTML without adding a huge library or framework around it. We can achieve this by using a static site generator for addressing the need for reusable patterns. With the right foundations, it can support the implementation of robust and accessible HTML markup. Here we will explore how Eleventy, a static site generator, can help us in this regard.

Eleventy provides multiples ways to create reusable components. We will implement patterns that follow good practices and, by using them consistently we can ensure that the overall project does not deviate from our goal.

Enforcing better HTML markup in Eleventy

When you design a system, or a language, then if the features can be broken into relatively loosely bound groups of relatively closely bound features, then that division is a good thing to be made a part of the design. This is just good engineering.

Tim Berners Lee, Principles of Design

Reusable components with Nunjucks macros

Templating in Eleventy is what allows us to control how pages are rendered. It makes it possible to generate variables, loop over an array or run a custom plugin to generate data. Eleventy can handle a dozen different language models such as Nunjucks which we are going to use.

Nunjucks macros

Nunjucks has this feature called macros that hit all the boxes for building reusable components. It is similar to a function in a programming language and accepts arguments. The method for defining components macro is inspired by Trys Mudford detailed article on the subject.

For instance, we could create a button macro that takes in a label and output the markup for a tag component.

{% macro button(label, type="button", status='success') %}
<button type="{{type}}" class="tag-{{status}}">
{% endmacro %}

Now button is available to be called like a normal function:

{{ button(label='Disconnect', status='alert') }}

{# Output #}
<button type="button" class="tag-alert">Disconnect</button>

The only catch is that all macro parameters are optional by default and not providing one will not prevent our component from being rendered. Since we try to enforce specific parameters within our components to make them more accessible, we would rather be informed whenever a critical parameter is not provided. So how can we achieve this?

Building filter to enforce required parameters inside macros

We can take advantage of another Eleventy feature to do this, namely filters. In Eleventy, filters are JavaScript functions that accept content, modify it, and then return it to be displayed instead of the original.

This sounds a lot like macros, you might say, and that would be true. The only difference is that filters are best used to transform data rather than generate snippets of content.

We can start by creating a filter that throws a generic error when the value it receives is undefined.

eleventyConfig.addFilter("enforced", function(value) {
if(value === undefined){
throw new Error("A parameter is missing");

By applying this filter to the parameters of the macros we can make them mandatory.

{% macro button(label,type="button", status='success') %}
<button type="{{ type }}" class="tag-{{ status }}">
{{ label | enforced }}
{% endmacro %}

We can then use it inside our component definition to flag parameters we want as required.

<button type="{{ type }}">{{ label | enforced }}</button>

Using this button component without providing a label parameter would throw an error which is what we want.

{# Throws an error because the label is not provided #}

{{ button(status='alert') }}

And now, by combining Nunjucks macros and a filter, we can create reusable components and set up rules to make sure they meet our criteria. The Front End Checklist or the Web Quality Assurance Checklist are both great resources to find ideas of criteria for writing better HTML markup. In the following sections we will address common HTML markup issues by utilizing this method.

How to enforce labels inside form fields?

To enable users with disabilities to access form fields, it is essential to identify them. That way, screen reader users could scan the form fields by name or use keyboard shortcuts to quickly navigate from one field to another.


These criteria would lead to the following result for a password field.

<label for="password">
<input type="password" spellcheck="false" id="name" name="password" required>

The associated component could be built using the following Nunjucks macro:

<label for="{{ inputId | enforced }}">
{{ label | enforced }}
{% if not required %}
<span aria-hidden="true">(Optional)</span>
{% endif %}
type="{{ inputType or 'text' }}"
id="{{ inputId | enforced }}"
name="{{ inputName | enforced }}"
{% if spellcheck%} spellcheck="{{ spellcheck}}" {% endif %}
{% if required %} required {% endif %}


How to allow flexible headings inside components?

For screen reader users, headings describe the relationships between sections and subsections and — where used correctly — provide both an outline and a means of navigation.

Heydon Pickering

A common challenge you may face when building with a component-first approach is to settle for a single heading level. Yet, components should not be tied to a single situation, otherwise they will lose their primary benefit of being reusable. As such, it would not be appropriate to specify in advance a heading level which you cannot change according to the context.

Having a flexible way of setting the heading of a component can make it easier to respect correct heading structures while keeping the benefits of components. We can achieve that flexibility by passing the heading element as a parameter inside our macro and it just works.

Here we will build a post card component. The heading level should be provided depending on the context.


  • Headings should be flexible
<article class="card">
{%if imageUrl%}
<img src="{{imageUrl}}" alt="{{imageAlt | enforced}}"/>

<{{headingElement | enforced }}>
<a href="{{ url | enforced }}">
{{ title | enforced }}

{%if description %}

{%if time %}
<time datetime="{{time}}">{{ time }}</time>
{% endif %}

Inside a template we can use the card component while passing the correct heading element.

<h2>Card components</h2>
{{ component(
"card", {title: "About", url: "/about", headingElement: "h3", description: "Links and attributions"}

{# Output #}
<h2>Card components</h2>
<article class="card">
<h3><a href="/about">About</a></h3>
<p>Links and attributions</p>
{# Output #}


How to enforce missing alternative text on images?

In the context of visuals, alternative text is a textual description for people who cannot process information visually. In an effort to maintain a comparable experience for everyone, we can add these descriptions to visual contents. If you are interested you can find a thorough guide on how to write an image description. If the image is purely decorative, a null (empty) alt text should be provided (alt="") so that they can be ignored by assistive technologies, such as screen readers.

<img src="horizontal-wiggly-line.png" alt="">

In the post card component created previously, an image is included to illustrate posts. By using the enforced filter we can enforce an alternative text on these images.

{%if imageUrl%}
<img src="{{imageUrl}}" alt="{{imageAlt | enforced}}"/>

Separation of concerns

In Nunjucks you define variables with the set keyword. For a more convenient use, we unpack each arguments from the params object.

As shown in the following code snippet we could retrieve the value and enforce it at the top of the macros.

{% set imageAlt = params.imageAlt | enforced %}
{% set url = params.url | enforced %}
{% set headingElement = params.headingElement | enforced %}
{% set title = params.title| enforced %}
{% set imageUrl = params.imageUrl %}

{%if imageUrl%}
<img src="{{imageUrl}}" alt="{{imageAlt}}"/>
<!-- -->
<a href="{{ url }}">
{{ title }}
{%if description %}

This separation enables us to separate the logic from the template.

Addressing redundant content

In a card, the alternative text is supposed to describe the content of the image, and that content usually refers to the same content. As a result, the alternative text might be similar to the heading, which would create a duplicate. To prevent this, we may want to include more logic that detects a matching alternative text to the heading and throws out an error. First, we have to improve the filter so it throws an error when the force is set to true.

eleventyConfig.addNunjucksFilter("enforced", function(value, force) {
if(value === undefined ){
throw new Error("A parameter is missing")
if( force === true ){
throw new Error(`An error occured: ${value}`);

The inside the macro component we can throw an error if the heading title is similar to the image description.

{# Advanced #}
{% set check_redundancy = "The title is redundant with alt text" | enforced(true) if title === imageAlt %}

How to enforce the language of the page?

Specifying the language will enable text to speech tools to pronounce the content with the appropriate accent for the language used. It can also help translation tools. At the root of the project we can create a _data folder that will hold Eleventy global data files. Global data files allow you to store data in a single file and reference it in all of your templates.

By creating a global data file we can apply the correct language attribute across the entire project. Here we are adding this attribute inside a site.js global data file.

module.exports = {
title: "HTML Practices",
description: "Enforcing HTML best practices in Eleventy",
language: "en",

We can the reference the language attribute in the HTML opening tag of templates.

<!DOCTYPE html>
<html lang="{{site.language }}">
<head><!-- Head content --></head>
<body><!-- Body content --></body>

Validating the HTML code

There is a limit to what you can achieve by manually testing your websites. And even by “enforcing” good practices into your workflow, you are bound to miss something once in a while. To limit such occurrences you can use an automated tool for validating HTML markup. Inside Eleventy we can use Evaluatory , an open-source tool for website validation. It tests a website for accessibility and markup issues. We can use Evaluatory to automate checks on save inside our project.

Wrapping Up

Writing good HTML markup is one step towards providing a better web experience. And as long as this foundation is sturdy, you can effectively provide a better experience overall. You can explore this Github repository to find all the implementations mentioned in this article.

About Yannick Nana

Yannick Nana is a frontend developer from Lille (France) with a passion for learning and exploring new ways of developing the web. He has a background in cognitive science, and is committed to writing robust and semantic HTML.

Website: yannicknana.fr