Разработка кастомного плагина 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 };

  // Async 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 дня. Плагин с async-операциями (изображения, внешний API) — 3–5 дней. Полноценный плагин с тестами, документацией, публикацией на npm — 1–2 недели.