atproto blogging
1//! Subdomain Dioxus application.
2//!
3//! Separate router for subdomain hosting with simpler route structure.
4
5use dioxus::prelude::*;
6use jacquard::smol_str::{SmolStr, ToSmolStr};
7use jacquard::types::string::AtIdentifier;
8
9use crate::components::identity::RepositoryIndex;
10use crate::components::{EntryPage, NotebookCss};
11use crate::host_mode::SubdomainContext;
12use crate::views::{NotebookEntryByRkey, NotebookEntryEdit, NotebookIndex, SubdomainNavbar};
13
14/// Subdomain route enum - simpler paths without /:ident/:notebook prefix.
15#[derive(Debug, Clone, Routable, PartialEq)]
16#[rustfmt::skip]
17pub enum SubdomainRoute {
18 #[layout(SubdomainNavbar)]
19 /// Landing page - custom entry or notebook index.
20 #[route("/")]
21 SubdomainLanding {},
22 /// Explicit notebook index.
23 #[route("/index")]
24 SubdomainIndexPage {},
25 /// Entry by title.
26 #[route("/:title")]
27 SubdomainEntry { title: SmolStr },
28 /// Entry by rkey.
29 #[route("/e/:rkey")]
30 SubdomainEntryByRkey { rkey: SmolStr },
31 /// Entry edit by rkey.
32 #[route("/e/:rkey/edit")]
33 SubdomainEntryEdit { rkey: SmolStr },
34 /// Profile/repository view.
35 #[route("/u/:ident")]
36 SubdomainProfile { ident: AtIdentifier<'static> },
37}
38
39/// Look up notebook by global path and build SubdomainContext.
40pub async fn lookup_subdomain_context(
41 fetcher: &crate::fetch::Fetcher,
42 path: &str,
43) -> Option<SubdomainContext> {
44 use jacquard::IntoStatic;
45 use jacquard::smol_str::SmolStr;
46 use jacquard::xrpc::XrpcClient;
47 use weaver_api::sh_weaver::notebook::resolve_global_notebook::ResolveGlobalNotebook;
48
49 let request = ResolveGlobalNotebook::new().path(path).build();
50
51 match fetcher.send(request).await {
52 Ok(response) => match response.into_output() {
53 Ok(output) => {
54 let notebook = output.notebook;
55
56 let owner = notebook.uri.authority().clone().into_static();
57 let Some(rkey) = notebook.uri.rkey() else {
58 tracing::warn!(path, uri = %notebook.uri, "Notebook URI missing rkey");
59 return None;
60 };
61 let rkey = rkey.0.to_smolstr();
62 let notebook_path = notebook
63 .path
64 .map(|p| SmolStr::new(p.as_ref()))
65 .unwrap_or_else(|| SmolStr::new(path));
66
67 tracing::info!(path, %owner, %rkey, "Notebook lookup succeeded");
68 Some(SubdomainContext {
69 owner,
70 notebook_path,
71 notebook_rkey: rkey,
72 notebook_title: notebook.title.clone().unwrap_or_default().to_smolstr(),
73 })
74 }
75 Err(e) => {
76 tracing::warn!(path, error = %e, "Failed to parse notebook response");
77 None
78 }
79 },
80 Err(e) => {
81 tracing::warn!(path, error = %e, "Global notebook lookup request failed");
82 None
83 }
84 }
85}
86
87/// Extract subdomain from host if it matches base domain pattern.
88pub fn extract_subdomain(host: &str, base: &str) -> Option<String> {
89 let suffix = format!(".{}", base);
90 if host.ends_with(&suffix) && host.len() > suffix.len() {
91 Some(host[..host.len() - suffix.len()].to_string())
92 } else {
93 None
94 }
95}
96
97const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css");
98const LAYOUTS_CSS: Asset = asset!("/assets/styling/layouts.css");
99
100/// Root component for subdomain app.
101#[component]
102pub fn SubdomainApp() -> Element {
103 tracing::info!("SubdomainApp: rendering root");
104 rsx! {
105 document::Link { rel: "icon", href: crate::FAVICON }
106 document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
107 document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" }
108 document::Link { rel: "stylesheet", href: crate::THEME_DEFAULTS_CSS }
109 document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Serif:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" }
110 document::Link { rel: "stylesheet", href: crate::MAIN_CSS }
111 document::Link { rel: "stylesheet", href: LAYOUTS_CSS }
112 document::Link { rel: "stylesheet", href: ENTRY_CSS }
113 crate::components::toast::ToastProvider {
114 Router::<SubdomainRoute> {}
115 }
116 }
117}
118
119/// Landing page - check for custom "/" entry, else show notebook index.
120#[component]
121fn SubdomainLanding() -> Element {
122 tracing::info!("SubdomainLanding: start");
123 let ctx = use_context::<SubdomainContext>();
124 tracing::info!("SubdomainLanding: got context, rendering");
125
126 // TODO: Check for entry with custom path "/" for this notebook.
127 // For now, just render the notebook index.
128 rsx! {
129
130 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() }
131 NotebookIndex {
132 ident: ctx.owner_ident(),
133 book_title: ctx.notebook_title.clone(),
134 }
135 }
136}
137
138/// Explicit notebook index route.
139#[component]
140fn SubdomainIndexPage() -> Element {
141 let ctx = use_context::<SubdomainContext>();
142
143 rsx! {
144
145 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() }
146 NotebookIndex {
147 ident: ctx.owner_ident(),
148 book_title: ctx.notebook_title.clone(),
149 }
150 }
151}
152
153/// Entry by title.
154#[component]
155fn SubdomainEntry(title: SmolStr) -> Element {
156 let ctx = use_context::<SubdomainContext>();
157
158 rsx! {
159
160 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() }
161 EntryPage {
162 ident: ctx.owner_ident(),
163 book_title: ctx.notebook_title.clone(),
164 title: title,
165 }
166 }
167}
168
169/// Entry by rkey.
170#[component]
171fn SubdomainEntryByRkey(rkey: SmolStr) -> Element {
172 let ctx = use_context::<SubdomainContext>();
173
174 rsx! {
175
176 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() }
177 NotebookEntryByRkey {
178 ident: ctx.owner_ident(),
179 book_title: ctx.notebook_title.clone(),
180 rkey: rkey,
181 }
182 }
183}
184
185/// Entry edit by rkey.
186#[component]
187fn SubdomainEntryEdit(rkey: SmolStr) -> Element {
188 let ctx = use_context::<SubdomainContext>();
189
190 rsx! {
191
192 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() }
193 NotebookEntryEdit {
194 ident: ctx.owner_ident(),
195 book_title: ctx.notebook_title.clone(),
196 rkey: rkey,
197 }
198 }
199}
200
201/// Profile/repository view for an identity.
202#[component]
203fn SubdomainProfile(ident: AtIdentifier<'static>) -> Element {
204 rsx! {
205 RepositoryIndex { ident }
206 }
207}