3. Taking a tour

10 Mar, 2024

By now, our project should look like this:

  • 11ty-zones
    • _data
    • _includes
    • _site
    • images
    • node_modules
    • posts
    • style
    • .eleventy.js
    • .eleventyignore
    • .gitignore
    • 404.md
    • about.md
    • archive.njk
    • changelog.md (added in 1.1.0)
    • feed.njk (added in 1.1.0)
    • index.md
    • LICENSE
    • package-lock.json
    • package.json
    • README.md

(Not seeing a few of these? Files whose names begin with a dot, known as dotfiles, are hidden by default on most systems. Some shortcuts to unhide them: On Windows, see Microsoft's support page (it's different between Windows 10 and 11). On macOS, in Finder, press ⌘ Cmd + ⇧ Shift + ..)

Let's break all this down into parts.

Non-Eleventy stuff

  • 11ty-zones
    • node_modules
      • (various...)
    • .gitignore
    • changelog.md (added in 1.1.0)
    • LICENSE
    • package-lock.json
    • package.json
    • README.md

Good news: You can safely ignore all of these for now! It's nice to know why they're here and what they do, though, so here's a quick rundown.

Git-related

.gitignore is a list of things for Git (if we're using it) to exclude from our repository and leave untracked: _site, because we can always rebuild it, and node_modules, because we can always reinstall it.

Node-related

To recap, package.json is a manifest with everything Node needs to know about our package, and node_modules is the folder where npm keeps all of the packages our package depends on.

There's also package-lock.json. See, I kind of fibbed earlier when I said npm install uses package.json to know what versions of what dependencies to install - in truth, it uses package-lock.json if available. This file is automatically generated by npm and "describes the exact [node_modules] tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates."

In other words: Between the time I released this project and the time you installed it, some dependency or another may have released a newer version that says it's compatible with the one we need but accidentally breaks things anyway. package-lock.json makes sure you get the same "known working" versions that I had.

Project info

As with most software, Eleventy-Zones comes with a "read me" file, README.md. (Codeberg, like most Git hosting providers, treats this as a sort of homepage for the project's repository when you view the repo on the web.) It's really just a one-sentence summary and a link to this demo site, but hey.

changelog.md, added in 1.1.0, lists the changes made with each release of the project. It's not very long either.

Eleventy-Zones is released under the MIT License; copyright notices and the full text of this license can be found in the LICENSE file.

Configuration

  • 11ty-zones
    • .eleventy.js
    • .eleventyignore

.eleventy.js is Eleventy's (optional) config file. Whenever I say Eleventy does something "by default," this is probably where to tell it to do something else. (It's also where we can add our own filters and shortcodes, but more on those later.)

Eleventy-Zones mainly sticks with "vanilla" behavior here - my hope is that if this is part of how you're learning Eleventy, it'll mean fewer surprises when you start a new project from scratch. (I was really tempted to change some directory settings, though; for an opinionated example, see this demo site's repo.)

For our project, the config mostly just copies our images and CSS into the build and defines a few custom filters. It also adds Eleventy's HTML <base> plugin in case you're deploying your site to a subdirectory (like how this demo site is in the 11ty-zones-demo subdirectory of cypressSap.codeberg.page). Since 1.1.0, it also also adds Eleventy's RSS plugin (which actually just adds a few more useful filters).

.eleventyignore is an (also optional) list of things we don't want Eleventy to process. By default, Eleventy builds any *.md files it finds in our project into HTML pages, but since changelog.md and README.md aren't meant to be part of our site, those are listed here.

The output folder

  • 11ty-zones
    • _site
      • (various...)

As mentioned earlier, the output folder Eleventy builds our site into is called _site by default.

Since this is for the finished product only, you typically won't need to edit anything in here.

Assets

  • 11ty-zones
    • images
      • bg_weatherbeaten.gif
      • favicon.png
    • style
      • style.css

When building pages, Eleventy only cares about our template files (which we'll talk about next). This means that, by default, other files like our images and CSS here won't end up in our _site folder - our config file tells Eleventy to go ahead and copy these over anyway.

Templates

  • 11ty-zones
    • _includes
      • base.njk
      • footer.njk
      • header.html
      • post.njk
      • postlist.njk
    • posts
      • 2024-02-22-Post-Template.md
    • 404.md
    • about.md
    • archive.njk
    • feed.njk (added in 1.1.0)
    • index.md

Now for the big one!

Templates are the stars of our project - these files are combined with each other and various sources of data to build finished HTML pages. Eleventy supports a number of different languages for them (which you can mix and match); I've gone with probably the most popular combination:

Template files often begin with a block of front matter: Some data (usually in human-readable YAML) that the rest of the template, its content, can use.

By default, Eleventy looks for templates throughout the project folder (ignoring the output folder). It doesn't really care how they're organized or what they're named; it builds each into a page following whatever structure we've created.

For example, posts/2024-02-22-Post-Template.md gets built to _site/posts/2024-02-22-Post-Template/index.html, archive.njk gets built to _site/archive/index.html, and so on. (For index.md, Eleventy does know that _site/index/index.html probably isn't what we want, and builds it to _site/index.html instead.)

You can override this output structure by setting a template's permalink data property. For example, in 404.md's front matter, the line permalink: 404.html tells Eleventy to build to _site/404.html rather than the default _site/404/index.html.

Templates don't have to be built into HTML pages - feed.njk (added in 1.1.0) gets built into _site/feed.xml, again using the permalink property. (Templating languages like Nunjucks neither know nor care that we're not building to HTML here; they only look for their own syntax. Without the permalink, it would be built to the default _site/feed/index.html, but the contents of that file would be exactly the same.)

Includes

Not all templates are built 1:1 into finished files - special templates called includes are meant to be consumed by other templates instead. Think of them as components that you can reuse in multiple places.

By default, Eleventy looks for include templates in the _includes folder. (Like "regular" templates, you can name and organize these however you like.)

Layouts

Includes are usually inserted into other templates, but first I'd like to introduce Eleventy's layouts, a special kind of include. These work kind of the opposite way, wrapping around the templates that call them.

_includes/base.njk is a layout with our standard page structure, and most of our regular templates use it (with the line layout: base.njk in their front matter). It's mostly boilerplate HTML, peppered with some things like {{ meta.lang }} and {{ title }}. In Nunjucks, these double-braces are how we reference variables to be filled in with data when the template is built.

Inside its <main> element, one variable is used a little differently: {{ content | safe }}. Variables can be processed through filter functions, and in Nunjucks this is done with a pipe (|) - so {{ content | safe }} "pipes" the content variable through the safe filter before filling it in. But what are those?

Well, content is provided by Eleventy: It represents the content of whatever template called the layout, already built into HTML and ready to be wrapped by the layout.

The problem there is that Nunjucks escapes HTML from variables - it changes (for example) "<p>" into "&lt;p&gt;" so the browser won't treat it as an HTML tag. That's a good default for security, but if content were escaped, most of our site would just look like a wall of code! That's why Nunjucks comes with the safe filter, which stops a variable from being escaped.

Our project has one other layout, _includes/post.njk, which is used by our blog posts. This one doesn't need all the usual HTML boilerplate because it just uses _includes/base.njk as its layout - a trick called layout chaining.

Partials

Simple includes, like our site's header and footer, are often called partials. In Nunjucks, we use the include tag to insert them, like {% include "header.html" %}.

_includes/header.html is pretty straightforward - it really is just a fragment of plain old HTML! _includes/footer.njk does a little more work, using some logical Nunjucks tags (if, else, endif) so the author's name is made into a link only if a URL is provided.

Macros

Nunjucks macros, like the one defined in _includes/postlist.njk, allow more complexity than partials: They accept arguments so we can pass them data, controlling their behavior.

Our postlist macro returns a list of blog posts. With the arguments it's passed in archive.njk, it lists every post, but index.md passes it different arguments for a list of just the three most recent posts with a link to the archive page.

(Macros aren't the only tool for this kind of situation - Eleventy's shortcodes are another common one. For some pros and cons of different approaches, check out this discussion on GitHub and this post by Jérôme Coupé.)

Data files

  • 11ty-zones
    • _data
      • eleventyComputed.js
      • meta.js
    • posts
      • posts.11tydata.js

We've briefly mentioned data and how it can come from various sources, such as a template's front matter. Our project also has some data files that make data available to multiple templates at once.

By default, the _data folder is where Eleventy looks for global data files. These can be referenced by their filename: _data/meta.js, which has some metadata about our blog, becomes the global data object meta. (Remember that meta.lang variable that _includes/base.njk referenced?) As usual, you can organize as you see fit - if we moved this file to _data/foo/myImportantData.js, we'd reference foo.myImportantData.lang instead.

_data/eleventyComputed.js is special: Computed data is processed last, after any other data sources, which means it can be calculated from those sources and takes priority over them. (Sorry - this file can't be renamed.) We use this to check if each template already has title data, then (if it doesn't) generate one from its filename - mimicking Zonelets's behavior.

posts/posts.11tydata.js is a directory data file that applies only to templates in the posts folder. (These need the same name as their folder; posts/foo.11tydata.js wouldn't work.) This file sets the layout and tags data (more on tags in a moment) for all of our blog posts - that way we don't have to add front matter again and again on every new post to set the same thing.

(Now's probably a good time to bring up Eleventy's data cascade, which settles any conflicts between our various data sources. For example, if you do set a layout in some post's front matter, that one will be used instead of the one set in posts/posts.11tydata.js, because front matter is higher priority than directory data files.)

As for tags: Eleventy uses our tags data to group our content into arrays called collections, which are then available in a global data object also called collections.

Since posts/posts.11tydata.js gives our posts the "blogPosts" tag, they'll all be added to collections.blogPosts. This collection powers our blog's navigation: It's used by the "next/previous post" links at the bottom of _includes/post.njk, and it's passed to the postlist macro from both index.md and archive.njk.

(tags is also an array, which makes it an exception to the data cascade: By default, array data gets merged rather than replaced. For example, if we put tags: cats in some post's front matter, its tags will end up as ['blogPosts', 'cats'].)