Build Awesome / 11ty Filters

A collection of useful Eleventy filters for Nunjucks/Liquid via Eleventy Blades plugin.

Table of Contents

Install

Install Eleventy blades
npm install @anydigital/eleventy-blades

Then choose one of the following options:

Option A. Starting 11ty from scratch?

Consider symlinking entire eleventy.config.js:

ln -s ./node_modules/@anydigital/eleventy-blades/src/eleventy.config.js

Learn more: /build-awesome-11ty/tools/#base-config

Living examples:

Option B. Adding to existing 11ty site?

Install as a plugin in your eleventy.config.js (recommended):

import eleventyBladesPlugin from "@anydigital/eleventy-blades";

export default function (eleventyConfig) {
  eleventyConfig.addPlugin(eleventyBladesPlugin, {
    mdAutoRawTags: true,
    mdAutoNl2br: true,
    autoLinkFavicons: true,
    siteData: true,
    filters: ["attr_set", "attr_concat", ...],
  });
}

Option C. Individual imports

For advanced usage, import individual components only in eleventy.config.js:

import { siteData, mdAutoRawTags, mdAutoNl2br, autoLinkFavicons, attrSetFilter, attrConcatFilter, ... } from "@anydigital/eleventy-blades";

export default function (eleventyConfig) {
  siteData(eleventyConfig);
  mdAutoRawTags(eleventyConfig);
  mdAutoNl2br(eleventyConfig);
  autoLinkFavicons(eleventyConfig);
  attrSetFilter(eleventyConfig);
  attrConcatFilter(eleventyConfig);
  ...
}

Or use a preconfigured template:

🥷 Build Awesome Starter ↗  11ty ⁺ Tailwind ⁺ Typography ⁺ Blades


attr_concat

A filter that concatenates values to an attribute array, returning a new object with the combined array. Useful for adding items to arrays like tags, classes, or other list-based attributes.

Why use this? When working with objects that have array attributes (like tags), you often need to add additional values without mutating the original object. The attr_concat filter provides a clean way to combine existing array values with new ones, automatically handling duplicates.

Features:

Example: Add tags to a post object in .njk:

{% set enhancedPost = post | attr_concat('tags', ['featured', 'popular']) %}

PRO Example: Add scripts and styles to the site object in .liquid:

{% capture _ %}[
  "https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-tomorrow.min.css",
  "https://cdn.jsdelivr.net/npm/prismjs@1/plugins/treeview/prism-treeview.min.css",
  "https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7/css/all.min.css",
  "/styles.css"
]{% endcapture %}
{% assign site = site | attr_concat: 'styles', _ %}

{% capture _ %}[
  "https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js",
  "https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js",
  "https://cdn.jsdelivr.net/npm/prismjs@1/plugins/treeview/prism-treeview.min.js"
]{% endcapture %}
{% assign site = site | attr_concat: 'scripts', _ %}

How it works
/**
 * Concatenate values to an attribute array
 *
 * This function takes an object, an attribute name, and values to append.
 * It returns a new object with the attribute as a combined array of unique items.
 *
 * @param {Object} obj - The object to modify
 * @param {string} attr - The attribute name
 * @param {Array|string|*} values - Values to concatenate (array, JSON string array, or single value)
 * @returns {Object} A new object with the combined unique array
 */
export function attrConcat(obj, attr, values) {
  // Get the existing attribute value, default to empty array if not present
  const existingArray = obj?.[attr] || [];

  // Check if existing value is an array, convert if not
  if (!Array.isArray(existingArray)) {
    console.error(`attrConcat: Expected ${attr} to be an array, got ${typeof existingArray}`);
  }

  // Process the values argument
  let valuesToAdd = [];
  if (Array.isArray(values)) {
    valuesToAdd = values;
  } else if (typeof values === "string" && values.length >= 2 && values.at(0) == "[" && values.at(-1) == "]") {
    // Try to parse as JSON array
    try {
      const parsed = JSON.parse(values);
      if (Array.isArray(parsed)) {
        valuesToAdd = parsed;
      } else {
        valuesToAdd = [values];
      }
    } catch {
      // Not valid JSON, treat as single value
      valuesToAdd = [values];
    }
  } else {
    // If it's a single value, wrap it in an array
    valuesToAdd = [values];
  }

  // Combine arrays and remove duplicates using Set
  const combinedArray = [...new Set([...existingArray, ...valuesToAdd])];

  // Return a new object with the combined array
  return {
    ...obj,
    [attr]: combinedArray,
  };
}

