Розробка кастомного плагіна Eleventy

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка кастомного плагіна Eleventy
Середня
~2-3 робочих дні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка користувацького плагіна 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, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");

    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, '&quot;')}">
      Копіювати
    </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 тижні.