feat: See Also and Linked From sections on posts

- `backlinksWith` filter scans raw source files to find posts that
  link to the current post (backlinks), with per-build caching.
- `postByUrl` filter looks up a post by absolute URL for title display.
- post.njk: "See Also" renders resolved `related` URLs with titles;
  "Linked From" lists backlinks computed at build time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-23 18:13:57 +01:00
parent 76356be972
commit 2e416ab2e1
2 changed files with 59 additions and 0 deletions
+34
View File
@@ -338,6 +338,40 @@ withBlogSidebar: true
</article>
{# See Also — related posts from frontmatter (resolved wikilinks → URLs) #}
{% if related and related.length > 0 %}
<section class="post-related mt-8 pt-6 border-t border-surface-200 dark:border-surface-700">
<h2 class="text-xs font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-widest mb-3">See Also</h2>
<ul class="space-y-1.5 list-none p-0 m-0">
{% for relUrl in related %}
{% set _relPost = collections.posts | postByUrl(relUrl) %}
<li>
<a href="{{ relUrl }}" class="text-sm text-accent-700 dark:text-accent-300 hover:underline">
{{ _relPost.data.title if _relPost else relUrl }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{# Linked From — posts that link to this one, computed from raw source files #}
{% set _backlinks = collections.posts | backlinksWith(page.url) %}
{% if _backlinks and _backlinks.length > 0 %}
<section class="post-backlinks mt-6 pt-6 border-t border-surface-200 dark:border-surface-700">
<h2 class="text-xs font-semibold text-surface-500 dark:text-surface-400 uppercase tracking-widest mb-3">Linked From</h2>
<ul class="space-y-1.5 list-none p-0 m-0">
{% for post in _backlinks %}
<li>
<a href="{{ post.url }}" class="text-sm text-accent-700 dark:text-accent-300 hover:underline">
{{ post.data.title or "Untitled" }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{# Comments section #}
{% include "components/comments.njk" %}
+25
View File
@@ -1094,6 +1094,31 @@ export default function (eleventyConfig) {
return stages[stage] || null;
});
// Backlinks — find all published posts whose raw source links to the given URL path.
// Reads each file once per build and caches it to avoid repeated disk I/O.
{
const contentCache = new Map();
eleventyConfig.addFilter("backlinksWith", function (posts, currentUrl) {
const fullUrl = `${siteUrl}${currentUrl}`;
return (posts || []).filter((post) => {
if (post.url === currentUrl) return false;
let content = contentCache.get(post.inputPath);
if (content === undefined) {
try { content = readFileSync(post.inputPath, "utf-8"); }
catch { content = ""; }
contentCache.set(post.inputPath, content);
}
return content.includes(fullUrl);
});
});
}
// Look up a post by its absolute blog URL — used to show titles in related-links lists.
eleventyConfig.addFilter("postByUrl", function (posts, url) {
const path = url.replace(siteUrl, "");
return (posts || []).find((p) => p.url === path || p.url === `${path}/`);
});
// Strip garden/* tags from a category list so they don't render as
// plain category pills alongside the garden badge.
eleventyConfig.addFilter("withoutGardenTags", (categories) => {