Nunjucks / Liquid HTML blades for 11ty/Build Awesome, Jekyll, etc.

Table of Contents

Install

Install HTML blades via npm
npm install @anydigital/blades
cd ./_includes  # your includes dir
ln -s ../node_modules/@anydigital/blades/_includes/blades

That's it! Now you can use HTML blades in your templates like this:

In Nunjucks:

{% extends 'blades/html.njk' %}
{% include 'blades/gtm.njk' %}

In Liquid:

{% include blades/html.liquid %}
{% include blades/gtm.liquid for_body=true %}
{% render blades/links, links: site.links, current_url: page.url %}

All CSS and HTML blades above are available as a Jekyll theme:

Install as Jekyll theme

In you _config.yml:

remote_theme: anydigital/blades@v0.27.0-beta # or latest
plugins:
  - jekyll-remote-theme

Living example: /anydigital/bladeswitch/blob/main/_config.yml

Or use a preconfigured template:

🥷 Bladeswitch Starter ↗  Jekyll ⁺ Pico ⁺ Blades


Base HTML

Usage in Nunjucks layout:

{% extends 'blades/html.njk' %}

{% block body %}...{% endblock %}

Living example: /anydigital/build-awesome-starter/blob/main/_includes/default.njk


How it works
<!doctype html>
<html lang="{{ site.lang | d('en') }}">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover" />
    <link rel="icon" href="{{ site.favicon | d('/favicon.ico') }}" />
    <title>
      {{- title | string | striptags ~ ' | ' if title -}}
      {{- site.title -}}
    </title>
    <meta name="description" content="{{ summary }}" />

    {%- for href in site.styles %}
      <link rel="stylesheet" href="{{ href }}" />
    {%- endfor %}
    <style>
      {{ site.inline_styles | d([]) | join('\n') }}
    </style>

    {%- for src in site.scripts %}
      <script src="{{ src }}" defer></script>
    {%- endfor %}
    <script type="module">
      {{ site.inline_scripts | d([]) | join('\n') }}
    </script>

    {{ content_for_header }}
    {% include 'blades/gtm.njk' %}
  </head>

  <body>
    {% set for_body = true %}{% include 'blades/gtm.njk' %}
    {% block body %}
    {% endblock %}
  </body>
</html>
  • title | string avoids error with | striptags when you pass a pure number.

Usage in Liquid layout:

{% capture body %}...{% endcapture %}

{% include blades/html.liquid %}

Living example: /anydigital/bladeswitch/blob/main/_includes/default.liquid


How it works
<!doctype html>
<html lang="{{ site.lang | default: 'en' }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">
    <link rel="icon" href="{{ site.favicon | default: '/favicon.ico' }}">
    <title>
      {%- if title %}{{ title | strip_html | append: ' | ' }}{% endif -%}
      {{- site.title -}}
    </title>
    <meta name="description" content="{{ summary }}">

    {%- for href in site.styles %}
      <link rel="stylesheet" href="{{ href | relative_url }}">
    {%- endfor %}
    <style>
      {{ site.inline_styles | join: '\n' }}
    </style>

    {%- for src in site.scripts %}
      <script src="{{ src }}" defer></script>
    {%- endfor %}
    <script type="module">
      {{ site.inline_scripts | join: '\n' }}
    </script>

    {{ content_for_header }}
    {% include blades/gtm.liquid %}
  </head>

  <body>
    {% include blades/gtm.liquid for_body=true %}

    {{ body }}
  </body>
</html>

Usage with Eleventy Navigation plugin:

{% assign links = collections.all | eleventyNavigation %}
{% render blades/links, links: links, current_url: page.url %}

Usage inside Pico's Navar:

TBD

How it works
<ul>
  {%- for link in links %}
    <li>
      
      <a href="{{ link.url }}" {% if link.url == current_url %}aria-current="page"{% endif %}>
        {{- link.title -}}
      </a>
    </li>
  {%- endfor %}
</ul>


Sitemap 🆕

/anydigital/blades/blob/main/_includes/blades/sitemap.xml.njk


Google Tag Manager (GTM)

Usage in Nunjucks:

    ...
    {% include 'blades/gtm.njk' %}
  </head>
  <body>
    {% set for_body = true %}{% include 'blades/gtm.njk' %}
    ...

Living example: /anydigital/blades/blob/main/_includes/blades/html.njk

Parameters:


How it works
{% if site.prod and site.gtm_id %}
  
    {% if not for_body %}

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{{ site.gtm_id }}');</script>
<!-- End Google Tag Manager -->

    {% else %}

<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ site.gtm_id }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->

    {% endif %}
  
{% endif %}

Usage in Liquid:

    ...
    {% include blades/gtm.liquid %}
  </head>
  <body>
    {% include blades/gtm.liquid for_body=true %}
    ...

Living example: /anydigital/blades/blob/main/_includes/blades/html.liquid


How it works
{% if site.prod and site.gtm_id %}
  {% capture _ %}
    {% unless for_body %}

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{{ site.gtm_id }}');</script>
<!-- End Google Tag Manager -->

    {% else %}

<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ site.gtm_id }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->

    {% endunless %}
  {% endcapture %}
  {{ _ | strip }}
{% endif %}
<!--prettier-ignore-->

PS: 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/blades, it’s even easier. You can simply:

ln -s ./node_modules/@anydigital/blades/.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 }}

PS: Liquid tricks

Syntax highlighting & auto-formatting in VS Code~editors
  1. 🧩 Install /prettier/prettier-vscode (if not yet)
  2. 🧩 Install /docs/storefronts/themes/tools/shopify-liquid-vscode
    (2-in-1 extension for highlighting & formatting)

This is a huge advantage for .liquid over .njk so far.

Create array
{% capture _new_array %}
1
2
3
{% endcapture %}
{% assign _new_array = _new_array | strip | split: '\n' %}
Beware: False Positives in .liquid

❌ Wrong:

{% if my_string %} This will always show if the string exists, even if empty. {% endif %}

✅ Right (Checking for content):

{% if my_string != blank %} This only shows if there is actual text. {% endif %}

✅ Right (Checking arrays/strings by size):

{% if my_array.size > 0 %} This ensures the list isn't empty. {% endif %}