@raredigits/11ty-rare-related-posts
v0.1.0
Published
Eleventy plugin: related-posts filter with curated picks and shared-label auto-fill. Works in Liquid and Nunjucks, configurable to any collection/field.
Maintainers
Readme
@raredigits/11ty-rare-related-posts
A small Eleventy plugin that adds a relatedPosts
filter. For each page it returns:
- Curated picks — whatever the page lists in
related: [...], in your order, or - Auto-fill — when nothing is curated, the posts that share the most labels/tags with the current page (ranked by overlap, then recency).
Works in Liquid and Nunjucks, reads its collection itself, and every name (collection, fields, the filter itself) is configurable — so it drops into any site without clashing with what's already there.
📦 Source & issues: github.com/raredigits/11ty
Install
npm install @raredigits/11ty-rare-related-postsSetup
// eleventy.config.js
import relatedPosts from "@raredigits/11ty-rare-related-posts";
export default function (eleventyConfig) {
eleventyConfig.addPlugin(relatedPosts, {
collection: "posts", // collection to auto-fill from (null = all pages)
labelsField: "tags", // front-matter field holding topics
relatedField: "related", // front-matter field holding curated picks
excludeField: "relation",// front-matter field to opt a post out
filterName: "relatedPosts",
limit: 3, // how many related posts to show
fields: { // what each result card exposes
title: true,
image: true,
tldr: true,
tags: false,
date: true,
},
dateFormat: "YYYY", // token string, Intl options, or a function
locale: "en",
});
}All options are optional. With zero config the plugin auto-fills from every
page (collections.all) using the tags field — so it works on a default
Eleventy setup with no custom collections.
Options
| Option | Default | Purpose |
| -------------- | ---------------- | -------------------------------------------------------------- |
| collection | null | Collection name to auto-fill from. null → all pages. |
| labelsField | "tags" | Front-matter field used to score topical overlap. |
| relatedField | "related" | Front-matter field holding a curated list. |
| excludeField | "relation" | Front-matter field that opts a post out (see below). |
| filterName | "relatedPosts" | Name of the filter in templates (rename to avoid clashes). |
| limit | 3 | Max number of auto-filled posts. |
| fields | (see below) | Which fields each result card exposes, and where they map. |
| dateFormat | "YYYY" | How dateFormatted is rendered. |
| locale | "en" | Locale for month names / Intl formatting. |
| warn | true | Warn in the build log when a curated slug can't be resolved. |
If collection names a collection that's empty or missing, the plugin falls
back to all pages instead of erroring.
Excluding a post — relation: off
Add the exclude field to any page's front matter to keep it out of auto-fill everywhere (e.g. a changelog, a legal page, a stub):
---
title: Changelog
relation: off # also accepts false / no / none
---Excluded posts are skipped only by auto-fill — you can still link them
explicitly via related when you really mean to. Rename the field with the
excludeField option.
Choosing what each card shows — fields
Each result is a normalized card so your template doesn't depend on front-matter field names. Per field, the value can be:
false— omit it,true— include it, reading the conventional source field,"someField"— include it, reading fromsomeFieldinstead, or["a", "b"]— try each field in order, first non-empty value wins.
fields: {
title: true, // → card.title (from `title`)
image: ["cover", "img"], // → card.image (try `cover`, fall back to `img`)
tldr: true, // → card.tldr (from `tldr`)
tags: true, // → card.tags[] (defaults to your labelsField)
date: true, // → card.dateFormatted
}The array form is useful when front matter is inconsistent — e.g. some posts
store the image under cover and others under img.
A card always carries url, date (raw), and data (the raw front matter) as
an escape hatch, plus whichever of title / image / tldr / tags /
dateFormatted you enabled.
Date format — dateFormat
dateFormat: "YYYY" // 2024
dateFormat: "DD MMM YYYY" // 09 Mar 2024 (tokens: YYYY YY MMMM MMM MM M DD D)
dateFormat: { dateStyle: "medium" } // Intl.DateTimeFormat options, uses `locale`
dateFormat: (d) => `${d.getFullYear()}` // a function for full controlUse in templates
Styling: the Rare Styles library already ships a class set for this module (the
related/feature-rowmarkup below), so if you use it you don't need to write any CSS — just drop in the markup.
Auto mode — the plugin finds the collection for you:
{% assign related = page.url | relatedPosts %}
{% assign related = page.url | relatedPosts: 5 %} {# override the limit #}
{% if related.size > 0 %}
{% for post in related %}
{% if post.image %}<img src="{{ post.image }}" alt="{{ post.title }}">{% endif %}
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
{% if post.tldr %}<p>{{ post.tldr }}</p>{% endif %}
<span>{{ post.dateFormatted }}</span>
{% endfor %}
{% endif %}Use the card fields (post.title, post.image, post.tldr, post.tags,
post.dateFormatted) so the plugin's fields and dateFormat settings drive
the output. Reach into post.data for anything you didn't expose as a field.
Explicit mode — pass a collection in yourself (handy if you don't want the plugin to pick the collection, or have several):
{% assign related = collections.news | relatedPosts: page.url %}
{% assign related = collections.news | relatedPosts: page.url, 5 %}The same works in Nunjucks ({% set related = page.url | relatedPosts %}).
Curating related posts by hand
Add a related list to a page's front matter. Curation always wins over
auto-fill, keeps your order, and ignores limit:
---
title: My post
related:
- my-other-post # slug, url, or last path segment — all resolve
- /blog/deep-dive/
---Curated slugs resolve against every page on the site, not just the configured collection — so you can link anywhere.
Hiding a post from recommendations
To stop a specific post from ever being auto-suggested anywhere on the site,
add relation: off to that post's front matter:
---
title: "Vacation Notice: The August Disappearing Act"
relation: off # also accepts false / no / none
---That's it — rebuild and it disappears from every other post's related list.
It's skipped only by auto-fill, so you can still link to it deliberately with
related. The field name is configurable via the excludeField option.
No collection at all?
If a site has nothing to resolve against, list related items inline as objects:
related:
- { url: "https://example.com/x", title: "An external read" }
- { url: "/standalone/", title: "A standalone page", img: "/img/x.png" }These render with the same post.url / post.data.title shape as real
collection items.
Tuning the algorithm
The recommendation logic lives in src/related.js as a pure
function with no Eleventy dependency, and is exported separately:
import { pickRelated } from "@raredigits/11ty-rare-related-posts/logic";It's covered by unit tests (npm test) that run without a build, so you can
iterate on scoring quickly.
Local development
Eleventy loads plugins once at startup and does not watch node_modules.
So when you edit this package while developing against a site (via npm link
or a file: dependency), the running dev server keeps the old version in
memory — your changes won't show up, and you may see stale or empty output.
After changing anything under src/, restart the dev server:
# Ctrl+C to stop, then:
npm startEdits to the site's own templates and content hot-reload as usual; only
changes to this package need a restart. Running npm test here needs no
restart — the tests import the source directly.
License
MIT
