How I built my own excerpt for Markdown content in Eleventy

I was not really happy with Eleventy's native excerpt solution requiring just a separator and having the excerpt content preserved in the content, without any way to style it differently. So I tried different alternatives, and settled on a solution with some Markdown-it plugins and a bunch of regexes.

To be able to style the content lead whatever it contains, I'm using the great and simple markdown-it-container plugin for Markdown-it, with a lead container.

For exemple, I can write this in the begining of my Markdown file, after the YAML Front Matter:

::: lead
This paragraph is in the lead.

This other paragraph is also in the lead.

This is no more part of the content lead…

With this really simple syntax, I can put whatever I want in the lead and style it in the content page.

And I can also extract it for the excerpt!

I could have used a Nunjucks filter, as I previously did, but it means the excerpt for the same content would have been computed multiple times (unless I did some memoization) for different content listings in the homepage, a category page, archives pages, Atom and JSON feeds, Algolia index, etc.

Fortunately, Eleventy provides the eleventyConfig.setFrontMatterParsingOptions() function which allows passing options to gray-matter, the npm package it relies on to parse front matter.

This is the function that allows setting a custom separator for the default excerpt feature with the excerpt_separator option, as shown in Eleventy documentation, but in addition to the simple true boolean shown here, the excerpt option can be a function, which gets the Markdown content and options as parameters.

Here's the function I built to generate my own excerpt:

const markdownItPlainText = require('markdown-it-plain-text');
const excerptMd = new markdownIt().use(markdownItPlainText);

function grayMatterExcerpt(file, options) {
  const regex = /^.*::: lead(((?!(:::)).|\n)+):::.*$/gm;
  let excerpt = '';
  let leadFound = false;

  if ((leadMatches = regex.exec(file.content)) !== null) {
    lead = leadMatches[1];
    leadFound = true;
  } else {
  excerpt = excerptMd.plainText
    .replace(/{%(((?!(%})).|\n)+)%}/gm, '') // remove short codes
    .replace(/{{(((?!(}})).|\n)+)}}/gm, '') // remove nunjucks variables
    .replace(/{#(((?!(#})).|\n)+)#}/gm, '') // remove nunjucks comments
    .replace(/<style>(((?!(<\/style>)).|\n)+)<\/style>/gm, '') // remove inline CSS
      /<script type="application\/ld\+json">(((?!(<\/script>)).|\n)+)<\/script>/gm,
    ) // remove JSON+LD
    .replace(/(<\/h[1-6]>)/gm, '. $1') // add a dot at the end of headings
    .replace(/<\/?([a-z][a-z0-9]*)\b[^>]*>|<!--[\s\S]*?-->/gm, '') // remove HTML tags
    .replace(/(\[\^[^\]]+\])/gm, '') // remove Markdown footnotes
    .replace(/\[([^\]]+)\]\(\)/gm, '$1') // remove Markdown links without URL (from {% link_to %} for example)
    .replace(/ +(\.|,)/gm, '$1'); // remove space before punctuation

  if (!leadFound && excerpt.length > 150) {
    // Keep only 145 characters and an ellipsis if there was no declared lead
    excerpt = excerpt.replace(/^(.{145}[^\s]*).*/gm, '$1') + '…';
  file.excerpt = excerpt;

  excerpt: grayMatterExcerpt,

It first looks for a ::: lead container to use directly, or takes the full content.

It then parses the result with Markdown-it and the markdown-it-plain-text plugin, and then removes some parts that are really not useful in an excerpt: Nunjucks short codes/variables/comments, inline CSS rules, JSON/LD metadata, remaining HTML tags, footnotes, etc.

Finally, if the full content was used (no ::: lead found), it limits the result length.