Eleventy & Nunjucks Tricks

Table of Contents


Getting Started Tricks

Not sure where to begin? Start with a minimal template:

Best Minimal Starters as of Feb 2026

Repo Stats
npm Dependencies Custom CSS
Custom JS
Custom Templates
CMS Included?
/anydigital/bladeswitch

3 ♻️ 0 ♻️ uses pico.css & bricks[.css] 0 ♻️ uses eleventy-bricks ~15 lines ♻️ uses bricks[.liquid] Sveltia CMS
/anydigital/sveleven

~10 ~30 lines ♻️ uses tailwind & bricks[.css] 0 ♻️ uses eleventy-bricks ~15 lines ♻️ uses bricks[.njk] Sveltia CMS
/11ty/eleventy-base-blog

~10 ~300 lines ~200 lines ~200 lines
/MWDelaney/zeropoint

~15 ~200 lines ~300 lines ~250 lines not included, but documented
Not up-to-date:
/danurbanowicz/eleventy-sveltia-cms-starter

~10 ~150 lines ~130 lines ~250 lines Sveltia CMS
Not so minimal:
/scottsweb/elva

~25 ⚠️ ~1000 lines ⚠️ ~30 files ⚠️ ~20 files Front Matter CMS (local use only)
/madrilene/eleventy-excellent

~40 ⚠️ ~40 files ⚠️ Tailwind config for CUBE CSS ~40 files ⚠️ ~40 files ⚠️
/adamstddrd/grease

~5 ♻️ ~25 files ⚠️ ~15 files ~15 files
/nhoizey/pack11ty

~15 ~30 files ⚠️ ~10 files ~10 files

Command Line Tricks

Find and kill 11ty processes

ps aux | grep eleventy
pkill -f eleventy

You can even combine it with other processes hanging around:

ps aux | grep -E 'eleventy|tailwind|.bin/serve'
pkill -f tailwind
pkill -f .bin/serve

🥷 Reusable 11ty npm scripts via npm workspace
from /anydigital/eleventy-bricks

This package provides a pre-configured do folder setup that helps organize your development workflow using npm workspaces. The do folder contains scripts for building and running your Eleventy project.

Installation:

  1. Install /anydigital/eleventy-bricks to reuse pre-defined 11ty scripts from there:
npm install @anydigital/eleventy-bricks
  1. Create a helper folder do to symlink the do/package.json within:
mkdir do
cd ./do
ln -s ../node_modules/@anydigital/eleventy-bricks/src/do/package.json
  1. Finally register do folder as npm workspace in your root package.json:
{
  ...
  "workspaces": ["do"],
  "scripts": {
    "start": "npm -w do run start",
    "stage": "npm -w do run stage",
    "build": "npm -w do run build"
  },
  ...
}

Done! 🎉 Now you can run:

Living example: /anydigital/sveleven

Benefits:


Configuration Tricks

🥷 Symlinked eleventy.config.js
from /anydigital/eleventy-bricks

The package includes a fully-configured Eleventy config file eleventy.config.js that you can symlink to your project to get:

Benefits of symlinking:

Installation as simple as:

npm install @anydigital/eleventy-bricks
ln -s ./node_modules/@anydigital/eleventy-bricks/src/eleventy.config.js

Data & Processing Tricks

Global siteData helper

🧩 Install via Plugin — or copy-paste from src/siteData.js

Adds global site data to your Eleventy project, providing commonly needed values that can be accessed in all templates:

Variable Value
{{ site.year }} The current year as a number (e.g., 2026)
{{ site.prod }} Boolean indicating if running in production mode (true for eleventy build, false for eleventy serve)

🧩 Install via Plugin — or copy-paste from src/processors/autoLinkFavicons.js

Automatically adds favicon images from Google's favicon service to links that display plain URLs or domain names. This processor processes all HTML output files and adds inline favicon images next to link text that appears to be a plain URL.

