Skip to main content

max / makenotwork

4.0 KB · 116 lines History Blame Raw
1 //! Documentation page routes: index and individual doc pages.
2
3 use axum::{
4 extract::{Path, State},
5 response::IntoResponse,
6 Json,
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 AppState,
16 };
17
18 /// GET /docs: index page listing all docs grouped by section.
19 #[tracing::instrument(skip_all, name = "docs::docs_index")]
20 pub async fn docs_index(
21 State(state): State<AppState>,
22 session: Session,
23 MaybeUserUnverified(maybe_user): MaybeUserUnverified,
24 ) -> Result<impl IntoResponse> {
25 let csrf_token = get_csrf_token(&session).await;
26
27 // Group index entries by section, preserving load order.
28 let mut sections: Vec<DocSection> = Vec::new();
29 for entry in state.docs.index() {
30 let section = sections
31 .iter_mut()
32 .find(|s| s.name == entry.section);
33 match section {
34 Some(s) => {
35 s.entries.push(DocSectionEntry {
36 title: entry.title.clone(),
37 slug: entry.slug.clone(),
38 });
39 }
40 None => {
41 sections.push(DocSection {
42 name: entry.section.clone(),
43 entries: vec![DocSectionEntry {
44 title: entry.title.clone(),
45 slug: entry.slug.clone(),
46 }],
47 subsections: Vec::new(),
48 });
49 }
50 }
51 }
52
53 // Post-process the Guide section: bucket entries into subcategories.
54 if let Some(guide) = sections.iter_mut().find(|s| s.name == "Guide") {
55 const SUBCATEGORIES: &[(&str, &[&str])] = &[
56 ("Getting Started", &["getting-started", "sandbox", "profile", "security", "best-practices"]),
57 ("Content & Organization", &["02-content", "items", "projects", "audio", "video", "software", "tags", "metadata", "collections", "blog"]),
58 ("Selling & Revenue", &["03-selling", "pricing", "payouts", "analytics", "promo-codes", "contact-sharing", "fan-plus"]),
59 ("Fans & Distribution", &["fan-guide", "discovery", "rss", "mailing-lists", "export"]),
60 ("Advanced", &["custom-domains", "git", "migration", "tiers"]),
61 ];
62
63 let entries = std::mem::take(&mut guide.entries);
64 for &(label, slugs) in SUBCATEGORIES {
65 let sub_entries: Vec<DocSectionEntry> = slugs
66 .iter()
67 .filter_map(|&slug| entries.iter().find(|e| e.slug == slug))
68 .map(|e| DocSectionEntry {
69 title: e.title.clone(),
70 slug: e.slug.clone(),
71 })
72 .collect();
73 if !sub_entries.is_empty() {
74 guide.subsections.push(DocSubsection {
75 label: label.to_string(),
76 entries: sub_entries,
77 });
78 }
79 }
80 }
81
82 Ok(DocIndexTemplate {
83 csrf_token,
84 session_user: maybe_user,
85 sections,
86 })
87 }
88
89 /// GET /docs/search.json: full-text search index for client-side filtering.
90 #[tracing::instrument(skip_all, name = "docs::docs_search_index")]
91 pub async fn docs_search_index(
92 State(state): State<AppState>,
93 ) -> Json<Vec<docengine::DocSearchEntry>> {
94 Json(state.docs.search_index())
95 }
96
97 /// GET /docs/{slug}: individual doc page.
98 #[tracing::instrument(skip_all, name = "docs::doc_page")]
99 pub async fn doc_page(
100 State(state): State<AppState>,
101 session: Session,
102 MaybeUserUnverified(maybe_user): MaybeUserUnverified,
103 Path(slug): Path<String>,
104 ) -> Result<impl IntoResponse> {
105 let page = state.docs.get(&slug).ok_or(AppError::NotFound)?;
106 let csrf_token = get_csrf_token(&session).await;
107
108 Ok(DocTemplate {
109 csrf_token,
110 session_user: maybe_user,
111 title: page.title.clone(),
112 section: page.section.clone(),
113 content: page.html_content.clone(),
114 })
115 }
116