Developing Sites with Eleventy (Static Site Generator)
Eleventy (11ty) is a Node.js static site generator created by Zachem Leatherman from Netlify. Key difference from Hugo and Jekyll: JavaScript as the configuration and extension language, support for 10+ template languages (Nunjucks, Liquid, Handlebars, Mustache, EJS, HAML, Pug, WebC), and zero client-side JavaScript by default. This makes Eleventy flexible for everything from documentation to large marketing sites.
Project architecture
mysite/
├── .eleventy.js # Configuration (or eleventy.config.js)
├── src/
│ ├── _data/ # Global data (JS, JSON, YAML)
│ │ ├── site.js
│ │ ├── navigation.json
│ │ └── team.yaml
│ ├── _includes/ # Reusable components
│ │ ├── layouts/
│ │ │ ├── base.njk
│ │ │ └── post.njk
│ │ └── components/
│ │ ├── card.njk
│ │ └── hero.njk
│ ├── blog/ # Blog collection
│ │ ├── blog.json # Cascade data for entire folder
│ │ └── *.md
│ ├── services/
│ ├── assets/
│ │ ├── css/
│ │ └── js/
│ └── index.njk
├── package.json
└── _site/ # Build output
eleventy.config.js configuration
const { EleventyHtmlBasePlugin } = require("@11ty/eleventy");
const pluginRss = require("@11ty/eleventy-plugin-rss");
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const Image = require("@11ty/eleventy-img");
const yaml = require("js-yaml");
const path = require("path");
module.exports = function(eleventyConfig) {
// Plugins
eleventyConfig.addPlugin(EleventyHtmlBasePlugin);
eleventyConfig.addPlugin(pluginRss);
eleventyConfig.addPlugin(pluginSyntaxHighlight, {
preAttributes: { tabindex: 0 }
});
// YAML parser for _data
eleventyConfig.addDataExtension("yaml,yml", contents => yaml.load(contents));
// Passthrough copy
eleventyConfig.addPassthroughCopy("src/assets/fonts");
eleventyConfig.addPassthroughCopy({ "src/assets/images/favicon": "/" });
// Filters
eleventyConfig.addFilter("dateFormat", function(date, format = "MM/dd/yyyy") {
return new Intl.DateTimeFormat("en-US").format(new Date(date));
});
eleventyConfig.addFilter("readingTime", function(content) {
const words = content.split(/\s+/).length;
const minutes = Math.ceil(words / 200);
return `${minutes} min read`;
});
eleventyConfig.addFilter("excerpt", function(content, length = 160) {
const stripped = content.replace(/<[^>]*>/g, '');
return stripped.length > length
? stripped.substring(0, length).trim() + '…'
: stripped;
});
// Async Image Shortcode
eleventyConfig.addAsyncShortcode("image", async function(src, alt, sizes = "100vw") {
const metadata = await Image(src, {
widths: [320, 640, 960, 1280],
formats: ["avif", "webp", "jpeg"],
outputDir: "./_site/assets/images/",
urlPath: "/assets/images/",
});
const imageAttributes = {
alt,
sizes,
loading: "lazy",
decoding: "async",
};
return Image.generateHTML(metadata, imageAttributes);
});
// Collections
eleventyConfig.addCollection("blog", function(collectionApi) {
return collectionApi.getFilteredByGlob("src/blog/*.md")
.filter(post => !post.data.draft)
.reverse();
});
eleventyConfig.addCollection("tagList", function(collectionApi) {
const tagSet = new Set();
collectionApi.getAll().forEach(item => {
(item.data.tags || []).forEach(tag => {
if (!["post", "all"].includes(tag)) tagSet.add(tag);
});
});
return [...tagSet].sort();
});
// Markdown settings
const markdownIt = require("markdown-it");
const markdownItAnchor = require("markdown-it-anchor");
const markdownItAttrs = require("markdown-it-attrs");
const md = markdownIt({ html: true, linkify: true, typographer: true })
.use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.ariaHidden({ placement: "after" }),
slugify: s => s.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '')
})
.use(markdownItAttrs);
eleventyConfig.setLibrary("md", md);
// Directory config
return {
dir: {
input: "src",
output: "_site",
includes: "_includes",
data: "_data",
},
htmlTemplateEngine: "njk",
markdownTemplateEngine: "njk",
templateFormats: ["md", "njk", "html"],
};
};
Nunjucks templates
{# src/_includes/layouts/base.njk #}
<!DOCTYPE html>
<html lang="{{ site.lang | default('en') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if title %}{{ title }} | {{ site.title }}{% else %}{{ site.title }}{% endif %}</title>
<meta name="description" content="{{ description | default(site.description) }}">
<meta property="og:title" content="{{ title | default(site.title) }}">
<meta property="og:url" content="{{ site.url }}{{ page.url }}">
<link rel="canonical" href="{{ site.url }}{{ page.url }}">
<link rel="stylesheet" href="/assets/css/main.css">
</head>
<body>
{% include "components/header.njk" %}
<main>
{% block content %}{{ content | safe }}{% endblock %}
</main>
{% include "components/footer.njk" %}
<script src="/assets/js/main.js" defer></script>
</body>
</html>
{# src/_includes/layouts/post.njk #}
---
layout: layouts/base.njk
---
<article class="post">
<header>
<h1>{{ title }}</h1>
<time datetime="{{ date | htmlDateString }}">
{{ date | dateFormat }}
</time>
<span class="reading-time">{{ content | readingTime }}</span>
</header>
{% if image %}
{% image image, title, "(max-width: 768px) 100vw, 1200px" %}
{% endif %}
<div class="post__body">{{ content | safe }}</div>
{% if tags %}
<ul class="tags">
{% for tag in tags %}
{% if tag != "post" %}
<li><a href="/tags/{{ tag | slug }}/">#{{ tag }}</a></li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</article>
Data Cascade
Eleventy supports data cascade — priority from global to local:
// src/_data/site.js — global data
module.exports = {
title: "Company Name",
url: process.env.SITE_URL || "https://example.com",
lang: "en",
description: "Site description",
author: {
name: "Team",
email: "[email protected]"
}
};
// src/blog/blog.json — data for entire blog/ folder
{
"layout": "layouts/post.njk",
"tags": ["post"],
"permalink": "/blog/{{ page.fileSlug }}/"
}
Front matter of individual post overrides folder data.
Pagination
{# src/blog/index.njk #}
---
title: Blog
pagination:
data: collections.blog
size: 12
alias: posts
reverse: true
permalink: "/blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber + 1 }}/{% endif %}"
---
<div class="posts-grid">
{% for post in posts %}
{% include "components/post-card.njk" %}
{% endfor %}
</div>
{% if pagination.pages.length > 1 %}
<nav class="pagination">
{% if pagination.href.previous %}
<a href="{{ pagination.href.previous }}">← Previous</a>
{% endif %}
<span>{{ pagination.pageNumber + 1 }} / {{ pagination.pages.length }}</span>
{% if pagination.href.next %}
<a href="{{ pagination.href.next }}">Next →</a>
{% endif %}
</nav>
{% endif %}
Vite integration
// vite.config.js
export default {
build: {
outDir: '_site/assets',
emptyOutDir: false,
rollupOptions: {
input: { main: 'src/assets/js/main.js' }
}
}
}
Run in parallel: concurrently "eleventy --serve" "vite build --watch"
Timeline
Site on starter template with custom content — 4–6 days. Development from scratch with custom collections, pagination, image optimization, CI/CD — 2–3 weeks. Large portal with dozens of content types, multilingual, integrations — 1–2 months.







