atproto blogging
1//! Host mode context for subdomain and custom domain routing.
2
3use crate::env::WEAVER_APP_HOST;
4use jacquard::smol_str::{SmolStr, format_smolstr};
5use jacquard::types::string::AtIdentifier;
6use serde::{Deserialize, Serialize};
7
8/// Unified host context resolved by middleware.
9///
10/// This is inserted into request extensions by the host resolution middleware
11/// and read by the Dioxus app to determine which router to use.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub enum HostContext {
14 /// Main weaver.sh domain - use full Route enum.
15 MainDomain,
16 /// Subdomain hosting (notebook.weaver.sh) - use SubdomainRoute.
17 Subdomain(SubdomainContext),
18 /// Custom domain (myblog.com) - use CustomDomainRoute.
19 CustomDomain(CustomDomainContext),
20}
21
22impl HostContext {
23 /// Get the link mode for this host context.
24 pub fn link_mode(&self) -> LinkMode {
25 match self {
26 HostContext::MainDomain => LinkMode::MainDomain,
27 HostContext::Subdomain(_) => LinkMode::Subdomain,
28 HostContext::CustomDomain(_) => LinkMode::CustomDomain,
29 }
30 }
31}
32
33/// Context for subdomain routing - identifies the notebook being served.
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(bound(deserialize = ""))]
36pub struct SubdomainContext {
37 /// DID of the notebook owner.
38 #[serde(deserialize_with = "deserialize_static_ident")]
39 pub owner: AtIdentifier<'static>,
40 /// Notebook path (same as subdomain).
41 pub notebook_path: SmolStr,
42
43 /// Notebook title.
44 pub notebook_title: SmolStr,
45 /// Notebook rkey for direct lookups.
46 pub notebook_rkey: SmolStr,
47}
48
49fn deserialize_static_ident<'de, D>(deserializer: D) -> Result<AtIdentifier<'static>, D::Error>
50where
51 D: serde::Deserializer<'de>,
52{
53 use jacquard::IntoStatic;
54 let did: AtIdentifier<'de> = Deserialize::deserialize(deserializer)?;
55 Ok(did.into_static())
56}
57
58impl SubdomainContext {
59 /// Get the owner as an AtIdentifier for route parameters.
60 pub fn owner_ident(&self) -> AtIdentifier<'static> {
61 self.owner.clone()
62 }
63}
64
65/// Context for custom domain routing - identifies the publication being served.
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(bound(deserialize = ""))]
68pub struct CustomDomainContext {
69 /// Custom domain (e.g., "myblog.com").
70 pub domain: SmolStr,
71 /// DID of the publication owner.
72 #[serde(deserialize_with = "deserialize_static_ident")]
73 pub owner: AtIdentifier<'static>,
74 /// Publication rkey.
75 pub publication_rkey: SmolStr,
76 /// Publication name.
77 pub publication_name: SmolStr,
78 /// For weaver-backed publications, the backing notebook URI.
79 pub notebook_uri: Option<SmolStr>,
80}
81
82/// Link mode for generating appropriate URLs based on host context.
83///
84/// Components use this context to generate links that work on both
85/// the main domain and subdomain hosting.
86#[derive(Clone, Debug, Default, PartialEq, Eq)]
87pub enum LinkMode {
88 /// Main domain - full paths with /:ident/:notebook/:entry
89 #[default]
90 MainDomain,
91 /// Subdomain - simplified paths like /:entry
92 Subdomain,
93 /// Custom domain - paths like /:path for publications
94 CustomDomain,
95}
96
97impl LinkMode {
98 /// Check if we're in subdomain mode.
99 pub fn is_subdomain(&self) -> bool {
100 matches!(self, LinkMode::Subdomain)
101 }
102
103 /// Check if we're in a hosted mode (subdomain or custom domain).
104 pub fn is_hosted(&self) -> bool {
105 matches!(self, LinkMode::Subdomain | LinkMode::CustomDomain)
106 }
107
108 /// Generate link to a notebook entry by title.
109 pub fn entry_link(
110 &self,
111 ident: &AtIdentifier<'_>,
112 book_title: &str,
113 entry_title: &str,
114 ) -> SmolStr {
115 match self {
116 LinkMode::MainDomain => format_smolstr!("/{}/{}/{}", ident, book_title, entry_title),
117 LinkMode::Subdomain | LinkMode::CustomDomain => format_smolstr!("/{}", entry_title),
118 }
119 }
120
121 /// Generate link to a notebook entry by rkey.
122 pub fn entry_rkey_link(
123 &self,
124 ident: &AtIdentifier<'_>,
125 book_title: &str,
126 rkey: &str,
127 ) -> SmolStr {
128 match self {
129 LinkMode::MainDomain => format_smolstr!("/{}/{}/e/{}", ident, book_title, rkey),
130 LinkMode::Subdomain | LinkMode::CustomDomain => format_smolstr!("/e/{}", rkey),
131 }
132 }
133
134 /// Generate link to edit a notebook entry by rkey.
135 pub fn entry_edit_link(
136 &self,
137 ident: &AtIdentifier<'_>,
138 book_title: &str,
139 rkey: &str,
140 ) -> SmolStr {
141 match self {
142 LinkMode::MainDomain => format_smolstr!("/{}/{}/e/{}/edit", ident, book_title, rkey),
143 LinkMode::Subdomain | LinkMode::CustomDomain => format_smolstr!("/e/{}/edit", rkey),
144 }
145 }
146
147 /// Generate link to a notebook index.
148 pub fn notebook_link(&self, ident: &AtIdentifier<'_>, book_title: &str) -> SmolStr {
149 match self {
150 LinkMode::MainDomain => format_smolstr!("/{}/{}", ident, book_title),
151 LinkMode::Subdomain | LinkMode::CustomDomain => SmolStr::new_static("/"),
152 }
153 }
154
155 /// Generate link to a profile/repository.
156 pub fn profile_link(&self, ident: &AtIdentifier<'_>) -> SmolStr {
157 match self {
158 LinkMode::MainDomain => format_smolstr!("/{}", ident),
159 LinkMode::Subdomain | LinkMode::CustomDomain => format_smolstr!("/u/{}", ident),
160 }
161 }
162
163 /// Generate link to a standalone entry.
164 pub fn standalone_entry_link(&self, ident: &AtIdentifier<'_>, rkey: &str) -> SmolStr {
165 match self {
166 LinkMode::MainDomain => format_smolstr!("/{}/e/{}", ident, rkey),
167 // Standalone entries don't exist in hosted mode - link to main domain
168 LinkMode::Subdomain | LinkMode::CustomDomain => {
169 format_smolstr!("{}/{}/e/{}", WEAVER_APP_HOST, ident, rkey)
170 }
171 }
172 }
173
174 /// Generate link to edit a standalone entry.
175 pub fn standalone_entry_edit_link(&self, ident: &AtIdentifier<'_>, rkey: &str) -> SmolStr {
176 match self {
177 LinkMode::MainDomain => format_smolstr!("/{}/e/{}/edit", ident, rkey),
178 // Edit on main domain
179 LinkMode::Subdomain | LinkMode::CustomDomain => {
180 format_smolstr!("{}/{}/e/{}/edit", WEAVER_APP_HOST, ident, rkey)
181 }
182 }
183 }
184
185 /// Generate link to create a new draft.
186 pub fn new_draft_link(&self, ident: &AtIdentifier<'_>, notebook: Option<&str>) -> SmolStr {
187 match (self, notebook) {
188 (LinkMode::MainDomain, Some(nb)) => format_smolstr!("/{}/new?notebook={}", ident, nb),
189 (LinkMode::MainDomain, None) => format_smolstr!("/{}/new", ident),
190 // Drafts are on main domain
191 (LinkMode::Subdomain | LinkMode::CustomDomain, Some(nb)) => {
192 format_smolstr!("{}/{}/new?notebook={}", WEAVER_APP_HOST, ident, nb)
193 }
194 (LinkMode::Subdomain | LinkMode::CustomDomain, None) => {
195 format_smolstr!("{}/{}/new", WEAVER_APP_HOST, ident)
196 }
197 }
198 }
199
200 /// Generate link to drafts list.
201 pub fn drafts_link(&self, ident: &AtIdentifier<'_>) -> SmolStr {
202 match self {
203 LinkMode::MainDomain => format_smolstr!("/{}/drafts", ident),
204 LinkMode::Subdomain | LinkMode::CustomDomain => {
205 format_smolstr!("{}/{}/drafts", WEAVER_APP_HOST, ident)
206 }
207 }
208 }
209
210 /// Generate link to invites page.
211 pub fn invites_link(&self, ident: &AtIdentifier<'_>) -> SmolStr {
212 match self {
213 LinkMode::MainDomain => format_smolstr!("/{}/invites", ident),
214 LinkMode::Subdomain | LinkMode::CustomDomain => {
215 format_smolstr!("{}/{}/invites", WEAVER_APP_HOST, ident)
216 }
217 }
218 }
219
220 /// Generate link to a document by path (for custom domain publications).
221 pub fn document_link(&self, path: &str) -> SmolStr {
222 format_smolstr!("/{}", path.trim_start_matches('/'))
223 }
224}