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.
4 files changed,
+56 insertions,
-0 deletions
| 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 |
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 |
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 |
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">
|