Introduction to 11ty (Eleventy)

Eleventy (written "11ty") is a static site generator built on Node.js. It takes templates, Markdown and data files and turns them into plain HTML files you can host anywhere. It ships zero client-side JavaScript by default, needs no framework, and lets you mix template languages such as Markdown, Nunjucks, Liquid and plain JavaScript. These examples target Eleventy 3, which needs Node.js 18 or later.

1. Installation

Create a project folder and install Eleventy locally:

mkdir my-site
cd my-site
npm init -y
npm pkg set type="module"
npm install @11ty/eleventy

Setting "type": "module" lets you use import / export in your config and data files.

2. Your first page

Create index.md:

# Hello, Eleventy!

This page was written in **Markdown**.

Build the site, or build and start a live-reloading dev server:

npx @11ty/eleventy           # build once into _site/
npx @11ty/eleventy --serve   # build, watch and serve at http://localhost:8080

Eleventy writes _site/index.html. Add scripts to package.json so you don't have to remember the commands:

"scripts": {
  "build": "eleventy",
  "start": "eleventy --serve"
}

3. Configuration and folders

Keeping your source files in a src/ folder stops Eleventy from processing things like README.md. Create eleventy.config.js in the project root:

export default function (eleventyConfig) {
  // filters, shortcodes, plugins and passthrough copy go here
}

export const config = {
  dir: {
    input: "src",          // source files
    output: "_site",       // built site (the default)
    includes: "_includes", // layouts and partials, inside src/
    data: "_data",         // global data, inside src/
  },
};

A typical project then looks like this:

my-site/
├── eleventy.config.js
├── package.json
├── src/
│   ├── _data/
│   │   └── site.js
│   ├── _includes/
│   │   └── base.njk
│   ├── css/
│   │   └── style.css
│   ├── posts/
│   │   ├── posts.json
│   │   ├── first-post.md
│   │   └── second-post.md
│   ├── about.md
│   └── index.njk
└── _site/          # generated, don't edit

How files map to URLs

Source fileOutput fileURL
src/index.njk_site/index.html/
src/about.md_site/about/index.html/about/
src/posts/first-post.md_site/posts/first-post/index.html/posts/first-post/

Supported template languages

ExtensionLanguage
.mdMarkdown (also processed with Liquid by default)
.htmlHTML (also processed with Liquid by default)
.njkNunjucks
.liquidLiquid
.11ty.jsJavaScript

4. Front matter

Front matter is YAML between --- lines at the top of a file. It sets data for that page.

---
title: About me
description: A little about who I am
permalink: /about-me/
---

# {{ title }}

{{ description }}

Some keys have special meaning:

KeyPurpose
layoutWrap the page in a layout from _includes/
permalinkChange the output URL (or false to skip writing the file)
tagsAdd the page to one or more collections
dateSet the page date (used for sorting)
paginationGenerate multiple pages from data
eleventyExcludeFromCollectionsKeep the page out of collections

5. Layouts

A layout wraps page content. Create src/_includes/base.njk. The page's rendered content arrives as content, and the safe filter stops it from being escaped:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ title }} | {{ site.title }}</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/about/">About</a>
  </nav>
  <main>
    {{ content | safe }}
  </main>
</body>
</html>

Use it from any page:

---
layout: base.njk
title: About
---

# About

I build websites with Eleventy.

Layout chaining

Layouts can use other layouts. src/_includes/post.njk adds post-specific markup, then hands off to base.njk:

---
layout: base.njk
---
<article>
  <h1>{{ title }}</h1>
  <time datetime="{{ page.date.toISOString() }}">{{ page.date | readableDate }}</time>
  {{ content | safe }}
</article>

readableDate is a custom filter defined in section 9.

6. Collections

Any page with a tag joins a collection of that name. Create src/posts/first-post.md:

---
title: My first post
date: 2026-10-01
tags: post
layout: post.njk
---

Welcome to my blog!

Directory data files

Rather than repeating tags and layout in every post, put them in a data file named after the folder, src/posts/posts.json. Every file in that folder inherits it:

{
  "tags": "post",
  "layout": "post.njk"
}

Listing a collection

Collections are sorted oldest first by date. On the home page, src/index.njk:

---
layout: base.njk
title: Home
---
<h1>Latest posts</h1>
<ul>
  {% for post in collections.post | reverse %}
    <li>
      <a href="{{ post.url }}">{{ post.data.title }}</a>
      <small>{{ post.date | readableDate }}</small>
    </li>
  {% endfor %}
</ul>

Each item in a collection has url, date, data (its front matter and other data) and content. collections.all contains every page.

Custom collections

Build collections in the config file, for example from a folder instead of a tag:

export default function (eleventyConfig) {
  eleventyConfig.addCollection("posts", (collectionApi) =>
    collectionApi.getFilteredByGlob("src/posts/*.md").reverse()
  );
}

7. Data files

Files in src/_data/ are available to every template, named after the file. A JSON file, src/_data/navigation.json:

[
  { "text": "Home", "url": "/" },
  { "text": "About", "url": "/about/" },
  { "text": "Blog", "url": "/blog/" }
]
<nav>
  {% for item in navigation %}
    <a href="{{ item.url }}"
      {% if item.url == page.url %}aria-current="page"{% endif %}>{{ item.text }}</a>
  {% endfor %}
</nav>

A JavaScript data file can compute values or fetch data at build time. src/_data/site.js:

export default {
  title: "My Site",
  author: "Stuart",
  year: new Date().getFullYear(),
};
// src/_data/repos.js — runs once per build
export default async function () {
  const res = await fetch("https://api.github.com/users/11ty/repos");
  return res.json();
}

Use them as {{ site.title }} or {% for repo in repos %}.

The data cascade

When the same key is set in several places, the more specific source wins. From highest to lowest priority:

  1. Computed data (eleventyComputed)
  2. Front matter in the page
  3. Template data file (first-post.json next to first-post.md)
  4. Directory data files (posts/posts.json), then parent folders
  5. Front matter in layouts
  6. Global data in _data/

8. Pagination

Pagination generates many pages from one template. Split a long list into pages of five, src/blog.njk:

---
layout: base.njk
title: Blog
pagination:
  data: collections.post
  size: 5
  alias: posts
  reverse: true
permalink: "/blog/{% if pagination.pageNumber > 0 %}page-{{ pagination.pageNumber + 1 }}/{% endif %}"
---
{% for post in posts %}
  <h2><a href="{{ post.url }}">{{ post.data.title }}</a></h2>
{% endfor %}

<nav>
  {% if pagination.href.previous %}<a href="{{ pagination.href.previous }}">Newer</a>{% endif %}
  {% if pagination.href.next %}<a href="{{ pagination.href.next }}">Older</a>{% endif %}
</nav>

One page per data item

With size: 1, you get a page for every item. Given src/_data/team.json:

[
  { "name": "Ada Lovelace", "role": "Engineer" },
  { "name": "Grace Hopper", "role": "Admiral" }
]

This template, src/team.njk, creates /team/ada-lovelace/ and /team/grace-hopper/:

---
layout: base.njk
pagination:
  data: team
  size: 1
  alias: person
permalink: "/team/{{ person.name | slugify }}/"
eleventyComputed:
  title: "{{ person.name }}"
---
<h1>{{ person.name }}</h1>
<p>{{ person.role }}</p>

9. Filters and shortcodes

Filters transform a value. Shortcodes output reusable chunks of markup. Both are defined in the config file and work in Nunjucks, Liquid and JavaScript templates.

export default function (eleventyConfig) {
  // Filter: {{ page.date | readableDate }}
  eleventyConfig.addFilter("readableDate", (date) =>
    new Date(date).toLocaleDateString("en-GB", {
      day: "numeric",
      month: "long",
      year: "numeric",
    })
  );

  // Shortcode: {% year %}
  eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`);

  // Shortcode with arguments: {% youtube "dQw4w9WgXcQ", "Video title" %}
  eleventyConfig.addShortcode("youtube", (id, title) =>
    `<iframe src="https://www.youtube-nocookie.com/embed/${id}"
      title="${title}" loading="lazy" allowfullscreen></iframe>`
  );

  // Paired shortcode: {% callout "warning" %}Careful!{% endcallout %}
  eleventyConfig.addPairedShortcode("callout", (content, type = "info") =>
    `<div class="callout callout-${type}">${content}</div>`
  );
}

Useful built-in filters include slugify (turn text into a URL-safe slug) and, in Nunjucks, safe, reverse, first and length.

10. CSS, images and passthrough copy

Eleventy only processes template files. Files like CSS, images and fonts must be copied across with passthrough copy:

export default function (eleventyConfig) {
  eleventyConfig.addPassthroughCopy("src/css");     // → _site/css/
  eleventyConfig.addPassthroughCopy("src/images");  // → _site/images/
  eleventyConfig.addPassthroughCopy({ "src/static/favicon.ico": "favicon.ico" });
}

Then link them as usual: <link rel="stylesheet" href="/css/style.css">. The dev server reloads when they change.

11. JavaScript templates

A .11ty.js file exports a function that returns HTML. It's handy for pages that need real logic, and uses template literals instead of a template language. src/feed.11ty.js:

export const data = {
  permalink: "/posts.json",
  eleventyExcludeFromCollections: true,
};

export default function ({ collections }) {
  const posts = collections.post.map((post) => ({
    title: post.data.title,
    url: post.url,
    date: post.date,
  }));
  return JSON.stringify(posts, null, 2);
}

This writes a JSON file listing every post, which shows that Eleventy can produce any text format, not only HTML.

12. Next steps

Handy commands

CommandWhat it does
npx @11ty/eleventyBuild the site into _site/
npx @11ty/eleventy --serveBuild, watch and serve with live reload
npx @11ty/eleventy --watchRebuild on changes without a server
npx @11ty/eleventy --dryrunRun a build without writing files
npx @11ty/eleventy --versionShow the installed version