Build Awesome / 11ty Filters
A collection of useful Eleventy filters for Nunjucks/Liquid via Eleventy Blades plugin.
Table of Contents- Install
- attr_concat
- attr_includes
- attr_set
- fetch
- if
- merge
- remove_tag
- section
- strip_tag
- Example: Unwrap a wrapping from content
- unindent
Install
Install Eleventy blades
npm install @anydigital/eleventy-bladesThen 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.jsLearn 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_concatA 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_concatfilter provides a clean way to combine existing array values with new ones, automatically handling duplicates.Features:
- Non-mutating: Creates a new object, leaving the original unchanged
- Automatically removes duplicates using Set
- Handles multiple input types: arrays, JSON string arrays (killer feature for
.liquid), or single values - Creates the attribute as an empty array if it doesn't exist
- Logs an error if the existing attribute is not an array
TBC:Supports nested attributes (e.g.,data.tags)
Example: Add tags to a post object in
.njk:{% set enhancedPost = post | attr_concat('tags', ['featured', 'popular']) %}PROExample: Add scripts and styles to thesiteobject 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_includesA 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_includesfilter provides a flexible way to filter by any array attribute, with support for nested properties using dot notation.Example: Get all posts that include
#javascripttag{% 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_setA 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); }
fetchA filter that fetches content from remote URLs or local files. For remote URLs, it uses
@11ty/eleventy-fetchto 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
fetchfilter 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-fetchpackage to be installed:npm install @11ty/eleventy-fetchNOTE:If@11ty/eleventy-fetchis 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:
- Supports a URL (starting with
http://orhttps://) or a local file path (relative to the input directory):- Remote URLs: Downloads and caches content using
@11ty/eleventy-fetch- Caches files for 1 day by default
- Stores cached files in
[input-dir]/_downloads/directory - Automatically revalidates after cache expires
- Local files: Reads files relative to the Eleventy input directory
- No caching needed for local files
- Supports any file type that can be read as text
- Remote URLs: Downloads and caches content using
- Error handling: Throws descriptive errors if fetching fails
- Conditional loading: Only available when
@11ty/eleventy-fetchis installed
Use Cases:
- Fetch content from external APIs during build time
- Include README files from GitHub repositories
- Reuse content from local files across multiple pages
- Download and inline external CSS or JavaScript
- Fetch data from headless CMS or external data sources
- Include shared content snippets without using Eleventy's include syntax
NOTE:The filter returns raw text content. Use Eleventy's built-in filters like| safe,| markdown, or| fromJsonto 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}`); } }); }
ifAn 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
.liquidtemplates.Features:
- Returns
trueValueif condition is truthy, otherwise returnsfalseValue - Treats empty objects
{}as falsy - Default
falseValueis an empty string if not provided - Works with any data type for values
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); }
mergeA 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
mergefilter 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_tagA 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_tagfilter provides a simple way to remove unwanted tags like<script>,<style>, or any other HTML elements from your content.Features:
- Removes both opening and closing tags along with their content
- Handles self-closing tags (e.g.,
<br />,<img />) - Handles tags with attributes
- Case-insensitive matching
- Non-destructive: Returns new string, doesn't modify original
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); }
sectionA 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:
- 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!- 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:
- Multiple names: A single section can have multiple names separated by commas:
<!--section:name1,name2--> - Case-insensitive: Section names are matched without regard to case
- Multiple occurrences: If a section name appears multiple times, the filter concatenates all matching sections
- Non-destructive: Returns extracted content without modifying the original input
- EOF support: Sections continue until the next
<!--section*-->marker or the end of the file
Syntax Rules:
- Sections start with:
<!--section:NAME-->or<!--section:NAME1,NAME2--> - Sections end at the next
<!--section*-->marker or end of file - Whitespace around names and inside comments is automatically trimmed
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_tagA 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. Unlikeremove_tag, which discards the entire element and its content,strip_tagsurgically removes only the tags themselves.Features:
- Removes only the opening and closing tags — inner content is preserved
- Handles tags with any attributes
- Strips all occurrences of the tag, including nested ones
- Case-insensitive matching
- Non-destructive: Returns a new string, leaves the original unchanged
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); }
- Example: Unwrap a wrapping