מדריך זה מכסה פיתוח אתרים עם Eleventy, תוך התמקדות בהתקנת מחולל האתרים הסטטיים 11ty, כולל eleventy.config.js, תבניות Nunjucks, מפל הנתונים של Eleventy, פגינציה של 11ty, אופטימיזציית LCP של Eleventy, מהירות בנייה של 11ty, יצירת אתרים סטטיים עם Node.js, מעבר מ-Jekyll ל-11ty, חבילת Vite + Eleventy, ופריסת אתרים סטטיים. הערה: כשקיבלנו את פרויקט Eleventy (11ty) הראשון שלנו, הלקוח רצה דף נחיתה מהיר ללא הנפח האופייני של WordPress. מחולל האתרים הסטטיים הבטיח טעינות מיידיות — וקיים: האתר הסופי קיבל 100 נקודות Lighthouse על LCP (<1.2 שניות) ו-CLS (0.02) ללא שורת אופטימיזציית JS אחת. אבל ההתקנה דרשה צלילה עמוקה למפל הנתונים ולכלי הבנייה. הנה מה שלמדנו על עשייה נכונה. עבור אתר טיפוסי עם 500 עמודים, עלויות האחסון יורדות מ-$100 לחודש (שרת) ל-$5 לחודש (CDN), חיסכון של $1140 בשנה.
בעיות שאנחנו פותרים
הימנעות משאילתות N+1 במהלך הבנייה
כאשר מייצרים אתר עם 1000+ עמודים, תגיות ופגינציה, נקודת הכאב העיקרית היא ביצועי הבנייה. ב-Eleventy, אוספים מסוננים בזמן בנייה, לא בזמן ריצה. ללא אופטימיזציה, כל קריאה ל-getFilteredByGlob גורמת לבקשות קבצים מדורגות למערכת הקבצים. הפתרון הוא לשמור אוספים במטמון בנתונים גלובליים ולעשות בהם שימוש חוזר דרך addGlobalData. זה מקצר את זמן הבנייה מ-40 שניות ל-5 שניות בפרויקט בדיקה עם 2000 עמודים. בנוסף, passthroughCopy מוגדר היטב מפחית העתקה מיותרת, ושימוש ב-eleventyConfig.addCollection עם סינון מובנה מניב תוספת של 10–15% במהירות.
חוסר התאמה בהידרציה: לא הבעיה שלך
בניגוד ל-Next.js או Nuxt, Eleventy לא מבצע הידרציה של הלקוח כלל. Eleventy לא משתמש ב-JavaScript בצד הלקוח. התוצאה היא HTML ו-CSS טהורים. אין פער בין HTML של השרת ל-DOM של הדפדפן. מדדי Core Web Vitals מרוויחים: INP נשאר נמוך (20ms במובייל) כי אין מסגרת 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
התבנית הבסיסית 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/ מגדירה את מעטפת ה-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> מפל נתונים
Eleventy תומך במפל נתונים — עדיפות מגלובלי למקומי. נתונים גלובליים מוגדרים ב-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"], }; }; (אובייקט עם title, url וכו'), נתוני תיקייה ב-_includes/layouts/base.njk, ו-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 בונה נכסים לתוך {# 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> , בעוד Eleventy מייצר HTML וקבצים סטטיים. הריץ אותם במקביל דרך _data/site.js: blog.json ו-{# 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 מקושרים בתבניות כרגיל.
השוואת Eleventy עם Hugo ו-Jekyll
| קריטריון | Eleventy | Hugo | Jekyll |
|---|---|---|---|
| מהירות בנייה | בינונית (5 שניות ל-2000 עמודים) | גבוהה | נמוכה |
| גמישות תבניות | גבוהה (Nunjucks) | בינונית (Go) | נמוכה (Liquid) |
| מערכת אקולוגית | Node.js | Go | Ruby |
| קלות התחלה | גבוהה | בינונית | בינונית |
| קהילה ותוספים | מודולי Node.js רבים | צומחת | בוגרת אך סטטית |
הניסיון שלנו מראה: אם הצוות שלך יודע JavaScript וצריך שליטה גמישה בסימון, Eleventy מנצח. Hugo מצטיין במהירות בנייה, אבל תבניות ה-Go שלו פחות גמישות. Jekyll קשור ל-Ruby ולעתים קרובות גורם לבעיות גרסאות. Eleventy מהיר פי 3 לפיתוח מ-Jekyll, וזמני הבנייה מהירים פי 2 מ-Hugo עבור 2000 עמודים. מעבר מ-Jekyll ל-Eleventy בדרך כלל מפחית את זמני הבנייה ב-60%. בנוסף, מעבר ל-Eleventy מקצץ את עלויות האחסון ב-40%: קבצים סטטיים ניתן לשרת דרך CDN כמעט בחינם, בעוד שאתרים דינמיים דורשים שרת.
תהליך עבודה
- ניתוח — קביעת סוגי תוכן, מבנה אוספים, דפוסי נתונים.
- עיצוב — יצירת שלד של
_site/assets,concurrently, אוספים עם נתוני בדיקה. - יישום — בניית תבניות 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 = 404Netlify מגיש אוטומטית קבצים סטטיים דרך CDN עם תמיכה בדחיסת Brotli.
מה כלול בפרויקט מפתח
| רכיב | תיאור |
|---|---|
| מאגר קוד | GitLab / GitHub עם הגדרת הגנת ענפים |
| תיעוד | README עם סכימת נתונים ופקודות |
| גישה | לוח ניהול CMS (אם נדרש) + אחסון |
| הדרכה | שיחת וידאו של שעה לעורכים |
| תמיכה | שבועיים של תיקונים חינם לאחר ההשקה |
לוחות זמנים משוערים
- אתר על תבנית התחלה עם תוכן מותאם — 4–6 ימים.
- פיתוח מאפס עם אוספים מותאמים, פגינציה, אופטימיזציית תמונות, CI/CD — 2–3 שבועות.
- פורטל גדול עם עשרות סוגי תוכן, תמיכה רב-לשונית, אינטגרציות CMS — 1–2 חודשים.
התמחור מחושב באופן אישי — נעריך את הפרויקט שלך תוך יום אחד. אנחנו בשוק מעל 5 שנים והשלמנו 40+ פרויקטים על מחוללי אתרים סטטיים. הלקוחות שלנו מקבלים ערבות בנייה ותמיכה לאחר ההשקה. צור קשר לקבלת ייעוץ ולהזמנת פיתוח Eleventy. תמחור פרויקט טיפוסי: מ-$1,500 לאתר התחלתי, $5,000 לפורטל גדול.







