Eleventy Custom Plugin Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Custom Eleventy Plugin Development

Eleventy plugins are JavaScript functions that accept eleventyConfig and register filters, shortcodes, collections, event handlers, and transforms within it. Unlike Jekyll plugins in Ruby, everything is in a single Node.js ecosystem: you can use the entire npm and async/await without restrictions.

Plugin Anatomy

// myplugin.js — minimal plugin
module.exports = function(eleventyConfig, options = {}) {
  // Options with defaults
  const config = {
    outputDir: options.outputDir || "_site/assets",
    quality: options.quality || 80,
    formats: options.formats || ["webp", "jpeg"],
    ...options,
  };

  // Register plugin components
  eleventyConfig.addFilter("myFilter", function(value) {
    return value;
  });

  eleventyConfig.addShortcode("myShortcode", function(arg) {
    return `<span>${arg}</span>`;
  });

  // Return is optional, but you can return a public API
};

Connection:

// eleventy.config.js
const myPlugin = require("./src/_plugins/myplugin");

module.exports = function(eleventyConfig) {
  eleventyConfig.addPlugin(myPlugin, {
    quality: 85,
    formats: ["avif", "webp", "jpeg"],
  });
};

Image Optimization Plugin

Extension of the official @11ty/eleventy-img with custom logic:

// 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 for single image
  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}`);

    // Determine absolute path
    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] Failed to process image: ${src}`, e.message);
      return `<img src="${src}" alt="${alt || ""}" loading="lazy">`;
    }
  });

  // Synchronous shortcode for OG-images (pre-generated)
  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;
  });

  // Filter to get image URL of specified size
  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);
  });
};

Plugin for Generating Pages from External Data

Fetch data from API and create pages during build:

// src/_plugins/cms-pages.js
const fetch = require("node-fetch");

module.exports = function(eleventyConfig, options = {}) {
  const { apiUrl, collection, template, permalinkFn } = options;

  // Register global data from 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 returned ${response.status}`);
      return [];
    }

    const data = await response.json();
    return data.items || data;
  });

  // Transform to add metadata
  eleventyConfig.addTransform("addPageMeta", function(content, outputPath) {
    if (!outputPath?.endsWith(".html")) return content;
    // Add last-modified meta
    return content.replace(
      '</head>',
      `<meta name="last-modified" content="${new Date().toISOString()}">\n</head>`
    );
  });
};
// src/_data/services.js — alternative approach via _data
module.exports = async function() {
  const res = await fetch("https://api.example.com/services");
  const data = await res.json();

  // Cache during development
  return data;
};

Syntax Highlighting Plugin with Extended Features

// src/_plugins/codeblock.js
const { readFileSync } = require("fs");
const path = require("path");

module.exports = function(eleventyConfig) {

  // Shortcode to insert code from file
  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 escaping
    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;')}">
      Copy
    </button>
  </div>
  <pre class="language-${lang}"><code class="language-${lang}">${escaped}</code></pre>
</div>`;
  });

  // Shortcode for diff blocks
  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>`;
  });
};

Plugin for Multilingual Support

// 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;

  // Load all translations
  const translations = {};
  langs.forEach(lang => {
    const filePath = path.join(localesDir, `${lang}.yaml`);
    if (existsSync(filePath)) {
      translations[lang] = yaml.load(readFileSync(filePath, "utf8"));
    }
  });

  // Translation filter
  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] Translation not found: ${key} (${currentLang})`);
      return key;
    }

    return value;
  });

  // Shortcode for language switcher
  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="Language selection">${links.join("")}</nav>`;
  });
};

Plugin Testing

// tests/plugin.test.js (Jest)
const Eleventy = require("@11ty/eleventy");

test("img shortcode generates 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"');
});

Publishing to 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"]
}

Timeline

Simple plugin (set of filters, 2-3 shortcodes) — 1-2 days. Plugin with async operations (images, external API) — 3-5 days. Full-featured plugin with tests, documentation, and npm publishing — 1-2 weeks.