1use leptos::prelude::*;
11use leptos_meta::{provide_meta_context, Title};
12use leptos_router::components::{ParentRoute, Route, Router, Routes};
13use leptos_router::{path, Lazy, SsrMode};
14
15use crate::components::frontend_layout::FrontendLayout;
16use crate::components::share_meta::provide_share_context;
17use crate::components::ui::{provide_toaster, ToastRegion};
18use crate::context::{ShowcaseFilters, UserContext};
19use crate::navigation::{NavigationBridge, RouteMarker, RouteProgress};
20use crate::pages::about::AboutRoute;
21use crate::pages::admin::assets::AssetsRoute;
22use crate::pages::admin::comments::AdminCommentsRoute;
23use crate::pages::admin::dashboard::DashboardRoute;
24use crate::pages::admin::friends::FriendsAdminRoute;
25use crate::pages::admin::logs::LogsRoute;
26use crate::pages::admin::mcp::McpRoute;
27use crate::pages::admin::note_editor::{EditNoteRoute, NewNoteRoute};
28use crate::pages::admin::notes::notebooks::NotebooksRoute;
29use crate::pages::admin::notes::AdminNotesRoute;
30use crate::pages::admin::posts::PostsRoute;
31use crate::pages::admin::posts_trash::PostsTrashRoute;
32use crate::pages::admin::preview::PreviewRoute;
33use crate::pages::admin::profile::ProfileRoute;
34use crate::pages::admin::runner::RunnerRoute;
35use crate::pages::admin::settings::SettingsRoute;
36use crate::pages::admin::system::SystemRoute;
37use crate::pages::admin::write::{WriteEditRoute, WriteRoute};
38use crate::pages::admin::AdminShellRoute;
39use crate::pages::archives::ArchivesRoute;
40use crate::pages::changelog::ChangelogRoute;
41use crate::pages::components_showcase::{ShowcaseDetailRoute, ShowcaseRoute};
42use crate::pages::friends::FriendsRoute;
43use crate::pages::home::HomeRoute;
44use crate::pages::login::LoginRoute;
45use crate::pages::not_found::NotFound;
46use crate::pages::notes::{NoteRoute, NotebookRoute, NotesRoute};
47use crate::pages::post_detail::PostRoute;
48use crate::pages::register::RegisterRoute;
49use crate::pages::search::SearchRoute;
50use crate::pages::tags::TagRoute;
51use crate::pages::writing_guide::WritingGuideRoute;
52use crate::theme::provide_theme;
53
54#[cfg(feature = "server")]
56pub fn shell(options: LeptosOptions) -> impl IntoView {
57 use crate::static_assets::{
58 self, ADMIN_CSS, CORE_CSS, CORE_JS, HIGHLIGHT_CSS, KATEX_CSS, STYLE_CSS,
59 };
60 use crate::theme::ThemePreload;
61 use leptos_meta::MetaTags;
62
63 let admin_css = crate::request_ctx::request_parts()
65 .is_some_and(|p| static_assets::needs_admin_css(p.uri.path()));
66
67 view! {
68 <!DOCTYPE html>
69 <html lang="zh-CN">
70 <head>
71 <meta charset="utf-8"/>
72 <meta name="viewport" content="width=device-width, initial-scale=1"/>
73 <meta name="theme-color" content="#f5f5f3" media="(prefers-color-scheme: light)"/>
74 <meta name="theme-color" content="#0f0f0e" media="(prefers-color-scheme: dark)"/>
75 <link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
76 <link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48"/>
77 <link rel="apple-touch-icon" href="/apple-touch-icon.png"/>
78 <link rel="manifest" href="/manifest.webmanifest"/>
79 <link rel="stylesheet" href=static_assets::url(STYLE_CSS)/>
80 {admin_css
81 .then(|| {
82 view! {
83 <link
84 rel="stylesheet"
85 id=static_assets::ADMIN_CSS_ELEMENT_ID
86 href=static_assets::url(ADMIN_CSS)
87 />
88 }
89 })}
90 <link rel="stylesheet" href=static_assets::url(HIGHLIGHT_CSS)/>
91 <link rel="stylesheet" href=static_assets::url(KATEX_CSS)/>
92 <link rel="stylesheet" href=static_assets::url(CORE_CSS)/>
93 <script
94 type="application/json"
95 id=static_assets::VERSIONS_ELEMENT_ID
96 inner_html=static_assets::versions_json()
97 ></script>
98 <script src=static_assets::url(CORE_JS)></script>
99 <ThemePreload/>
100 <AutoReload options=options.clone()/>
101 <HydrationScriptsWithPreload options/>
102 <MetaTags/>
103 </head>
104 <body>
105 <App/>
106 </body>
107 </html>
108 }
109}
110
111#[cfg(feature = "server")]
122#[component]
123fn HydrationScriptsWithPreload(options: LeptosOptions) -> impl IntoView {
124 let scripts = view! { <HydrationScripts options/> };
125 let names = crate::request_ctx::request_parts()
126 .and_then(|parts| crate::route_prefetch::match_route(parts.uri.path()))
127 .map(|target| crate::route_prefetch::route_view_names(&target))
128 .unwrap_or_default();
129 let (loader, modules) = leptos::reactive::owner::take_context::<leptos::WasmSplitManifest>()
130 .and_then(|manifest| {
131 manifest.0.try_read_value().map(|value| {
132 let (pkg, manifest, loader) = &*value;
133 (
134 format!("{pkg}/{loader}"),
135 route_modules(manifest, names)
136 .into_iter()
137 .map(|module| format!("{pkg}/{module}.wasm"))
138 .collect::<Vec<_>>(),
139 )
140 })
141 })
142 .unzip();
143 view! {
144 {scripts}
145 {loader.map(|href| view! { <link rel="modulepreload" href=href/> })}
146 {modules
147 .unwrap_or_default()
148 .into_iter()
149 .map(|href| {
150 view! {
151 <link
152 rel="preload"
153 href=href
154 r#as="fetch"
155 r#type="application/wasm"
156 crossorigin="anonymous"
157 fetchpriority="low"
158 />
159 }
160 })
161 .collect_view()}
162 }
163}
164
165#[cfg(feature = "server")]
167fn route_modules<'a>(
168 manifest: &'a std::collections::HashMap<String, Vec<String>>,
169 names: &[&str],
170) -> Vec<&'a str> {
171 let mut modules: Vec<&str> = Vec::new();
172 for name in names {
173 let prefix = format!("__{name}_route_view_");
174 let Some((_, list)) = manifest.iter().find(|(key, _)| {
175 key.strip_prefix(&prefix)
176 .is_some_and(|hash| hash.bytes().all(|b| b.is_ascii_digit()))
177 }) else {
178 continue;
179 };
180 for module in list {
181 if !modules.contains(&module.as_str()) {
182 modules.push(module);
183 }
184 }
185 }
186 modules
187}
188
189#[component]
191pub fn App() -> impl IntoView {
192 provide_meta_context();
193 provide_share_context();
194 provide_theme();
195 provide_context(UserContext::new());
196 provide_context(RwSignal::new(ShowcaseFilters::default()));
197 provide_toaster();
198 crate::hooks::ssr_resource::provide_route_data_registry();
199 let (is_routing, set_is_routing) = signal(false);
201
202 view! {
204 <Title text="Yggdrasil Blog"/>
205 <Router set_is_routing>
206 <NavigationBridge/>
207 <RouteMarker is_routing>
208 <Routes fallback=|| {
209 view! {
210 <FrontendLayout>
211 <NotFound/>
212 </FrontendLayout>
213 }
214 }>
215 <ParentRoute path=path!("") view=|| view! { <FrontendLayout/> }>
216 <Route
217 path=path!("")
218 view={Lazy::<HomeRoute>::new()}
219 ssr=SsrMode::InOrder
220 />
221 <Route
224 path=path!("page/:page")
225 view={Lazy::<HomeRoute>::new()}
226 ssr=SsrMode::Async
227 />
228 <Route
229 path=path!("notes")
230 view={Lazy::<NotesRoute>::new()}
231 ssr=SsrMode::InOrder
232 />
233 <Route
234 path=path!("notes/book/:id")
235 view={Lazy::<NotebookRoute>::new()}
236 ssr=SsrMode::Async
237 />
238 <Route
239 path=path!("notes/:slug")
240 view={Lazy::<NoteRoute>::new()}
241 ssr=SsrMode::Async
242 />
243 <Route
244 path=path!("archives")
245 view={Lazy::<ArchivesRoute>::new()}
246 ssr=SsrMode::InOrder
247 />
248 <Route
249 path=path!("tags/:tag")
250 view={Lazy::<TagRoute>::new()}
251 ssr=SsrMode::Async
252 />
253 <Route
254 path=path!("post/:slug")
255 view={Lazy::<PostRoute>::new()}
256 ssr=SsrMode::Async
257 />
258 <Route path=path!("search") view={Lazy::<SearchRoute>::new()}/>
259 <Route
260 path=path!("about")
261 view={Lazy::<AboutRoute>::new()}
262 ssr=SsrMode::InOrder
263 />
264 <Route
265 path=path!("about/writing")
266 view={Lazy::<WritingGuideRoute>::new()}
267 ssr=SsrMode::InOrder
268 />
269 <Route
270 path=path!("about/components")
271 view={Lazy::<ShowcaseRoute>::new()}
272 ssr=SsrMode::Async
273 />
274 <Route
275 path=path!("about/components/:component")
276 view={Lazy::<ShowcaseDetailRoute>::new()}
277 ssr=SsrMode::Async
278 />
279 <Route
280 path=path!("friends")
281 view={Lazy::<FriendsRoute>::new()}
282 ssr=SsrMode::InOrder
283 />
284 <Route
285 path=path!("changelog")
286 view={Lazy::<ChangelogRoute>::new()}
287 ssr=SsrMode::Async
288 />
289 </ParentRoute>
290 <ParentRoute path=path!("admin") view={Lazy::<AdminShellRoute>::new()}>
291 <Route path=path!("") view={Lazy::<DashboardRoute>::new()}/>
292 <Route path=path!("write") view={Lazy::<WriteRoute>::new()}/>
293 <Route path=path!("write/:id") view={Lazy::<WriteEditRoute>::new()}/>
294 <Route path=path!("preview/:slug") view={Lazy::<PreviewRoute>::new()}/>
295 <Route path=path!("posts") view={Lazy::<PostsRoute>::new()}/>
296 <Route path=path!("posts/trash") view={Lazy::<PostsTrashRoute>::new()}/>
297 <Route path=path!("notes") view={Lazy::<AdminNotesRoute>::new()}/>
298 <Route path=path!("notes/new") view={Lazy::<NewNoteRoute>::new()}/>
299 <Route path=path!("notes/edit/:id") view={Lazy::<EditNoteRoute>::new()}/>
300 <Route path=path!("notebooks") view={Lazy::<NotebooksRoute>::new()}/>
301 <Route path=path!("assets") view={Lazy::<AssetsRoute>::new()}/>
302 <Route path=path!("friends") view={Lazy::<FriendsAdminRoute>::new()}/>
303 <Route path=path!("comments") view={Lazy::<AdminCommentsRoute>::new()}/>
304 <Route
305 path=path!("comments/:page")
306 view={Lazy::<AdminCommentsRoute>::new()}
307 />
308 <Route path=path!("system") view={Lazy::<SystemRoute>::new()}/>
309 <Route path=path!("runner") view={Lazy::<RunnerRoute>::new()}/>
310 <Route path=path!("mcp") view={Lazy::<McpRoute>::new()}/>
311 <Route path=path!("logs") view={Lazy::<LogsRoute>::new()}/>
312 <Route path=path!("settings") view={Lazy::<SettingsRoute>::new()}/>
313 <Route path=path!("profile") view={Lazy::<ProfileRoute>::new()}/>
314 </ParentRoute>
315 <Route path=path!("login") view={Lazy::<LoginRoute>::new()}/>
316 <Route path=path!("register") view={Lazy::<RegisterRoute>::new()}/>
317 </Routes>
318 </RouteMarker>
319 </Router>
320 <RouteProgress is_routing/>
321 <ToastRegion/>
322 }
323}
324
325#[cfg(all(test, feature = "server"))]
326mod tests {
327 use super::route_modules;
328
329 #[test]
330 fn route_modules_match_whole_route_name_and_dedupe() {
331 let manifest = [
332 (
333 "__note_route_view_11",
334 vec!["chunk_1.a", "split___note_route_view_11.x"],
335 ),
336 ("__notebook_route_view_12", vec!["chunk_2.a"]),
337 ("__friends_admin_route_view_13", vec!["chunk_3.a"]),
338 (
339 "__admin_shell_route_view_14",
340 vec!["chunk_1.a", "chunk_4.a"],
341 ),
342 ("__dashboard_route_view_15", vec!["chunk_4.a", "chunk_5.a"]),
343 ("fetch_note_16", vec!["chunk_6.a"]),
344 ]
345 .into_iter()
346 .map(|(key, modules)| {
347 (
348 key.to_string(),
349 modules.into_iter().map(String::from).collect(),
350 )
351 })
352 .collect();
353
354 assert_eq!(
356 route_modules(&manifest, &["note"]),
357 ["chunk_1.a", "split___note_route_view_11.x"]
358 );
359 assert_eq!(
361 route_modules(&manifest, &["admin_shell", "dashboard"]),
362 ["chunk_1.a", "chunk_4.a", "chunk_5.a"]
363 );
364 assert!(route_modules(&manifest, &["friends"]).is_empty());
366 }
367}