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, "&")
.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, '"')}">
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.







