Skip to content

11ty Slugs and Anchors

Updated on: Oct 21, 2021 by Stephanie Eckles
🍿 5 min. read

Extend the default `slug` filter and enable accessible heading anchors.

slug Filter Extension

The default slug filter uses slugify under the hood, but sometimes the default behavior isn't quite enough if you are using special characters, including emoji.

We can override the filter to enable strict mode, enforce lowercasing, and optionally add any other characters you encounter being problematic. In this case, I'm enforcing removing " because I have experienced issues without explicitly defining it.

Usage: {{ title | slug }}

// Import prior to `module.exports` within `.eleventy.js`
const slugify = require("slugify");

eleventyConfig.addFilter("slug", (str) => {
if (!str) {
return;
}

return slugify(str, {
lower: true,
strict: true,
remove: /["]/g,
});
});

Eleventy uses markdown-it for Markdown parsing, and shows a few options for configuring it in the 11ty docs.

There are several plugins you can add to extend markdown-it, but in this example we are adding anchor links to our content headings. We're also extending the idea from our slug update to update which characters are removed and replaced to create anchors.

Kudos to Nicolas Hoizey for the concise method of modifying the markdown-it-anchor behavior to add a wrapping div to assist in styling placement of the anchor symbol. And for raising the issue to improve the base behavior of this plugin.

The markdown-it-anchor plugin added three permalink output options to assist with accessibility. This snippet uses the linkInsideHeader option with a custom symbol and permalink render function, which produces output like the following that also adds a wrapping .heading-wrapper. I've also selected the aria-labelledby option to prevent the extra text showing up in RSS feeds and search engine results.

// npm install --save-dev markdown-it-anchor slugify
// Import prior to `module.exports` within `.eleventy.js`
const markdownIt = require("markdown-it");
const markdownItAnchor = require("markdown-it-anchor");
// If not already added from previous tip
const slugify = require("slugify");

const linkAfterHeader = markdownItAnchor.permalink.linkAfterHeader({
class: "anchor",
symbol: "<span hidden>#</span>",
style: "aria-labelledby",
});
const markdownItAnchorOptions = {
level: [1, 2, 3],
slugify: (str) =>
slugify(str, {
lower: true,
strict: true,
remove: /["]/g,
}),
tabIndex: false,
permalink(slug, opts, state, idx) {
state.tokens.splice(
idx,
0,
Object.assign(new state.Token("div_open", "div", 1), {
// Add class "header-wrapper [h1 or h2 or h3]"
attrs: [["class", `heading-wrapper ${state.tokens[idx].tag}`]],
block: true,
})
);

state.tokens.splice(
idx + 4,
0,
Object.assign(new state.Token("div_close", "div", -1), {
block: true,
})
);

linkAfterHeader(slug, opts, state, idx + 1);
},
};

/* Markdown Overrides */
let markdownLibrary = markdownIt({
html: true,
}).use(markdownItAnchor, markdownItAnchorOptions);

// This is the part that tells 11ty to swap to our custom config
eleventyConfig.setLibrary("md", markdownLibrary);

Example output:

<div class="heading-wrapper h2">
<h2 id="enable-anchor-links-on-content-headings">Enable anchor links on content headings</h2>
<a
class="tdbc-anchor"
href="#enable-anchor-links-on-content-headings"
aria-labelledby="enable-anchor-links-on-content-headings"
>

<span hidden>#</span>
</a>
</div>

Why use hidden? Because RSS, search engine results, and "reader mode" will (usually) respect it and prevent those environments from showing the anchor symbol.

You'll want the following CSS or similar to handle showing the anchor link symbol on your website:

/* Make the '#' anchor visible on your site */
.anchor hidden {
display: block;
}
View more config samples