/**
 * attr_concat filter - Concatenate values to an attribute array
 *
 * This filter takes an object, an attribute name, and values to append.
 * It returns a new object with the attribute as a combined array of unique items.
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function attrConcatFilter(eleventyConfig) {
  eleventyConfig.addFilter("attr_concat", attrConcat);
}

attr_includes

A filter that filters a list of items by checking if an attribute array includes a target value. Supports nested attribute names using dot notation.

Why use this? When working with Eleventy collections, you often need to filter items based on tags or other array attributes in front matter. The attr_includes filter provides a flexible way to filter by any array attribute, with support for nested properties using dot notation.

Example: Get all posts that include #javascript tag

{% set js_posts = collections.all | attr_includes('data.tags', '#javascript') %}

{% for post in js_posts %}
  <h2>{{ post.data.title }}</h2>
{% endfor %}

How it works
import lodash from "@11ty/lodash-custom";
const { get } = lodash;

/**
 * Core logic for filtering collection items by attribute value
 *
 * This function takes a collection, an attribute name, and a target value,
 * and returns items where the attribute matches the target value.
 * If the attribute is an array, it checks if the array includes the target value.
 *
 * Supports nested attribute names using dot notation (e.g., "data.tags").
 *
 * @param {Array} collection - The collection to filter
 * @param {string} attrName - The attribute name to check (supports dot notation for nested properties)
 * @param {*} targetValue - The value to match against
 * @returns {Array} Filtered collection
 */
export function attrIncludes(collection, attrName, targetValue) {
  // If no targetValue, return original collection
  if (!targetValue) {
    return collection;
  }

  return collection.filter((item) => {
    // Get the attribute value from the item (supports nested paths like "data.tags")
    const attrValue = get(item, attrName);

    // If the attribute is an array, check if it includes the target value
    if (Array.isArray(attrValue)) {
      return attrValue.includes(targetValue);
    }

    // Otherwise skip this item
    return false;
  });
}

/**
 * Registers the attr_includes filter with Eleventy
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function attrIncludesFilter(eleventyConfig) {
  eleventyConfig.addFilter("attr_includes", attrIncludes);
}

attr_set

A filter that creates a new object with an overridden attribute value. This is useful for modifying data objects in templates without mutating the original. Or even constructing an object from scratch.

Example: How to pass object(s) as argument(s) to a filter in .liquid?

{% assign _ctx = null | attr_set: 'collections', collections %}
{{ ... | renderContent: 'liquid,md', _ctx }}

How it works
/**
 * attr_set filter - Override an attribute and return the object
 *
 * This filter takes an object, a key, and a value, and returns a new object
 * with the specified attribute set to the given value.
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */

/**
 * Core attr_set function - Override an attribute and return a new object
 *
 * @param {Object} obj - The object to modify
 * @param {string} key - The attribute name to set
 * @param {*} value - The value to set for the attribute
 * @returns {Object} A new object with the specified attribute set to the given value
 */
export function attrSet(obj, key, value) {
  return {
    ...obj,
    [key]: value,
  };
}

export function attrSetFilter(eleventyConfig) {
  eleventyConfig.addFilter("attr_set", attrSet);
}

fetch

A filter that fetches content from remote URLs or local files. For remote URLs, it uses @11ty/eleventy-fetch to download and cache files. For local paths, it reads files relative to the input directory.

Why use this? When building static sites, you often need to include content from external sources or reuse content from local files. The fetch filter provides a unified way to retrieve content from both remote URLs and local files, with automatic caching for remote resources to improve build performance.

Requirements: This filter requires the @11ty/eleventy-fetch package to be installed:

npm install @11ty/eleventy-fetch

NOTE: If @11ty/eleventy-fetch is not installed, this filter will not be available. The plugin automatically detects whether the package is installed and only enables the filter if it's present.

Features:

Use Cases:

NOTE: The filter returns raw text content. Use Eleventy's built-in filters like | safe, | markdown, or | fromJson to process the content as needed.

Examples:

