atproto blogging
1//! Custom domain Dioxus application.
2//!
3//! Separate router for custom domain hosting (e.g., myblog.com).
4//! Uses site.standard.publication documents with path-based routing.
5
6use dioxus::prelude::*;
7use jacquard::smol_str::SmolStr;
8use jacquard::types::string::AtIdentifier;
9
10use crate::components::identity::RepositoryIndex;
11use crate::host_mode::CustomDomainContext;
12
13/// Custom domain route enum - path-based routing for publications.
14#[derive(Debug, Clone, Routable, PartialEq)]
15#[rustfmt::skip]
16pub enum CustomDomainRoute {
17 #[layout(CustomDomainLayout)]
18 /// Root/landing page.
19 #[route("/")]
20 Root {},
21 /// Explicit index page.
22 #[route("/index")]
23 Index {},
24 /// Entry by rkey (direct lookup).
25 #[route("/e/:rkey")]
26 EntryByRkey { rkey: SmolStr },
27 /// Entry edit by rkey.
28 #[route("/e/:rkey/edit")]
29 EntryEdit { rkey: SmolStr },
30 /// Profile/repository view.
31 #[route("/u/:ident")]
32 Profile { ident: AtIdentifier<'static> },
33 /// Path-based document (catch-all, must be last).
34 #[route("/*segments")]
35 PathPage { segments: Vec<String> },
36}
37
38/// Root component for custom domain app.
39#[component]
40pub fn CustomDomainApp() -> Element {
41 rsx! {
42 document::Link { rel: "icon", href: crate::FAVICON }
43 document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
44 document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" }
45 document::Link { rel: "stylesheet", href: crate::THEME_DEFAULTS_CSS }
46 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" }
47 document::Link { rel: "stylesheet", href: crate::MAIN_CSS }
48 crate::components::toast::ToastProvider {
49 Router::<CustomDomainRoute> {}
50 }
51 }
52}
53
54#[component]
55fn CustomDomainLayout() -> Element {
56 let ctx = use_context::<CustomDomainContext>();
57 rsx! {
58 nav { class: "custom-domain-nav",
59 a { href: "/", "{ctx.publication_name}" }
60 }
61 main { Outlet::<CustomDomainRoute> {} }
62 }
63}
64
65#[component]
66fn Root() -> Element {
67 rsx! { Index {} }
68}
69
70#[component]
71fn Index() -> Element {
72 let ctx = use_context::<CustomDomainContext>();
73
74 rsx! {
75 crate::views::NotebookIndex {
76 ident: ctx.owner.clone(),
77 book_title: ctx.publication_name.clone(),
78 }
79 }
80}
81
82#[component]
83fn EntryByRkey(rkey: ReadSignal<SmolStr>) -> Element {
84 let ctx = use_context::<CustomDomainContext>();
85
86 rsx! {
87 crate::views::NotebookEntryByRkey {
88 ident: ctx.owner.clone(),
89 book_title: ctx.publication_name.clone(),
90 rkey: rkey,
91 }
92 }
93}
94
95#[component]
96fn EntryEdit(rkey: ReadSignal<SmolStr>) -> Element {
97 let ctx = use_context::<CustomDomainContext>();
98
99 rsx! {
100 crate::views::NotebookEntryEdit {
101 ident: ctx.owner.clone(),
102 book_title: ctx.publication_name.clone(),
103 rkey: rkey,
104 }
105 }
106}
107
108#[component]
109fn Profile(ident: AtIdentifier<'static>) -> Element {
110 rsx! {
111 RepositoryIndex { ident }
112 }
113}
114
115#[component]
116fn PathPage(segments: ReadSignal<Vec<String>>) -> Element {
117 let ctx = use_context::<CustomDomainContext>();
118
119 let ident = use_memo(move || ctx.owner.clone());
120 let rkey = use_memo(move || ctx.publication_rkey.clone());
121 let path = use_memo(move || format!("/{}", segments().join("/")));
122
123 let (doc_res, doc_data) =
124 crate::data::use_custom_domain_document_data(ident.into(), rkey.into(), path.into());
125
126 #[cfg(feature = "fullstack-server")]
127 let _doc_res = doc_res?;
128
129 #[cfg(not(feature = "fullstack-server"))]
130 let _ = doc_res;
131
132 match &*doc_data.read() {
133 Some(data) => {
134 let title = data.document.title.as_ref();
135
136 rsx! {
137 article { class: "document-content",
138 h1 { "{title}" }
139 div {
140 class: "rendered-content",
141 dangerous_inner_html: "{data.rendered_html}"
142 }
143 }
144 }
145 }
146 None => rsx! {
147 div { class: "loading", "Loading..." }
148 },
149 }
150}