Отметим: когда мы взяли первый проект на Eleventy (11ty), клиент хотел быстрый лендинг без типичной "тяжеловесности" WordPress. Статический генератор обещал мгновенную загрузку — и не подвёл: итоговый сайт набрал 100 баллов Lighthouse по LCP (<1.2с) и CLS (0.02) без единой строчки JS-оптимизаций. Но конфигурация потребовала погружения в каскад данных и настройку сборщика. Делимся опытом, как делать правильно.
Проблемы, которые решаем
Как избежать N+1 запросов при сборке
При генерации сайта на 1000+ страниц с тегами и пагинацией основная боль — производительность сборки. В Eleventy коллекции фильтруются на этапе билда, а не рантайма. Если не оптимизировать, каждый проход getFilteredByGlob вызывает цепные запросы к файловой системе. Решение — кешировать коллекции в глобальных данных и переиспользовать через addGlobalData. Это снижает время билда с 40 до 5 секунд на тестовом проекте с 2000 страниц. Дополнительно: грамотно настроенный passthroughCopy уменьшает число холостых копирований, а использование eleventyConfig.addCollection с фильтрацией на месте даёт прирост скорости ещё на 10–15%.
Почему hydration mismatch не ваша проблема
В отличие от Next.js или Nuxt, Eleventy вообще не гидратирует клиент. Eleventy не использует клиентский JavaScript. Результат — чистый HTML и CSS. Нет расхождений между HTML сервера и DOM браузера. Core Web Vitals выигрывают: INP остаётся низким (20 мс на мобильных), так как нет JavaScript-фреймворка на странице. Для интернет-магазина на 5000 товаров это даёт LCP <1.2с без дополнительных усилий. Отсутствие гидрации также исключает класс ошибок, связанных с несоответствием начального состояния — вы просто получаете готовый HTML как есть.
Как мы это делаем
Архитектура проекта
Мы придерживаемся модульной структуры, где _data/, _includes/ и коллекции отделены.
mysite/
├── .eleventy.js
├── src/
│ ├── _data/
│ │ ├── site.js
│ │ ├── navigation.json
│ │ └── team.yaml
│ ├── _includes/
│ │ ├── layouts/
│ │ │ ├── base.njk
│ │ │ └── post.njk
│ │ └── components/
│ │ ├── card.njk
│ │ └── hero.njk
│ ├── blog/
│ │ ├── blog.json
│ │ └── *.md
│ ├── services/
│ ├── assets/
│ │ ├── css/
│ │ └── js/
│ └── index.njk
├── package.json
└── _site/
Конфигурация eleventy.config.js
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) {
// Плагины
eleventyConfig.addPlugin(EleventyHtmlBasePlugin);
eleventyConfig.addPlugin(pluginRss);
eleventyConfig.addPlugin(pluginSyntaxHighlight, {
preAttributes: { tabindex: 0 }
});
// Парсер YAML для _data
eleventyConfig.addDataExtension("yaml,yml", contents => yaml.load(contents));
// Passthrough copy
eleventyConfig.addPassthroughCopy("src/assets/fonts");
eleventyConfig.addPassthroughCopy({ "src/assets/images/favicon": "/" });
// Фильтры
eleventyConfig.addFilter("dateFormat", function(date, format = "dd.MM.yyyy") {
return new Intl.DateTimeFormat("ru-RU").format(new Date(date));
});
eleventyConfig.addFilter("readingTime", function(content) {
const words = content.split(/\s+/).length;
const minutes = Math.ceil(words / 200);
return `${minutes} мин`;
});
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);
});
// Коллекции
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
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);
// Конфиг директорий
return {
dir: {
input: "src",
output: "_site",
includes: "_includes",
data: "_data",
},
htmlTemplateEngine: "njk",
markdownTemplateEngine: "njk",
templateFormats: ["md", "njk", "html"],
};
};
Шаблоны Nunjucks
Базовый шаблон _includes/layouts/base.njk определяет обёртку HTML с SEO-тегами, ссылками на CSS и JS.
{# src/_includes/layouts/base.njk #}
<!DOCTYPE html>
<html lang="{{ site.lang | default('ru') }}">
<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>
Data Cascade
Eleventy поддерживает каскад данных — приоритет от глобального к локальному. Глобальные данные определяются в _data/site.js (объект с title, url и т.д.), данные папки — в blog.json, а front matter отдельного поста перекрывает всё. Такая иерархия позволяет гибко управлять контентом без дублирования.
Пагинация
{# src/blog/index.njk #}
---
title: Блог
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 }}">← Назад</a>
{% endif %}
<span>{{ pagination.pageNumber + 1 }} / {{ pagination.pages.length }}</span>
{% if pagination.href.next %}
<a href="{{ pagination.href.next }}">Вперёд →</a>
{% endif %}
</nav>
{% endif %}
Интеграция с Vite
Vite собирает ассеты в _site/assets, а Eleventy — HTML и статику. Запускаем параллельно через concurrently: eleventy --serve и vite build --watch. Файлы Vite линкуются в шаблонах как обычно.
Почему стоит выбрать Eleventy вместо Hugo или Jekyll?
| Критерий | Eleventy | Hugo | Jekyll |
|---|---|---|---|
| Скорость билда | Средняя (5с на 2000 стр.) | Высокая | Низкая |
| Гибкость шаблонов | Высокая (Nunjucks) | Средняя (Go) | Низкая (Liquid) |
| Экосистема | Node.js | Go | Ruby |
| Простота старта | Высокая | Средняя | Средняя |
| Сообщество и плагины | Много Node.js-модулей | Растёт | Зрелое, но статичное |
Наш опыт показывает: если команда владеет JS и нужен гибкий контроль над разметкой — Eleventy побеждает. Hugo силён в скорости билда, но его Golang-шаблоны менее гибки. Jekyll завязан на Ruby и часто вызывает конфликты версий. Eleventy в 2–3 раза быстрее в разработке на стартовом проекте благодаря знакомому JS-стеку и плагинам. Помимо этого, переход на Eleventy сокращает расходы на хостинг на 40%: статические файлы можно раздавать через CDN практически бесплатно, тогда как для динамических сайтов нужен сервер.
Процесс работы
- Аналитика — определяем типы контента, структуру коллекций, паттерн данных.
- Проектирование — создаём каркас
_data/,_includes/, коллекции с тестовыми данными. - Реализация — верстаем шаблоны Nunjucks, настраиваем плагины (RSS, изображения, подсветка кода).
- Тестирование — проверяем билд на 500+ страницах, замеряем LCP/CLS Lighthouse.
- Деплой — настраиваем CI/CD (Vercel, Netlify, Cloudflare Pages), подключаем CDN.
Пример конфигурации деплоя для Netlify
Укажите в `netlify.toml` команды сборки и паблиш-директорию:[build]
command = "npm run build"
publish = "_site"
[[redirects]]
from = "/*"
to = "/404.html"
status = 404
Netlify автоматически раздаёт статику через CDN с поддержкой Brotli-сжатия.
Что входит в работу под ключ
| Компонент | Описание |
|---|---|
| Репозиторий | GitLab / GitHub с настройкой branch protection |
| Документация | README со схемой данных и командами |
| Доступы | Админка CMS (если нужна) + хостинг |
| Обучение | 1 час видео-звонка для редакторов |
| Поддержка | 2 недели бесплатных правок после сдачи |
Сроки ориентировочно
- Сайт на стартовом шаблоне с кастомным контентом — 4–6 дней.
- Разработка с нуля с кастомными коллекциями, пагинацией, оптимизацией изображений, CI/CD — 2–3 недели.
- Крупный портал с десятками типов контента, мультиязычностью, интеграциями с CMS — 1–2 месяца.
Стоимость рассчитывается индивидуально — оценим проект за 1 день. На рынке более 5 лет, реализовали 40+ проектов на статических генераторах. Наши клиенты получают гарантию на билд и поддержку после запуска. Оценим ваш проект за 1 день — свяжитесь с нами. Получите консультацию инженера, закажите разработку на Eleventy.







