Home

Eleventy excerpts

2024-03-15

Background

Wherever I list posts on this site, e.g. on the Home page and Archive page, I want to display a reasonably long excerpt from the linked post, to give visitors an idea of its content. There seems to be an almost comical variation in what people want from excerpts - how long they should be, whether they should be marked manually or picked out automatically, or entered separately from the content altogether, whether images should be included or not etc. etc. Fun!

Of course I also have my own idea of how everything should work:

I think that's it. Pretty modest requirements if you ask me.

The Eleventy solution

This site is built using Eleventy, and thanks to its use of the gray-matter npm package, Eleventy has a way of extracting excerpts from posts automatically. It requires manually setting the excerpt cutoff-point in the text, but you can freely choose what separator to use:

module.exports = function (eleventyConfig) {
    eleventyConfig.setFrontMatterParsingOptions({
        excerpt: true,
        excerpt_separator: "<!-- cut -->",
    });

    // Further configuration
};

With this snippet in place in your .eleventy.js, the item.data.page object will include a property called excerpt when iterating over collections:

{% for post in collections.post %}
<article>
    ...
    <p>{{ post.data.page.excerpt }}</p>
    ...
</article>
{% endfor %}

This solution works well, but I don't want to mark my excerpts manually. I feel pretty strongly that the content of the site should be kept as free as possible from technical implementation details. This feels like a good principle, and will also make the content as portable as possible.

Another limitation of this solution is that the excerpt generated will be extracted from the raw content, rather than the rendered page. I use Markdown for my posts, so the excerpts I get using the built-in functionality will be snippets of the unprocessed Markdown source. Stripping Markdown formatting from text is a lot trickier than e.g. stripping HTML, as there is no common formatting of control characters etc. I'm sure there are clever regular expressions out there that solves this, but I feel more confident stripping out HTML. Another common approach is to manually run the excerpt through the markdown-it processor used by Eleventy, but I always thought that felt a bit clunky and unnecessary. The page will be processed by markdown-it as part of the site rendering anyway, why do it again just to generate an excerpt?

My solution

I settled on building a custom solution in the form of an Eleventy filter, that extracts the excerpt from the rendered page rather than from the source. To keep my .eleventy.js as short as possible, I put the code in a separate class:

class ExcerptGenerator {
    getExcerpt(content, length) {
        let excerptParagraphs = [];
        let currentLength = 0;
        const paragraphs = content.match(/<p>.*?<\/p>/gs) || [];

        for (let paragraph of paragraphs) {
            // Strip HTML from the paragraph
            const text = paragraph.replace(/(<([^>]+)>)/gi, "");

            if (currentlength > 0 && currentLength + text.length > length) {
                break;
            }

            excerptParagraphs.push(text);
            currentLength += text.length;
        }

        return excerptParagraphs.join(" ");
    }
}

The code finds all paragraph tags in the content, and then starts picking them out one by one, until adding another one would break the specified length limit. As part of this process, any HTML within the added paragraphs is stripped. A bit of a brutal approach perhaps, but it gets the job done. It feels like there's an elegant map reduce-like solution to this problem, but I'm not so sure that elegant solution would be any easier to read or maintain. I could also have used something like Cheerio to first parse the HTML and then pick out the paragraphs that way, but that would most likely have been slower and probably not much of an improvement readability-wise either. I'll stick to the brutalist approach for now.

Usage

The filter is then hooked up in .eleventy.js:

const ExcerptGenerator = require("./_scripts/excerptgenerator");

module.exports = function (eleventyConfig) {
    eleventyConfig.addFilter("excerpt", function (content) {
        return new ExcerptGenerator().getExcerpt(content, 500);
    });

    // Further configuration ...

    return eleventyConfig;
};

The string passed into the addFilter function is the name that the filter will be accessed through, in this case excerpt. This filter name is now ready to be used from any template:

{% for post in collections.post %}
<article>
    ...
    <p>{{ post.templateContent | excerpt }}</p>
    ...
</article>
{% endfor %}

This code takes the template content, i.e. the rendered content, from each post and sends it through the excerpt filter, resulting in a reasonably sized excerpt void of any styling and formatting.

Drawbacks

The above solution has worked well for all posts I've written so far, but in theory it could generate somewhat incoherent excerpts in certain cases. Consider a post with a very short first paragraph, followed by an image or a code block, and then a second paragraph discussing that middle content. The excerpt for this post would just consist of a single string combining the first and the second paragraph, completely omitting the middle content, which could make the text a bit difficult to understand. I'll deal with that if and when it happens. Just like with the automatic image scaling I wrote about recently, my goal here isn't perfection, but to have a decent end-result without any manual work. The lazier I can be when publishing new content, the more likely I am to actually do it.

Conclusion

I see a lot of people implementing custom excerpt functionality in Eleventy, which is a testament to its flexibility. On the other hand, it also says something about the current excerpt support. A couple of the customizations I've implemented as part of this site in the past have been workarounds for little rough edges like this in Eleventy, and they have all since been made obsolete by improvements either in Eleventy itself or in official plugins. I have a feeling excerpts could be up for a revamp as well at some point in the future. Until then, I'm happy with my own little flavor of excerpts.

If you want to read more about the ways I'm using Eleventy on this site, I've collected all my posts on a separate Eleventy page. Happy blogging!