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.
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.
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"
}
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
| Source file | Output file | URL |
|---|---|---|
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/ |
| Extension | Language |
|---|---|
.md | Markdown (also processed with Liquid by default) |
.html | HTML (also processed with Liquid by default) |
.njk | Nunjucks |
.liquid | Liquid |
.11ty.js | JavaScript |
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:
| Key | Purpose |
|---|---|
layout | Wrap the page in a layout from _includes/ |
permalink | Change the output URL (or false to skip writing the file) |
tags | Add the page to one or more collections |
date | Set the page date (used for sorting) |
pagination | Generate multiple pages from data |
eleventyExcludeFromCollections | Keep the page out of collections |
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.
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.
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!
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"
}
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.
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()
);
}
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 %}.
When the same key is set in several places, the more specific source wins. From highest to lowest priority:
eleventyComputed)first-post.json next to first-post.md)posts/posts.json), then parent folders_data/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>
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>
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.
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.
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.
@11ty/eleventy-img (responsive images), @11ty/eleventy-plugin-rss (feeds) and @11ty/eleventy-navigation (menus).eleventyConfig.addPlugin(pluginName)._site/ folder to any static host, such as Netlify, Cloudflare Pages, Vercel or GitHub Pages.| Command | What it does |
|---|---|
npx @11ty/eleventy | Build the site into _site/ |
npx @11ty/eleventy --serve | Build, watch and serve with live reload |
npx @11ty/eleventy --watch | Rebuild on changes without a server |
npx @11ty/eleventy --dryrun | Run a build without writing files |
npx @11ty/eleventy --version | Show the installed version |