eleventy: Slug filter doesn't create url safe slugs.

I’ve been using the slug filter to convert regular strings to strings suitable for use as urls. However I’ve realised it’s not set up to remove apostrophes (and possibly other characters?).

So it's a test becomes it's-a-test, and when rendered with eleventy the url is /tags/it's-a-test/ - the ampersand breaks the url. It also looks ugly!

I see that the package supports supplying a list of characters to remove.

If the intention is that this filter is used to create ‘safe’ urls, could Eleventy remove these by default?


As an alternative I’ve added my own filter using the slugify() option of the string library. I might stick with this anyway as it seems to do a good job of creating ‘pretty’ slugs.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 3
  • Comments: 53 (36 by maintainers)

Commits related to this issue

Most upvoted comments

yup @zachleat , it does replace the slug, so I just do remove some chars to make it little bit prettier tho

const slugify = require("slugify");
eleventyConfig.addFilter("slug", (input) => {
  const options = {
    replacement: "-",
    remove: /[&,+()$~%.'":*?<>{}]/g,
    lower: true
  };
  return slugify(input, options);
});

I think this change is required (I would recommend @sindresorhus/slugify, stable and well maintained) to make people’s life much easier in the future, with probably many more users than today.

One option to deal with it would be to release a 0.12.0 version with some deprecation/breaking change warnings (not only the slug, I think there are other changes) in the console, before a 1.0 version that really changes these.

This 0.12.0 version could already includes the new slugify function, just to make sure the warnings are shown only if the result is different from the current one.

For anybody else curious, here’s the difference between the default slug filter (which uses slugify; currently slugify@1.5.3 in Eleventy 0.12) vs @sindresorhus/slugify@1:

default slug filter (via slugify@1.5.3) custom slugify filter (via @sindresorhus/slugify@1.1.2)
{{ Ä-ä | slug }} = “a-a” {{ Ä-ä | slugify }} = “ae-ae”
{{ Ö-ö | slug }} = “o-o” {{ Ö-ö | slugify }} = “oe-oe”
{{ Ü-ü | slug }} = “u-u” {{ Ü-ü | slugify }} = “ue-ue”
{{ ẞ-ß | slug }} = “ss-ss” {{ ẞ-ß | slugify }} = “ss-ss”
const slugify = require("@sindresorhus/slugify");

module.exports = (eleventyConfig) => {
  eleventyConfig.addFilter("slugify", slugify);
  return {...};
};

Or, if you want to replace the built-in Eleventy “slug” filter with a different implementation, just name your custom slugify filter “slug”:

const slugify = require("@sindresorhus/slugify");
eleventyConfig.addFilter("slug", slugify);

NOTE: Only @sindresorhus/slugify v1 is supported (<kbd>npm i @sindresorhus/slugify@1</kbd>). v2 uses ESM modules and you’ll get errors when trying to use it:

Error was thrown:    Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /private/tmp/11ty-liquid-or/node_modules/@sindresorhus/slugify/index.js    require() of ES modules is not supported.


UPDATE: For my own future reference, I managed to get async filters working in Nunjucks using addNunjucksAsyncFilter() (async filter support wasn’t added into LiquidJS until v9.1.3, per https://github.com/harttle/liquidjs/issues/232) and the v2/ESM version of @sindresorhus/slugify:

  eleventyConfig.addNunjucksAsyncFilter("slugify", async function(str, callback) {
    // Where @sindresorhus/slugify is v2.1.0, which is an ESM module.
    const slugify = await import("@sindresorhus/slugify");
    callback(null, slugify.default(str.toString()));
  });

If you’re using Eleventy v1/Canary (which currently has liquidjs@9.25.0), this code works for an async LiquidJS filter or an .11ty.js template, but will return a Promise object in Nunjucks:

  eleventyConfig.addFilter("slugify", async (str) => {
    const slugify = await import("@sindresorhus/slugify");
    return slugify.default(str.toString());
  });
// test.11ty.js
module.exports = {
  async render(data) {
    return `slug=${ await this.slugify("Peter deHaan 'tis an idiot (11ty.js)") }`;
  }
};
STRING LANGUAGE RESULT
slug={{ "Peter deHaan 'tis an idiot (LiquidJS)" | slugify }} LiquidJS slug=peter-de-haan-tis-an-idiot-liquid-js
slug=${ await this.slugify("Peter deHaan 'tis an idiot (11ty.js)") } .11ty.js slug=peter-de-haan-tis-an-idiot-11ty-js
slug={{ "Peter deHaan 'tis an idiot (Nunjucks)" | slugify }} Nunjucks slug=[object Promise]

@bridgestew are you using eleventy-base-blog?

If so your opts defined here (docs: https://github.com/valeriangalliat/markdown-it-anchor): https://github.com/11ty/eleventy-base-blog/blob/master/.eleventy.js#L43

  let opts = {
    permalink: true,
    permalinkClass: "direct-link",
    permalinkSymbol: "#"
};

would be something like:

const slugify = require("slugify");
let opts = {
    permalink: true,
    permalinkClass: "direct-link",
    permalinkSymbol: "#",

    // this is the same function shared above
    slugify: function(input) {
      const options = {
        replacement: "-",
        remove: /[&,+()$~%.'":*?<>{}]/g,
        lower: true
      };
      return slugify(input, options);
    }
};

Does that make sense?

I’m going to move this into the new feature queue and it is logged for the next major version milestone.

This PR is merged and slugify will ship with 1.0, thanks y’all!

@Miosame you’re right, it might be enough.

I just wish the defaults would be more safe, but Eleventy can make it so.

Breaking changes should only be added on major version bumps.

11ty has 17k npm installs per week. You can only imagine the mad mob that would show up here if the slug function changed and broke their projects due to a npm install.

Perhaps it should be “fixed” on v1.0.0

I appreciate the pushback!

To be clear here the slug filter using the slugify package is only a user-land filter and is not used in any way internally in core—I feel like some of the alternatives being proposed here have assumed that it was used internally.

Exposing a new name and a soft-default-change (docs update) to point to this new name has a bunch of benefits:

  1. Existing URLs using slug do not break. This is so very important. Breaking compatibility between versions is one thing. Breaking URL output consistency is a next level of bad problem we want to avoid AT ALMOST ANY COST (sorry for all caps but I really don’t want to break any URLs). Changing the default slug to point to a new package would be a not just be a breaking change but would also (in my opinion, at bare minimum) require some extra code to compare old with new URLs to warn about URL changes.
  2. Allows incremental swapping to the new filter. Projects can use the old and the new method at the same time.
  3. No configuration switches needed. A configuration option would likely be global (citation needed?).

There is a special case of the above I ran into the other day on win10 dev environment. Because the the current slug filter does not remove dots from titles, if your title happens to have a one at the end, and you use titles for your URLs you can end up with something like: this-is-my-title./index.html

Having a dot at the end of a directory name or file turns out to be problematic in windows, and requires a rather roundabout way of deleting it: https://stackoverflow.com/questions/4075753/how-to-delete-a-folder-that-name-ended-with-a-dot

Figured some may find this useful if they have cleanup scripts to blow away the _site directory, and those scripts are suddenly failing on windows for unknown reasons, this could be one of them.

Thanks for sharing @kazzkiq! I wonder if you could just use slug? Does it overwrite the built in filter? I feel like that should work if it doesn’t

Purely for comedic purposes, but I think this might possibly work if we have a future [Eleventy v1] slug filter (which uses slugify), and a slugify filter (which uses @sindresorhus/slugify), but you want to alias one to the other (because you like the “slug” filter name, but newer “slugify” implementation…

eleventyConfig.addFilter("slug", eleventyConfig.getFilter("slugify"));

Yeah, it has great potential to break your existing URLs because it’s a completely different implementation, but #YOLO. 💥

https://github.com/11ty/eleventy/pull/1873 is open for feedback! I stuck with the 1.x @sindresorhus/slugify package for now and decided to stick with the slugify name for brevity and to match the upstream package name. We can revisit 2.x when more ESM stuff lands in Eleventy.

I don’t know if it’s possible to use the ESM version of @sindresorhus/slugify (yet?), I didn’t manage to make it work in my projects.

This is as close as I’ve ever gotten to using the ESM version in commonjs/Node. It seems to work for Liquid (9.1.3+ via eleventy@canary build) and Nunjucks (via .addNunjucksAsyncFilter()).

module.exports = function (eleventyConfig) {
  const slugifyFn = async (str) => {
    // Import the ESM module...
    const fn = await import("@sindresorhus/slugify");
    return fn.default(str);
  };

  eleventyConfig.addFilter("slugify", (str) => slugifyFn(str));
  // Note that the async Nunjucks filter uses the same name as the global `slugify` filter.
  eleventyConfig.addNunjucksAsyncFilter("slugify", (str, callback) => {
    slugifyFn(str)
      .then((value) => callback(null, value))
      .catch((err) => callback(err));
  });

Definitely easier to stick with @sindresorhus/slugify v1 (pre-ESM). I tried, but could never figure out how to get Node’s util.callbackify() function to work w/ @sindresorhus/slugify, which might have been able to shave a couple lines off of that code.

Is this a reason not to use @sindresorhus/slugify as the default for eleventy 1.0 as some suggested? Or will eleventy support ESM?

Oh, I wasn’t suggesting 11ty change the default slugify library (although personally I think @sindresorhus/slugify is the better option). I think it’d have too many issues w/ backwards compatibility and broken/changed links. But it’s an easy fix if you want to opt-in for individual sites.

Excellent, thank you @okitavera.

@kazzkiq I think you should be able to use custom filters in permalink generation - have you tried?

Oh, hmm. Well that’s not ideal.

I did try permalink: '/{{ "Hi I''m Zach" | slug }}/' (note the escaped single quote using '') which resulted in the URL http://localhost:8080/hi-i'm-zach/ which worked in both Chrome and Firefox. Where is the conversion happening in your URL? It doesn’t seem to be coming from the slugify filter.