Why use this? When you have links in your content that display raw URLs or domain names (like https://example.com/page), adding favicons provides a visual indicator of the external site. This processor automatically detects these plain-text URL links and enhances them with favicon images, making them more visually appealing and easier to recognize.

How it works:

  1. Scans all HTML output files for <a> tags
  2. Checks if the link text appears to be a plain URL or domain
  3. Extracts the domain from the URL
  4. Removes the domain from the link text (keeping only the path)
  5. Adds a favicon image from Google's favicon service inline with the remaining text

Example:

Before processing:

<a href="https://github.com/anydigital/eleventy-bricks">https://github.com/anydigital/eleventy-bricks</a>

After processing:

<a href="https://github.com/anydigital/eleventy-bricks" class="whitespace-nowrap" target="_blank">
  <i><img src="https://www.google.com/s2/favicons?domain=github.com&sz=32" /></i>
  <span>/anydigital/eleventy-bricks</span>
</a>

Rules:

mdAutoRawTags preprocessor

🧩 Install via Plugin — or copy-paste from src/processors/markdown.js

Prevents Nunjucks syntax from being processed in Markdown files by automatically wrapping {{, }}, {%, and %} with {% raw %} tags.

Why use this? When writing documentation or tutorials about templating in Markdown files, you often want to show Nunjucks/Liquid syntax as literal text. This preprocessor automatically escapes these special characters so they display as-is instead of being processed by the template engine.

Example:

Before mdAutoRawTags, writing this in Markdown:

### Using {{ variable }} to output variables

Would try to process {{ variable }} as a template variable. With mdAutoRawTags, it displays exactly as written.

mdAutoNl2br converter

🧩 Install via Plugin — or copy-paste from src/processors/markdown.js

Automatically converts \n sequences to <br> tags in Markdown content. This is particularly useful for adding line breaks inside Markdown tables where standard newlines don't work.

Why use this? Markdown tables don't support multi-line content in cells. By using \n in your content, this preprocessor will convert it to <br> tags, allowing you to display line breaks within table cells and other content.

Example:

In your Markdown file:

| Column 1               | Column 2                          |
| ---------------------- | --------------------------------- |
| Line 1\nLine 2\nLine 3 | Another cell\nWith multiple lines |

Will render as:

<td>Line 1<br />Line 2<br />Line 3</td>
<td>Another cell<br />With multiple lines</td>

Note: This processes literal \n sequences (backslash followed by 'n'), not actual newline characters. Type \n in your source files where you want line breaks.


Nunjucks Tricks

Syntax highlighting in VS Code~editors

This is a modern fork of the original extension. It is specifically designed to solve the "11ty problem" where you mix Nunjucks and HTML.

Why it's better: It injects Nunjucks grammar directly into the standard HTML grammar. This means you get full Nunjucks highlighting and the editor still knows it's an HTML file, giving you better Emmet and CSS autocompletion.

No Config: It works out of the box without needing to manually map file associations in your settings.

Auto-formatting in VS Code~editors

  1. 🧩 Install /prettier/prettier-vscode (if not yet)

  2. Install a compatible Prettier plugin for your project, for example:

npm i -D prettier-plugin-jinja-template
  1. It might be tricky to find a well-maintained Nunjucks plugin, but jinja-template works just fine with .njk via .html override:
{
  "plugins": ["prettier-plugin-jinja-template"],
  "overrides": [
    {
      "files": ["*.njk", "*.html"],
      "options": {
        "parser": "jinja-template"
      }
    }
  ]
}

PRO TIP: If you already use /anydigital/bricks, it’s even easier. You can simply:

ln -s ./node_modules/@anydigital/bricks/.prettierrc.json

Sort array by attribute

Per official .njk documentation:

sort(arr, reverse, caseSens, attr)
Sort arr with JavaScript's arr.sort function. If reverse is true, result will be reversed. Sort is case-insensitive by default, but setting caseSens to true makes it case-sensitive. If attr is passed, will compare attr from each item.

But you can actually do this trick:

{% for item in array | sort(attribute='weight') %}
  ...
{% endfor %}

Include and render .md file w/o its Front Matter in 11ty

{# first, get the raw content using `html` as plain-text engine #}
{% set _eval = "{% renderFile './YOUR_FILE.md', {}, 'html' %}" %}
{% set _raw_md = _eval | renderContent('njk') %}

{# then, remove the front matter using regex, and render using `md` #}
{{ _raw_md | replace(r/^---[\s\S]*?---/, '') | renderContent('md') | safe }}

Universal Template 'Bricks' (.njk & .liquid)
from /anydigital/bricks

The package includes reusable templates in the ./src/bricks/ directory. These are useful for common web development patterns.

Install Templates

npm install @anydigital/bricks
cd ./src/_includes
ln -s ../../node_modules/@anydigital/bricks/src/bricks

Base HTML Template (__html.*)

A unified base HTML template bricks/__html.{njk|liquid} that provides the essential document structure with built-in support for modern web best practices.

Usage:

{% extends 'bricks/__html.njk' %}

{% block body %}
  <!-- YOUR page content -->
{% endblock %}

Example: /anydigital/sveleven/blob/main/src/_theme/__layout.njk

{% capture body %}
  <!-- YOUR page content -->
{% endcapture %}

{% include 'bricks/__html' %}

Example: /anydigital/sveleven/blob/main/src/_theme/__layout.liquid

Features:

Variables:

A navigation template bricks/_nav.{njk|liquid} that renders a list of navigation links with proper accessibility attributes.

Parameters:

Usage example with Eleventy Navigation plugin:

{% assign nav_pages = collections.all | eleventyNavigation %}
{% render 'bricks/_nav', nav_pages: nav_pages, current_url: page.url %}

Output:

<nav>
  <a href="/">Home</a>
  <a href="/about" aria-current="page">About</a>
  <a href="/contact">Contact</a>
</nav>

Google Tag Manager (_gtm.*)

A template bricks/_gtm.{njk|liquid} for embedding Google Tag Manager scripts in your pages.

Parameters:

  • site.gtm_id - Your Google Tag Manager container ID (e.g., GTM-XXXXXXX)
  • site.prod - Boolean flag to enable GTM only in production
  • for_body - Boolean flag (default: false). When false, renders the script tag for the <head>. When true, renders the noscript fallback for the <body>.

Note: This template is automatically included when using __html.liquid. You only need to manually render it if you're not using that base template, see examples:


🥷 Universal 11ty Filters for .njk & .liquid from /anydigital/eleventy-bricks

if

🧩 Install via Plugin — or copy-paste from src/filters/if.js

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:

  • Returns trueValue if condition is truthy, otherwise returns falseValue
  • Treats empty objects {} as falsy
  • Default falseValue is 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 %}

merge

🧩 Install via Plugin — or copy-paste from src/filters/merge.js

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) %}

attr_set

🧩 Install via Plugin — or copy-paste from src/filters/attr_set.js

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 }}

attr_concat

🧩 Install via Plugin — or copy-paste from src/filters/attr_concat.js

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:

  • 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']) %}

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', _ %}

attr_includes

🧩 Install via Plugin — or copy-paste from src/filters/attr_includes.js

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 %}

fetch

🧩 Install via Plugin — or copy-paste from src/filters/fetch.js

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:

  • Supports a URL (starting with http:// or https://) 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
  • Error handling: Throws descriptive errors if fetching fails
  • Conditional loading: Only available when @11ty/eleventy-fetch is 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 | 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 }}

section

🧩 Install via Plugin — or copy-paste from src/filters/section.js

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:

  • 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

remove_tag

🧩 Install via Plugin — or copy-paste from src/filters/remove_tag.js

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:

  • 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 }}

strip_tag

🧩 Install via Plugin — or copy-paste from src/filters/strip_tag.js

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:

  • 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>

Awesome 11ty List


This page follows a similar structure to /docs/projects

Featured in: