Розробка користувацького плагіна Eleventy
Плагіни Eleventy — це функції JavaScript, які приймають eleventyConfig і реєструють у ньому фільтри, shortcodes, колекції, обробники подій, трансформації. На відміну від плагінів Jekyll на Ruby, все знаходиться в одній екосистемі Node.js: ви можете використовувати весь npm і async/await без обмежень.
Анатомія плагіна
// myplugin.js — мінімальний плагін
module.exports = function(eleventyConfig, options = {}) {
// Опції зі значеннями за замовчуванням
const config = {
outputDir: options.outputDir || "_site/assets",
quality: options.quality || 80,
formats: options.formats || ["webp", "jpeg"],
...options,
};
// Реєстрація компонентів плагіна
eleventyConfig.addFilter("myFilter", function(value) {
return value;
});
eleventyConfig.addShortcode("myShortcode", function(arg) {
return `<span>${arg}</span>`;
});
// Повернення необов'язкове, але можна повернути публічний API
};
Підключення:
// eleventy.config.js
const myPlugin = require("./src/_plugins/myplugin");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(myPlugin, {
quality: 85,
formats: ["avif", "webp", "jpeg"],
});
};
Плагін оптимізації зображень
Розширення офіційної @11ty/eleventy-img з користувацькою логікою:
// src/_plugins/images.js
const Image = require("@11ty/eleventy-img");
const path = require("path");
module.exports = function(eleventyConfig, options = {}) {
const defaults = {
widths: [320, 640, 960, 1280, 1920],
formats: ["avif", "webp", "jpeg"],
outputDir: "./_site/assets/images/",
urlPath: "/assets/images/",
sharpOptions: { quality: 82 },
sharpWebpOptions: { quality: 80 },
sharpAvifOptions: { quality: 70 },
};
const cfg = { ...defaults, ...options };
// Асинхронний shortcode для одного зображення
eleventyConfig.addAsyncShortcode("img", async function(
src,
alt,
sizes = "(max-width: 768px) 100vw, 1200px",
classList = ""
) {
if (!src) throw new Error(`Missing src for img shortcode in ${this.page?.inputPath}`);
// Визначити абсолютний шлях
const srcPath = src.startsWith("http")
? src
: path.join("src", src);
try {
const metadata = await Image(srcPath, cfg);
return Image.generateHTML(metadata, {
alt: alt || "",
sizes,
loading: "lazy",
decoding: "async",
class: classList,
});
} catch (e) {
console.warn(`[img] Не вдалося обробити зображення: ${src}`, e.message);
return `<img src="${src}" alt="${alt || ""}" loading="lazy">`;
}
});
// Синхронний shortcode для OG-зображень (попередньо згенерованих)
eleventyConfig.addNunjucksAsyncShortcode("ogImage", async function(src, alt) {
const metadata = await Image(path.join("src", src), {
widths: [1200],
formats: ["jpeg"],
outputDir: cfg.outputDir,
urlPath: cfg.urlPath,
});
return metadata.jpeg[0].url;
});
// Фільтр для отримання URL зображення заданого розміру
eleventyConfig.addNunjucksAsyncFilter("imageUrl", async function(src, width, callback) {
const metadata = await Image(path.join("src", src), {
widths: [width],
formats: ["jpeg"],
outputDir: cfg.outputDir,
urlPath: cfg.urlPath,
});
callback(null, metadata.jpeg[0].url);
});
};
Плагін для створення сторінок із зовнішніх даних
Отримання даних з API та створення сторінок під час збірки:
// src/_plugins/cms-pages.js
const fetch = require("node-fetch");
module.exports = function(eleventyConfig, options = {}) {
const { apiUrl, collection, template, permalinkFn } = options;
// Реєстрація глобальних даних з API
eleventyConfig.addGlobalData(`${collection}Items`, async function() {
const response = await fetch(apiUrl, {
headers: { "Authorization": `Bearer ${process.env.CMS_TOKEN}` }
});
if (!response.ok) {
console.warn(`[cms-pages] API повернув ${response.status}`);
return [];
}
const data = await response.json();
return data.items || data;
});
// Трансформація для додавання метаданих
eleventyConfig.addTransform("addPageMeta", function(content, outputPath) {
if (!outputPath?.endsWith(".html")) return content;
// Додати last-modified meta
return content.replace(
'</head>',
`<meta name="last-modified" content="${new Date().toISOString()}">\n</head>`
);
});
};
// src/_data/services.js — альтернативний підхід через _data
module.exports = async function() {
const res = await fetch("https://api.example.com/services");
const data = await res.json();
// Кешувати під час розроблення
return data;
};
Плагін синтаксичного виділення з розширеними можливостями
// src/_plugins/codeblock.js
const { readFileSync } = require("fs");
const path = require("path");
module.exports = function(eleventyConfig) {
// Shortcode для вставки коду з файлу
eleventyConfig.addShortcode("codeFile", function(filePath, lang, highlight = "") {
const fullPath = path.join(process.cwd(), filePath);
let code;
try {
code = readFileSync(fullPath, "utf8").trim();
} catch {
return `<!-- File not found: ${filePath} -->`;
}
// Екранування для HTML
const escaped = code
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
return `<div class="code-block" data-lang="${lang}">
<div class="code-block__header">
<span class="code-block__filename">${path.basename(filePath)}</span>
<button class="code-block__copy" data-code="${escaped.replace(/"/g, '"')}">
Копіювати
</button>
</div>
<pre class="language-${lang}"><code class="language-${lang}">${escaped}</code></pre>
</div>`;
});
// Shortcode для diff-блоків
eleventyConfig.addPairedShortcode("diff", function(content, lang = "diff") {
const lines = content.split("\n").map(line => {
if (line.startsWith("+")) return `<span class="diff-add">${line}</span>`;
if (line.startsWith("-")) return `<span class="diff-remove">${line}</span>`;
return `<span class="diff-context">${line}</span>`;
});
return `<pre class="diff-block"><code>${lines.join("\n")}</code></pre>`;
});
};
Плагін для багатомовної підтримки
// src/_plugins/i18n.js
const { readFileSync, existsSync } = require("fs");
const path = require("path");
const yaml = require("js-yaml");
module.exports = function(eleventyConfig, options = {}) {
const { defaultLang = "ru", langs = ["ru", "en"], localesDir = "src/_i18n" } = options;
// Завантажити всі переводи
const translations = {};
langs.forEach(lang => {
const filePath = path.join(localesDir, `${lang}.yaml`);
if (existsSync(filePath)) {
translations[lang] = yaml.load(readFileSync(filePath, "utf8"));
}
});
// Фільтр перекладу
eleventyConfig.addFilter("t", function(key, lang) {
const currentLang = lang || this.ctx?.lang || defaultLang;
const keys = key.split(".");
let value = translations[currentLang];
for (const k of keys) {
value = value?.[k];
if (value === undefined) break;
}
if (value === undefined) {
console.warn(`[i18n] Переклад не знайдено: ${key} (${currentLang})`);
return key;
}
return value;
});
// Shortcode для перемикача мов
eleventyConfig.addShortcode("langSwitcher", function(currentUrl, currentLang) {
const links = langs.map(lang => {
const url = lang === defaultLang
? currentUrl.replace(`/${currentLang}/`, "/")
: `/${lang}${currentUrl}`;
return `<a href="${url}" hreflang="${lang}" ${lang === currentLang ? 'aria-current="true"' : ''}>${lang.toUpperCase()}</a>`;
});
return `<nav class="lang-switcher" aria-label="Вибір мови">${links.join("")}</nav>`;
});
};
Тестування плагіна
// tests/plugin.test.js (Jest)
const Eleventy = require("@11ty/eleventy");
test("img shortcode генерує picture element", async () => {
const elev = new Eleventy("./test/input", "./test/output", {
config(eleventyConfig) {
require("../src/_plugins/images")(eleventyConfig);
}
});
const result = await elev.toJSON();
const page = result.find(p => p.url === "/test/");
expect(page.content).toContain("<picture>");
expect(page.content).toContain('type="image/avif"');
});
Публікація на npm
{
"name": "eleventy-plugin-mycompany-images",
"version": "1.0.0",
"description": "Image optimization plugin for Eleventy",
"main": "src/index.js",
"peerDependencies": {
"@11ty/eleventy": "^2.0.0"
},
"keywords": ["eleventy", "plugin", "images"]
}
Терміни
Простий плагін (набір фільтрів, 2-3 shortcodes) — 1-2 дні. Плагін з асинхронними операціями (зображення, зовнішній API) — 3-5 днів. Повнофункціональний плагін з тестами, документацією та публікацією на npm — 1-2 тижні.







