Skip to main content

Building an Eleventy Starter Template: HTML

05-18-22 Dustin Whisman

Our last article in this series covered how to structure and configure your first Eleventy starter template project and create a basic HTML layout. Today, you will learn how to modify that HTML even further.


In the last part of this series, we set up our initial project structure and configuration, and we created a minimal HTML layout that can be shared between different pages. In this article, we will make that HTML layout more flexible and dynamic, so we can modify it as needed on a page-by-page basis.

If you just want to see the code (or use the starter template), you can find the repo at https://github.com/dustin-jw/eleventy-starter. You can also see a snapshot of what the codebase looks like after following the steps in this article by going to the html branch.

Making Our HTML Layout Dynamic

Previously, we created a layout.njk file with our HTML structure and an index.md file with front matter defining a title and description:

---
title: Home Page
description: This is the home page. It isn't very interesting right now.
layout: layout.njk

However, our layout.njk has static values for title and description, so the ones we defined in index.md won’t show up in our built HTML. Let’s fix that.

Nunjucks uses curly braces, similar to Handlebars, Vue, or Angular, to include dynamic content. If we change the title and meta description tags, we’ll be able to reference those front matter variables in the layout. Here’s our updated layout.njk file:

<!doctype html>

<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ title }}</title>
    <meta name="description" content="{{ description }}">
    <!-- TODO: add link tags, other meta tags, open graph info, etc. -->
  </head>
  <body>
    <!-- TODO: make the header and footer overridable -->
    <header>
      <p>This is the header.</p>
    </header>
    <main>
      {{ content | safe }}</main>
    <footer>
      <p>This is the footer.</p>
    </footer>
  </body>
</html>

Now, if we run npm start and inspect the output index.html or visit http://localhost:8080, we should see the title and description that we set in the front matter of index.md.

We can create another page to further test how dynamic our titles and descriptions are. Let’s call it about.md.

---
title: About
description: This is the about page. You might put biographical information here if it's for a personal website or portfolio.
layout: layout.njk
---

# About

This is the About page. You may not want or need this, so feel free to delete it if it isn't useful to you!

If you visit localhost:8080/about, you should see this new About page, and you can inspect it to make sure the title and description are accurate.

So far, we’ve only changed simple text. How about more complicated markup?

Using Blocks

For something like the header and footer that we want to be dynamic, we can use Nunjucks blocks to override them at the page level. We will also need a block for our main content. Let’s define the blocks in our layout partial.

<body>
  {% block header %}
    <header>
      <p>This is the header.</p>
    </header>
  {% endblock %}
  <main>
    {% block content %}
      {{ content | safe }}
    {% endblock %}</main>
  {% block footer %}
    <footer>
      <p>This is the footer.</p>
    </footer>
  {% endblock %}
</body>

By wrapping our dynamic blocks this way, the default markup and content will be used unless we override the blocks from somewhere else. To do that, however, we have to use .njk files (instead of .md like we’ve been using), and we have to extend our layout instead of specifying it in front matter.

Let’s change about.md to about.njk, and let’s add a contact.njk file for contrast. In the “About” page, we’ll override the header and footer with page-specific content.

---
title: About
description: This is the about page. You might put biographical information here if it's for a personal website or portfolio.
---

{% extends 'layout.njk' %}

{% block header %}
  <header>
    <p>
      This header has custom content specific to the current page.</p>
  </header>
{% endblock %}

{% block footer %}
  <footer>
    <p>
      This footer has custom content specific to the current page.</p>
  </footer>
{% endblock %}

{% block content %}
  <h1>About</h1>
  <p>
    This is the About page. You may not want or need this, so feel free to delete it if it isn't useful to you!</p>
{% endblock %}

The Contact page will use Nunjucks for templating, but it won’t use blocks. Note that we can define the layout in front matter instead of using Nunjucks’ extend feature.

---
title: Contact Page
description: This is the contact page. You might use this to provide methods for people to get in touch with you.
layout: layout.njk
---

<h1>
  Contact Page
</h1>

<p>
  This is the Contact Page. If you wanted people to be able to reach you, you
  might provide social media handles, an email address, a physical address, or a
  phone number.
</p>

Now if we switch between the /about page and the /contact page, we should see that the header and footer are different for both of them. Note that the order in which we place our blocks doesn’t matter–they will always be rendered in the places defined in the layout.

Blocks unlock a lot of potential possibilities, and later on in this series, we’ll be using them for page-specific styles, scripts, and SEO information.

Linting and Accessibility

Before we start adding more pages and expanding our HTML, we should set up some linting to ensure that we’re outputting valid, accessible markup. We’ll be using pa11y-ci for this, but feel free to substitute another linting tool if there’s one that you already use for HTML.

We’ll install pa11y-ci, then create an npm script for linting our HTML output in the dist directory.

npm install --save-dev pa11y-ci

In our package.json:

"scripts": {
  "start": "eleventy --serve",
  "build": "eleventy",
  "lint:html": "pa11y-ci './dist/**/*.html'"
}

If you npm run lint:html, you should see some output that Pa11y is running against each HTML file found in the dist directory, and if all goes well, no issues will be found. However, we’ll want to customize our setup to use the axe runner, which uses axe-core by Deque to detect accessibility issues.

Let’s create a configuration file at the root of our project and call it .pa11yci.json. We’ll keep it simple for now, but pa11y has a lot of configuration options to choose from, should we need them. For now, we’ll specify the runners, using axe and htmlcs (HTML_CodeSniffer).

{
  "defaults": {
    "runners": [
      "axe",
      "htmlcs"
    ]
  }
}

Now, we’ll need to update our lint:html script in package.json to use the configuration file we just created.

"scripts": {
  "lint:html": "pa11y-ci -c .pa11yci.json './dist/**/*.html'"
}

We can test that this is working as expected by deliberately making our markup inaccessible, and running npm run lint:html. For example, if we add a role=“foo” attribute to our main element, we should see errors like “ARIA roles used must conform to valid values.” If we remove the invalid role, rebuild the site, and run npm run lint:html again, we should no longer see those errors.

And There We Have It

Our starter template is starting to come together now. We have a decent HTML structure that we can reuse and extend between pages, and we have linting set up to make sure our HTML stays in good shape.

We’ll be building on top of this foundation throughout the series, starting with CSS in Part Three. See you next time!

Related Content

Want to talk about how we can work together?

Katie can help

A portrait of Vice President of Business Development, Katie Jennings.

Katie Jennings

Vice President of Business Development