{# Fetch and display remote content #}
{% set readme = "https://raw.githubusercontent.com/user/repo/main/README.md" | fetch %}
<div class="readme">
  {{ readme | markdown | safe }}
</div>

{# Fetch JSON data from API #}
{% set data = "https://api.example.com/data.json" | fetch %}
{% set items = data | fromJson %}
{% for item in items %}
  <p>{{ item.title }}</p>
{% endfor %}

{# Include local file content #}
{% set changelog = "CHANGELOG.md" | fetch %}
{{ changelog | markdown | safe }}

{# Fetch CSS from CDN and inline it #}
<style>
  {{ "https://cdn.example.com/styles.css" | fetch }}
</style>

{# Reuse content across pages #}
{% set sharedContent = "_includes/shared/footer.html" | fetch %}
{{ sharedContent | safe }}

How it works
import EleventyFetch from "@11ty/eleventy-fetch";
import path from "path";
import fs from "fs/promises";

/**
 * fetch filter - Fetch a URL or local file and return its raw content
 *
 * This filter takes a URL or local file path. For URLs, it downloads them
 * using eleventy-fetch to the input directory's _downloads folder.
 * For local paths, it reads them relative to the input directory.
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function fetchFilter(eleventyConfig) {
  eleventyConfig.addFilter("fetch", async function (url) {
    if (!url) {
      throw new Error("fetch filter requires a URL or path");
    }

    // Get the input directory from Eleventy config
    const inputDir = this.eleventy.directories.input;

    // Check if it's a URL or local path
    const isUrl = url.startsWith("http://") || url.startsWith("https://");

    try {
      if (isUrl) {
        // Handle remote URLs with eleventy-fetch
        const cacheDirectory = path.join(inputDir, "_downloads");
        const content = await EleventyFetch(url, {
          duration: "1d", // Cache for 1 day by default
          type: "text", // Return as text
          directory: cacheDirectory,
        });
        return content.toString(); // toString() handles bytes cache (inside eleventyComputed)
      } else {
        // Handle local file paths relative to input directory
        const filePath = path.join(inputDir, url);
        const content = await fs.readFile(filePath, "utf-8");
        return content;
      }
    } catch (error) {
      throw new Error(`Failed to fetch ${url}: ${error.message}`);
    }
  });
}

if

An inline conditional/ternary operator filter that returns one value if a condition is truthy, and another if it's falsy. Similar to Nunjucks' inline if syntax, it is especially useful in .liquid templates.

Features:

Examples:

{# Basic usage (defaults to empty string) #}
<div class="{{ 'active' | if: isActive | default: 'inactive' }}">Status</div>

{# Toggle CSS classes #}
<button class="{{ 'btn-primary' | if: isPrimary | default: 'btn-secondary' }}">
  Click me
</button>

{# Display different text #}
<p>{{ 'Online' | if: user.isOnline, 'Offline' }}</p>

{# Use with boolean values #}
{% set isEnabled = true %}
<div>{{ 'Enabled' | if: isEnabled, 'Disabled' }}</div>

{# Conditional attribute values #}
<input type="checkbox" {{ 'checked' | if: isChecked }}>

{# With numeric values #}
<span class="{{ 'has-items' | if: items.length }}">
  {{ items.length }} items
</span>

{# Chain with other filters #}
{% set cssClass = 'featured' | if: post.featured | upper %}

How it works
/**
 * if utility function - Ternary/conditional helper
 *
 * Returns trueValue if condition is truthy, otherwise returns falseValue.
 * Similar to Nunjucks' inline if: `value if condition else other_value`
 *
 * @param {*} trueValue - The value to return if condition is truthy
 * @param {*} condition - The condition to evaluate
 * @param {*} falseValue - The value to return if condition is falsy (default: empty string)
 * @returns {*} Either trueValue or falseValue based on condition
 */
export function iff(trueValue, condition, falseValue = "") {
  // Treat empty objects {} as falsy
  if (condition && typeof condition === "object" && !Array.isArray(condition) && Object.keys(condition).length === 0) {
    return falseValue;
  }
  return !!condition ? trueValue : falseValue;
}

/**
 * if filter - Inline conditional/ternary operator for templates
 *
 * This filter provides a simple inline if/else similar to Nunjucks.
 *
 * Usage in Liquid templates:
 *   {{ "Active" | if: isActive, "Inactive" }}
 *   {{ "Yes" | if: condition }}
 *   {{ someValue | if: test, otherValue }}
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function ifFilter(eleventyConfig) {
  eleventyConfig.addFilter("if", iff);
}

merge

A filter that merges arrays or objects together, similar to Twig's merge filter. For arrays, it concatenates them. For objects, it performs a shallow merge where later values override earlier ones.

Why use this? When working with data in templates, you often need to combine multiple arrays or objects. The merge filter provides a clean way to merge data structures without writing custom JavaScript, making it easy to combine collections, merge configuration objects, or aggregate data from multiple sources.

Examples:

{# Merge configuration objects #}
{% set defaultConfig = { theme: 'light', lang: 'en' } %}
{% set userConfig = { theme: 'dark' } %}
{% set finalConfig = defaultConfig | merge(userConfig) %}

{# Result: { theme: 'dark', lang: 'en' } #}
{# Merge page metadata with defaults #}
{% set defaultMeta = {
  author: 'Site Admin',
  category: 'general',
  comments: false
} %}
{% set pageMeta = defaultMeta | merge(page.data) %}

How it works
/**
 * Merge objects together
 *
 * Shallow merges objects (later values override earlier ones)
 *
 * @param {Object} first - The first object
 * @param {...Object} rest - Additional objects to merge
 * @returns {Object} The merged result
 */
export function merge(first, ...rest) {
  // If first argument is null or undefined, treat as empty object
  if (first === null || first === undefined) {
    first = {};
  }

  // Only support objects
  if (typeof first === "object" && !Array.isArray(first)) {
    // Merge objects using spread operator (shallow merge)
    return rest.reduce(
      (acc, item) => {
        if (item !== null && typeof item === "object" && !Array.isArray(item)) {
          return { ...acc, ...item };
        }
        return acc;
      },
      { ...first },
    );
  }

  // If first is not an object, return empty object
  return {};
}

/**
 * merge filter - Merge objects together
 *
 * This filter merges objects, similar to Twig's merge filter.
 *
 * Usage in templates:
 *   {{ obj1 | merge(obj2) }}
 *   {{ obj1 | merge(obj2, obj3) }}
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function mergeFilter(eleventyConfig) {
  eleventyConfig.addFilter("merge", merge);
}

remove_tag

A filter that removes a specified HTML element from provided HTML content. It removes the tag along with its content, including self-closing tags.

Why use this? When working with content from external sources or user-generated content, you may need to strip certain HTML tags for security or presentation purposes. The remove_tag filter provides a simple way to remove unwanted tags like <script>, <style>, or any other HTML elements from your content.

Features:

Security note: While this filter can help sanitize HTML content, it should not be relied upon as the sole security measure. For critical security requirements, use a dedicated HTML sanitization library on the server side before content reaches your templates.

Example: Remove all script tags from content

{% set cleanContent = htmlContent | remove_tag('script') %}

{{ cleanContent | safe }}

How it works
/**
 * Remove specified HTML element from provided HTML
 *
 * @param {string} html - The HTML content to process
 * @param {string} tagName - The tag name to remove
 * @returns {string} The HTML with the specified tag removed
 */
export function removeTag(html, tagName) {
  if (!html || typeof html !== "string") {
    return html;
  }

  if (typeof tagName !== "string" || !tagName) {
    return html;
  }

  // Escape special regex characters in tag name
  const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

  // Remove opening and closing tags along with their content
  // This regex matches: <tag attributes>content</tag>
  const regex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>.*?<\\/${escapedTag}>`, "gis");
  let result = html.replace(regex, "");

  // Also remove self-closing tags: <tag />
  const selfClosingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?\\s*\\/?>`, "gi");
  result = result.replace(selfClosingRegex, "");

  return result;
}

/**
 * remove_tag filter - Remove specified HTML element from provided HTML
 *
 * Usage in templates:
 *   {{ htmlContent | remove_tag('script') }}
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function removeTagFilter(eleventyConfig) {
  eleventyConfig.addFilter("remove_tag", removeTag);
}

section

A filter that extracts a named section from content marked with HTML comments. This is useful for splitting a single content file (like a Markdown post) into multiple parts that can be displayed and styled independently in your templates.

Usage:

  1. Mark sections in your content file (e.g., post.md):

⚠️ NOTE: The ¡ symbol is used instead of ! only to give examples below. Use ! in your actual content files.

# My Post

<!--section:intro-->

This is the introduction that appears at the top of the page.

<!--section:main-->

This is the main body of the post with all the details.

<!--section:summary,sidebar-->

This content appears in both the summary and the sidebar!
  1. Use the filter in your templates:
{# Get the intro section #}
<div class="page-intro">
  {{ content | section('intro') | safe }}
</div>

{# Get the main section #}
<article>
  {{ content | section('main') | safe }}
</article>

{# Get the sidebar section #}
<aside>
  {{ content | section('sidebar') | safe }}
</aside>

Features:

Syntax Rules:


How it works
/**
 * Extract a named section from content marked with HTML comments
 *
 * @param {string} content - The content to process
 * @param {string} sectionName - The section name(s) to extract
 * @returns {string} The extracted section content
 */
export function section(content, sectionName) {
  if (!content || typeof content !== "string") {
    return content;
  }

  if (typeof sectionName !== "string" || !sectionName) {
    return "";
  }

  // Normalize section name for comparison (trim whitespace)
  const targetName = sectionName.trim().toLowerCase();

  // Regex to match section markers with content up to the next section or end of string
  // Captures: (1) section names, (2) content until next section marker or end
  const sectionRegex = /<[!]--section:([^>]+)-->([\s\S]*?)(?=<[!]--section|$)/g;

  let results = [];
  let match;

  // Find all sections
  while ((match = sectionRegex.exec(content)) !== null) {
    const namesStr = match[1];
    const sectionContent = match[2];
    const names = namesStr.split(",").map((n) => n.trim().toLowerCase());

    // Check if any of the names match the target
    if (names.includes(targetName)) {
      results.push(sectionContent);
    }
  }

  // Join all matching sections
  return results.join("");
}

/**
 * section filter - Extract a named section from content
 *
 * Usage in templates:
 *   {{ content | section('intro') }}
 *   {{ content | section('footer') }}
 *
 * Content format:
 *   <!--section:intro-->
 *   This is the intro content
 *   <!--section:main-->
 *   This is the main content
 *   <!--section:footer,sidebar-->
 *   This appears in both footer and sidebar sections
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function sectionFilter(eleventyConfig) {
  eleventyConfig.addFilter("section", section);
}

strip_tag

A filter that strips a specified HTML element from content while keeping its inner content intact. Only the opening and closing tags are removed; everything inside the tag is preserved in place.

Why use this? When rendering HTML from a CMS or external source you sometimes need to unwrap a specific element (e.g. remove a wrapping <div> or <section>) without losing the content it contains. Unlike remove_tag, which discards the entire element and its content, strip_tag surgically removes only the tags themselves.

Features:

Example: Unwrap a wrapping <div> from content

{% set unwrapped = htmlContent | strip_tag('div') %}

{{ unwrapped | safe }}

Input:

<div class="wrapper">
  <p>Hello</p>
  <p>World</p>
</div>

Output:

<p>Hello</p>
<p>World</p>

How it works
/**
 * Strip a specified HTML element from provided HTML, keeping its inner content
 *
 * @param {string} html - The HTML content to process
 * @param {string} tagName - The tag name to strip (opening/closing tags removed, inner content kept)
 * @returns {string} The HTML with the specified tag stripped but its inner content preserved
 */
export function stripTag(html, tagName) {
  if (!html || typeof html !== "string") {
    return html;
  }

  if (typeof tagName !== "string" || !tagName) {
    return html;
  }

  // Escape special regex characters in tag name
  const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

  // Remove opening tags (with optional attributes): <tag> or <tag attr="val">
  const openingRegex = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>`, "gi");
  let result = html.replace(openingRegex, "");

  // Remove closing tags: </tag>
  const closingRegex = new RegExp(`<\\/${escapedTag}>`, "gi");
  result = result.replace(closingRegex, "");

  return result;
}

/**
 * strip_tag filter - Strip a specified HTML element, keeping its inner content
 *
 * Usage in templates:
 *   {{ htmlContent | strip_tag('div') }}
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function stripTagFilter(eleventyConfig) {
  eleventyConfig.addFilter("strip_tag", stripTag);
}

unindent


How it works
/**
 * Remove the minimal common indentation from a multi-line string
 *
 * Finds the smallest leading-whitespace count across all non-empty lines
 * and strips that many characters from the beginning of every line.
 *
 * @param {string} content - The input string
 * @returns {string} The unindented string
 */
export function unindent(content) {
  const lines = String(content ?? "").split("\n");
  const minIndent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^(\s*)/)[1].length));
  return lines.map((l) => l.slice(minIndent)).join("\n");
}

/**
 * unindent filter - Remove minimal common indentation
 *
 * Strips the smallest leading-whitespace indent shared by all non-empty
 * lines, useful for dedenting captured or indented template blocks.
 *
 * Usage in templates:
 *   {{ content | unindent }}
 *
 * @param {Object} eleventyConfig - The Eleventy configuration object
 */
export function unindentFilter(eleventyConfig) {
  eleventyConfig.addFilter("unindent", unindent);
}