Skip to content

Validating the HTML of an Eleventy site

Let's build an HTML validator for an Eleventy site. It will validate all generated pages to make sure they are valid HTML. Any errors will be reported in the terminal.

Published
Updated
Reading time
4 min

Inspired by Matt's tweet about adding an HTML validator to his blog, I decided to build one for my site tinytip.co that is built with Eleventy 1.0.

My implementation is a bit different. I use the Eleventy Linter feature to run the validation and a different validator package that works offline. Let's implement it.

Add a linter

Add a new file html-validator.js next to your .eleventy.js config and export a validator function. The function will receive the content, the input path and the output path of the file processed by Eleventy:

exports.validate = function validate(content, inputPath, outputPath) {
  // TODO
  console.log("Hello from HTML validator");
};

Then we add the function as a linter to Eleventy. This will run our validate function whenever a file is processed by Eleventy:

const htmlValidator = require("./html-validator");

module.exports = function (eleventyConfig) {
  eleventyConfig.addLinter("html-validator", htmlValidator.validate);
};

Implement the validator

Next we want to validate our HTML. I use the npm package html-validate for that which works offline and comes with TypeScript definitions (which useful even if you don't use TypeScript). Install it as a dependency:

npm install -D html-validate

Now we can import the package, configure it and use it in our validate function. We first make sure that the file is an HTML file and then call validateString() to validate the content:

const { HtmlValidate, StaticConfigLoader } = require("html-validate");
const loader = new StaticConfigLoader();
const htmlValidate = new HtmlValidate(loader);

exports.validate = function validate(content, inputPath, outputPath) {
  if (!outputPath.endsWith(".html")) return;

  const validationResult = htmlValidate.validateString(content);
  console.log(`${validationResult.valid ? "✅" : "❌"} ${outputPath}`);
};

Start your Eleventy site and you should see the output for all generated pages in your terminal.

Store the results

We currently only output the validation result (valid or invalid) but no details. For actually fixing issues we need to know what exactly is wrong. We could print more details but doing that in the terminal is not very readable. Instead, we can store the validation result in a file.

We create a results object, store the result per file in that object and add a storeResults function that creates a JSON file. I put the file into the output directory of Eleventy but you can change that path to any other directory you like.

const fs = require("fs");

exports.results = {};

exports.storeResults = function storeResults() {
  const content = JSON.stringify(exports.results, null, 2);
  fs.writeFileSync("_site/html-validation.json", content);
};

exports.validate = function validate(content, inputPath, outputPath) {
  // ...
  exports.results[outputPath] = validationResult.valid 
    ? undefined 
    : validationResult;
};

Finally we need to call the storeResults function after Eleventy has finished processing all files. We can do that using the eleventy.after event:

module.exports = function (eleventyConfig) {
  eleventyConfig.on("eleventy.after", htmlValidator.storeResults);
  eleventyConfig.addLinter("html-validator", htmlValidator.validate);
};

When running Eleventy, you should see the output for all generated pages in your terminal and the results in a JSON file.

Add more context

The validator provides the line on which each error occurred. To make the output more useful, we can add some context by adding this line of code (and some lines before and after) to the output:

exports.validate = function validate(content, inputPath, outputPath) {
  // ...

  const validationResult = htmlValidate.validateString(content);
  const lines = content.split(/\r?\n/);
  validationResult.results.forEach((result) => {
    result.messages.forEach((message) => {
      message.codeLines = lines.slice(
        // Three lines before + actual line
        Math.max(message.line - 4, 0),
        // Three lines after
        message.line + 3
      );
    });
  });

  // ...
};

This will add the actual code to the validation result.

Configure the validator

You can configure the validator by passing a config object to the StaticConfigLoader constructor. This way you can for example disable some rules if you can't (or don't want to) fix them.

const loader = new StaticConfigLoader({
  extends: ["html-validate:recommended"],
  rules: {
    "no-trailing-whitespace": "off",
  },
});

That's it. You now see if your HTML is valid and if not you can fix the errors.

I implemented this validator for my site tinytip.co. Check it out if are interested in frontend tips but please don't inspect the HTML code, I did not yet fix the errors 🙈