Skip to main content

max / makenotwork

5.2 KB · 165 lines History Blame Raw
1 //! Documentation page routes: index and individual doc pages.
2
3 use axum::{
4 Json,
5 extract::{Path, State},
6 response::IntoResponse,
7 };
8 use tower_sessions::Session;
9
10 use crate::{
11 auth::MaybeUserUnverified,
12 error::{AppError, Result},
13 helpers::get_csrf_token,
14 templates::{DocIndexTemplate, DocSection, DocSectionEntry, DocSubsection, DocTemplate},
15 };
16
17 /// GET /docs: index page listing all docs grouped by section.
18 #[tracing::instrument(skip_all, name = "docs::docs_index")]
19 pub(super) async fn docs_index(
20 State(docs): State<std::sync::Arc<docengine::DocLoader>>,
21 session: Session,
22 MaybeUserUnverified(maybe_user): MaybeUserUnverified,
23 ) -> Result<impl IntoResponse> {
24 let csrf_token = get_csrf_token(&session).await;
25
26 // Group index entries by section, preserving load order.
27 let mut sections: Vec<DocSection> = Vec::new();
28 for entry in docs.index() {
29 let section = sections.iter_mut().find(|s| s.name == entry.section);
30 match section {
31 Some(s) => {
32 s.entries.push(DocSectionEntry {
33 title: entry.title.clone(),
34 slug: entry.slug.clone(),
35 });
36 }
37 None => {
38 sections.push(DocSection {
39 name: entry.section.clone(),
40 entries: vec![DocSectionEntry {
41 title: entry.title.clone(),
42 slug: entry.slug.clone(),
43 }],
44 subsections: Vec::new(),
45 });
46 }
47 }
48 }
49
50 // Post-process the Guide section: bucket entries into subcategories.
51 if let Some(guide) = sections.iter_mut().find(|s| s.name == "Guide") {
52 const SUBCATEGORIES: &[(&str, &[&str])] = &[
53 (
54 "Getting Started",
55 &[
56 "getting-started",
57 "sandbox",
58 "profile",
59 "security",
60 "best-practices",
61 ],
62 ),
63 (
64 "Content & Organization",
65 &[
66 "02-content",
67 "items",
68 "projects",
69 "audio",
70 "video",
71 "software",
72 "tags",
73 "metadata",
74 "collections",
75 "blog",
76 ],
77 ),
78 (
79 "Selling & Revenue",
80 &[
81 "03-selling",
82 "pricing",
83 "payouts",
84 "analytics",
85 "promo-codes",
86 "contact-sharing",
87 "fan-plus",
88 ],
89 ),
90 (
91 "Fans & Distribution",
92 &["fan-guide", "discovery", "rss", "mailing-lists", "export"],
93 ),
94 ("Advanced", &["custom-domains", "git", "migration", "tiers"]),
95 ];
96
97 let entries = std::mem::take(&mut guide.entries);
98 for &(label, slugs) in SUBCATEGORIES {
99 let sub_entries: Vec<DocSectionEntry> = slugs
100 .iter()
101 .filter_map(|&slug| entries.iter().find(|e| e.slug == slug))
102 .map(|e| DocSectionEntry {
103 title: e.title.clone(),
104 slug: e.slug.clone(),
105 })
106 .collect();
107 if !sub_entries.is_empty() {
108 guide.subsections.push(DocSubsection {
109 label: label.to_string(),
110 entries: sub_entries,
111 });
112 }
113 }
114 }
115
116 Ok(DocIndexTemplate {
117 csrf_token,
118 session_user: maybe_user,
119 sections,
120 })
121 }
122
123 /// GET /docs/search.json: full-text search index for client-side filtering.
124 #[tracing::instrument(skip_all, name = "docs::docs_search_index")]
125 pub(super) async fn docs_search_index(
126 State(docs): State<std::sync::Arc<docengine::DocLoader>>,
127 ) -> Json<Vec<docengine::DocSearchEntry>> {
128 Json(docs.search_index())
129 }
130
131 /// GET /docs/{slug}: individual doc page.
132 #[tracing::instrument(skip_all, name = "docs::doc_page")]
133 pub(super) async fn doc_page(
134 State(docs): State<std::sync::Arc<docengine::DocLoader>>,
135 session: Session,
136 MaybeUserUnverified(maybe_user): MaybeUserUnverified,
137 Path(slug): Path<String>,
138 ) -> Result<impl IntoResponse> {
139 let page = docs.get(&slug).ok_or(AppError::NotFound)?;
140 let csrf_token = get_csrf_token(&session).await;
141
142 // "What links here": resolve each source slug to its title off the link
143 // graph. A source is always a served page; filter_map skips any that ever
144 // stops resolving 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
156 Ok(DocTemplate {
157 csrf_token,
158 session_user: maybe_user,
159 title: page.title.clone(),
160 section: page.section.clone(),
161 content: page.html_content.clone(),
162 backlinks,
163 })
164 }
165