Skip to main content

max / makenotwork

docengine backlinks: render "Referenced by" section on doc pages Resolve DocLoader::backlinks() to titled entries in the doc_page route and render them as a section in the page template, wiring the reverse link graph through to the public docs UI.
Author: Max Johnson <me@maxj.phd> · 2026-07-23 17:07 UTC
Commit: c9b4682d318bc289753aacb4aedcf85b0b7f1d82
Parent: 4f33d24
4 files changed, +56 insertions, -0 deletions
@@ -139,11 +139,26 @@ pub async fn doc_page(
139 139 let page = docs.get(&slug).ok_or(AppError::NotFound)?;
140 140 let csrf_token = get_csrf_token(&session).await;
141 141
142 + // "What links here": resolve each source slug to its title off the link
143 + // graph. A source is always a served page, but filter_map keeps this robust
144 + // if that ever stops holding rather than rendering a titleless entry.
145 + let backlinks: Vec<DocSectionEntry> = docs
146 + .backlinks(&slug)
147 + .iter()
148 + .filter_map(|src| {
149 + docs.get(src).map(|p| DocSectionEntry {
150 + title: p.title.clone(),
151 + slug: src.clone(),
152 + })
153 + })
154 + .collect();
155 +
142 156 Ok(DocTemplate {
143 157 csrf_token,
144 158 session_user: maybe_user,
145 159 title: page.title.clone(),
146 160 section: page.section.clone(),
147 161 content: page.html_content.clone(),
162 + backlinks,
148 163 })
149 164 }
@@ -773,6 +773,10 @@ pub struct DocTemplate {
773 773 pub title: String,
774 774 pub section: String,
775 775 pub content: String,
776 + /// Pages that link to this one ("what links here"), in docs-index order.
777 + /// Empty when nothing links here, in which case the template omits the
778 + /// section entirely.
779 + pub backlinks: Vec<DocSectionEntry>,
776 780 }
777 781
778 782 /// Entry in a doc section for the index page.
@@ -6528,6 +6528,32 @@ textarea:focus-visible {
6528 6528 .doc-body th, .doc-body td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
6529 6529 .doc-body th { font-family: var(--font-mono); font-weight: 600; }
6530 6530
6531 + .doc-backlinks {
6532 + margin-top: 4rem;
6533 + padding-top: 1.5rem;
6534 + border-top: 1px solid var(--border);
6535 + }
6536 +
6537 + .doc-backlinks-title {
6538 + font-family: var(--font-mono);
6539 + font-size: 0.875rem;
6540 + font-weight: 600;
6541 + text-transform: uppercase;
6542 + letter-spacing: 0.05em;
6543 + color: var(--content-muted);
6544 + margin-bottom: 1rem;
6545 + }
6546 +
6547 + .doc-backlinks-list {
6548 + list-style: none;
6549 + margin: 0;
6550 + padding: 0;
6551 + }
6552 +
6553 + .doc-backlinks-list li { margin-bottom: 0.5rem; }
6554 + .doc-backlinks-list a { color: var(--content); text-decoration: none; }
6555 + .doc-backlinks-list a:hover { text-decoration: underline; }
6556 +
6531 6557 .text-reader-footer {
6532 6558 font-family: var(--font-mono);
6533 6559 text-align: center;
@@ -21,6 +21,17 @@
21 21 <div class="doc-body">
22 22 {{ content|safe }}
23 23 </div>
24 +
25 + {% if !backlinks.is_empty() %}
26 + <nav class="doc-backlinks" aria-label="Referenced by">
27 + <h2 class="doc-backlinks-title">Referenced by</h2>
28 + <ul class="doc-backlinks-list">
29 + {% for entry in backlinks %}
30 + <li><a href="/docs/{{ entry.slug }}">{{ entry.title }}</a></li>
31 + {% endfor %}
32 + </ul>
33 + </nav>
34 + {% endif %}
24 35 </article>
25 36
26 37 <footer class="text-reader-footer">