diff --git a/_includes/layouts/post.njk b/_includes/layouts/post.njk
index 9f86f3c..b24b113 100644
--- a/_includes/layouts/post.njk
+++ b/_includes/layouts/post.njk
@@ -338,6 +338,40 @@ withBlogSidebar: true
+{# See Also — related posts from frontmatter (resolved wikilinks → URLs) #}
+{% if related and related.length > 0 %}
+
+{% 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 %}
+
+{% endif %}
+
{# Comments section #}
{% include "components/comments.njk" %}
diff --git a/eleventy.config.js b/eleventy.config.js
index f352814..9e17900 100644
--- a/eleventy.config.js
+++ b/eleventy.config.js
@@ -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) => {