11ty/eleventy

Support generating IDs for headings, for section direct links

Closed this issue · 12 comments

Is your feature request related to a problem? Please describe.
I'm missing an It just works™️ way of generating IDs for headings, for section direct links.

Describe the solution you'd like
Option to allow 11ty to generate ID's for headings, for any source type, in a similar way that markdown-it-anchor does it. Supporting this out of the box, would help in generating direct links to e.g. code snippets without custom shortcodes. It'd also be one less thing to worry about when moving between templating languages.

Describe alternatives you've considered

  • Manually hardcoding the heading IDs. For headings to code snippets, this is probably what I'll do.
  • Shortcodes. It'd work, but I think it'd be a bit messy compared to having something like markdown-it-anchor regardless if source is .html or .md.
btrem commented

There are markdown plugins you can add to 11ty to enable adding attributes to elements with curly braces:
## page heading {#foo}
produces
<h2 id="foo">page heading</h2>
Is that what you had in mind?

btrem commented

I just did some research. Add your own plugins:
https://www.11ty.dev/docs/languages/markdown/#add-your-own-plugins

Markdown it anchor, which looks like what you need:
https://www.npmjs.com/package/markdown-it-anchor

@btrem The issue is about supporting more than Markdown. The feature request is for a source agnostic solution.

https://css-tricks.com/on-adding-ids-to-headers/ Has a potential solution, but would require jquery to do dynamically on the client
Wondering if you could add a custom transform that adds slugified ids for h1-h6 using something like cheerio.


A cheaper solution I've used elsewhere is to create your own shortcode that does something like this; which should be reusable between Nunjucks+Liquid+Markdown, etc.

  // In your .eleventy.js config file...
  eleventyConfig.addShortcode("h", function (level = 1, label = "") {
    const slug = eleventyConfig.javascriptFunctions.slug;
    const tag = `h${level}`;
    return `<${ tag } id="${ slug(label) }">${ label }</${ tag }>`;
  });

And my src/about.md might look like this:

<article>
  {% h 1, "THIS IS MARKDOWN" %}
  {% h 2, "This is Sparta!" %}

  A wild paragraph appears!
</article>

And my output HTML then looks like:

<article>
  <h1 id="this-is-markdown">THIS IS MARKDOWN</h1>
  <h2 id="this-is-sparta!">This is Sparta!</h2>
<p>A wild paragraph appears!</p>
</article>

NOTE: Note the ! at the end of the h2 tag's id= attribute; <h2 id="this-is-sparta!"> I think that might be a side effect of the current built-in slug filter's default configuration. If you want to remove exclamation points or other unwanted characters in your id, see #278 for ideas/workarounds.

If you don't like the single shortcode with variable levels approach, you could always just make 4-6 separate shortcodes for "h1", "h2", "h3", "h4", etc.
Then, instead of {% h 3, "A space between h+3" %}, it'd be {% h3 "no space between h+3" %}.

Anyways, I know this isn't the solution you were asking for, but for future travellers seeking workarounds, this might get you started.

btrem commented

The issue is about supporting more than Markdown. The feature request is for a source agnostic solution.

@jouni-kantola Sorry, I misread the first message in this issue (specifically, "something like markdown-it-anchor, which I thought meant that's what you wanted). Apologies.

@pdehaa Thank you! I appreciate it a lot. Being able to remove the extra dependency to markdown-it-anchor is 👌

@pdehaan Thanks a lot for your help! With inspiration from your shortcode, I adapted it to how markdown-it-anchor generates section headers.

Here's the shortcode:

module.exports = (eleventyConfig, symbol = "#") => {
  eleventyConfig.addShortcode(
    "h",
    function (level = 1, text = "", classList = "") {
      const tag = `h${level}`;
      const slug = eleventyConfig.javascriptFunctions.slug;
      const id = `${slug(text)}`.replace(/[&,+()$~%.'":*?!<>{}]/g, "");

      return `
<${tag} id="${id}"${classList && ` class="${classList}"`}>
  <span class="heading">${text}</span>
  <a class="direct-link" href="#${id}">${symbol}</a>
</${tag}>
`;
    }
  );
};

I use it like this to generate section headings for code snippets:

{% h 4 "PostCSS shortcode" "code-snippet__description" %}

I use CSS to position the symbol before/after the heading text.

/* Place direct link symbol before heading text */
h2 {
    display: flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
}

@jouni-kantola Looks great!

One interesting thing I found in the docs last night was https://www.11ty.dev/docs/filters/#access-existing-filters which might be a better way of getting a universal function than relying on eleventyConfig.javascriptFunctions.slug(text).

I think the usage is something like eleventyConfig.getFilter("slug")(text).

@zachleat How do you feel about a feature request like this? Should Eleventy come with an option to generate headings with direct links (e.g. how headings work in GitHub readmes), or should we use shortcode and close this issue?

@zachleat How do you feel about a feature request like this? Should Eleventy come with an option to generate headings with direct links (e.g. how headings work in GitHub readmes), or should we use shortcode and close this issue?

As someone trying to migrate a GitHub Pages Jekyll site to 11ty w/o having to edit hundreds of legacy .md files, this would be AMAZING.

I was in need of this as well so I put together this 11ty plugin

I’d be a fan of this living in plugin land—the one that @oyejorge posted looks great!

I would encourage anyone using jsdom based approaches to try out linkedom though, I’ve personally seen huge speed boosts: https://www.npmjs.com/package/linkedom

That said, I think we are okay to close for now? Happy to add any new plugins to the site