ssr options for stuff

Orual 23946661 6b7587e4

+288 -134
-10
crates/weaver-app/.env-example
··· 1 - WEAVER_APP_ENV="dev" 2 - WEAVER_APP_HOST="http://localhost" 3 - WEAVER_APP_DOMAIN="" 4 - WEAVER_PORT=8080 5 - WEAVER_APP_SCOPES="atproto transition:generic" 6 - WEAVER_CLIENT_NAME="Weaver" 7 - 8 - WEAVER_LOGO_URI="" 9 - WEAVER_TOS_URI="" 10 - WEAVER_PRIVACY_POLICY_URI=""
+10
crates/weaver-app/.env-prod
··· 1 + WEAVER_APP_ENV="prod" 2 + WEAVER_APP_HOST="https://alpha.weaver.sh" 3 + WEAVER_APP_DOMAIN="https://alpha.weaver.sh" 4 + WEAVER_PORT=8080 5 + WEAVER_APP_SCOPES="atproto transition:generic" 6 + WEAVER_CLIENT_NAME="Weaver" 7 + 8 + WEAVER_LOGO_URI="" 9 + WEAVER_TOS_URI="" 10 + WEAVER_PRIVACY_POLICY_URI=""
+5 -5
crates/weaver-app/src/components/css.rs
··· 1 1 #[allow(unused_imports)] 2 2 use crate::fetch; 3 3 #[allow(unused_imports)] 4 - use dioxus::{prelude::*, CapturedError}; 4 + use dioxus::{CapturedError, prelude::*}; 5 5 6 6 #[cfg(feature = "fullstack-server")] 7 7 use dioxus::fullstack::response::Response; ··· 20 20 use dioxus::fullstack::get_server_url; 21 21 rsx! { 22 22 document::Stylesheet { 23 - href: "{get_server_url()}/{ident}/{notebook}/css" 23 + href: "{get_server_url()}/css/{ident}/{notebook}" 24 24 } 25 25 } 26 26 } ··· 30 30 pub fn NotebookCss(ident: SmolStr, notebook: SmolStr) -> Element { 31 31 use jacquard::client::AgentSessionExt; 32 32 use jacquard::types::ident::AtIdentifier; 33 - use jacquard::{from_data, CowStr}; 33 + use jacquard::{CowStr, from_data}; 34 34 use weaver_api::sh_weaver::notebook::book::Book; 35 35 use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 36 36 use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; ··· 87 87 } 88 88 89 89 #[cfg(feature = "fullstack-server")] 90 - #[get("/{ident}/{notebook}/css", fetcher: Extension<Arc<fetch::CachedFetcher>>)] 90 + #[get("/css/{ident}/{notebook}", fetcher: Extension<Arc<fetch::CachedFetcher>>)] 91 91 pub async fn css(ident: SmolStr, notebook: SmolStr) -> Result<Response> { 92 92 use dioxus::fullstack::http::header::CONTENT_TYPE; 93 93 use jacquard::client::AgentSessionExt; 94 94 use jacquard::types::ident::AtIdentifier; 95 - use jacquard::{from_data, CowStr}; 95 + use jacquard::{CowStr, from_data}; 96 96 97 97 use weaver_api::sh_weaver::notebook::book::Book; 98 98 use weaver_renderer::css::{generate_base_css, generate_syntax_css};
+22 -26
crates/weaver-app/src/components/identity.rs
··· 1 - use crate::{Route, fetch}; 1 + use crate::{Route, data, fetch}; 2 2 use dioxus::prelude::*; 3 3 use jacquard::{smol_str::SmolStr, types::ident::AtIdentifier}; 4 4 use weaver_api::com_atproto::repo::strong_ref::StrongRef; ··· 20 20 pub fn RepositoryIndex(ident: ReadSignal<AtIdentifier<'static>>) -> Element { 21 21 use crate::components::ProfileDisplay; 22 22 23 - let fetcher = use_context::<fetch::CachedFetcher>(); 24 - 25 - // Fetch notebooks for this specific DID 26 - let notebooks = use_resource(move || { 27 - let fetcher = fetcher.clone(); 28 - async move { fetcher.fetch_notebooks_for_did(&ident()).await } 29 - }); 23 + // Fetch notebooks for this specific DID with SSR support 24 + let notebooks = data::use_notebooks_for_did(ident()).ok(); 30 25 31 26 rsx! { 32 27 document::Stylesheet { href: NOTEBOOK_CARD_CSS } ··· 40 35 // Main content area 41 36 main { class: "repository-main", 42 37 div { class: "notebooks-list", 43 - match notebooks() { 44 - Some(Ok(notebook_list)) => rsx! { 45 - for notebook in notebook_list.iter() { 46 - { 47 - let view = &notebook.0; 48 - let entries = &notebook.1; 49 - rsx! { 50 - div { 51 - key: "{view.cid}", 52 - NotebookCard { 53 - notebook: view.clone(), 54 - entry_refs: entries.clone() 38 + if let Some(notebooks_memo) = &notebooks { 39 + match &*notebooks_memo.read_unchecked() { 40 + Some(notebook_list) => rsx! { 41 + for notebook in notebook_list.iter() { 42 + { 43 + let view = &notebook.0; 44 + let entries = &notebook.1; 45 + rsx! { 46 + div { 47 + key: "{view.cid}", 48 + NotebookCard { 49 + notebook: view.clone(), 50 + entry_refs: entries.clone() 51 + } 55 52 } 56 53 } 57 54 } 58 55 } 56 + }, 57 + None => rsx! { 58 + div { "Loading notebooks..." } 59 59 } 60 - }, 61 - Some(Err(_)) => rsx! { 62 - div { "Error loading notebooks" } 63 - }, 64 - None => rsx! { 65 - div { "Loading notebooks..." } 66 60 } 61 + } else { 62 + div { "Loading notebooks..." } 67 63 } 68 64 } 69 65 }
+187 -1
crates/weaver-app/src/data.rs
··· 18 18 }; 19 19 #[allow(unused_imports)] 20 20 use std::sync::Arc; 21 - use weaver_api::sh_weaver::notebook::{BookEntryView, entry::Entry}; 21 + use weaver_api::sh_weaver::notebook::{BookEntryView, NotebookView, entry::Entry}; 22 22 // ============================================================================ 23 23 // Wrapper Hooks (feature-gated) 24 24 // ============================================================================ ··· 358 358 .await 359 359 .ok() 360 360 .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) 361 + } 362 + })); 363 + Ok(use_memo(move || { 364 + r.read_unchecked().as_ref().and_then(|v| v.clone()) 365 + })) 366 + } 367 + 368 + /// Fetches notebooks from UFOS with SSR support in fullstack mode 369 + #[cfg(feature = "fullstack-server")] 370 + pub fn use_notebooks_from_ufos() -> Result< 371 + Memo<Option<Vec<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>>, 372 + RenderError, 373 + > { 374 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 375 + let res = use_server_future(move || { 376 + let fetcher = fetcher.clone(); 377 + async move { 378 + fetcher 379 + .fetch_notebooks_from_ufos() 380 + .await 381 + .ok() 382 + .map(|notebooks| { 383 + notebooks 384 + .iter() 385 + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 386 + .collect::<Option<Vec<_>>>() 387 + }) 388 + .flatten() 389 + } 390 + })?; 391 + Ok(use_memo(move || { 392 + if let Some(Some(values)) = &*res.read_unchecked() { 393 + values 394 + .iter() 395 + .map(|v| { 396 + jacquard::from_json_value::<( 397 + NotebookView, 398 + Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>, 399 + )>(v.clone()) 400 + .ok() 401 + }) 402 + .collect::<Option<Vec<_>>>() 403 + } else { 404 + None 405 + } 406 + })) 407 + } 408 + 409 + /// Fetches notebooks from UFOS client-side only (no SSR) 410 + #[cfg(not(feature = "fullstack-server"))] 411 + pub fn use_notebooks_from_ufos() -> Result< 412 + Memo<Option<Vec<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>>, 413 + RenderError, 414 + > { 415 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 416 + let r = use_resource(move || { 417 + let fetcher = fetcher.clone(); 418 + async move { 419 + fetcher 420 + .fetch_notebooks_from_ufos() 421 + .await 422 + .ok() 423 + .map(|notebooks| notebooks.iter().map(|arc| (*arc).clone()).collect()) 424 + } 425 + }); 426 + Ok(use_memo(move || { 427 + r.read_unchecked().as_ref().and_then(|v| v.clone()) 428 + })) 429 + } 430 + 431 + /// Fetches notebook metadata with SSR support in fullstack mode 432 + #[cfg(feature = "fullstack-server")] 433 + pub fn use_notebook( 434 + ident: AtIdentifier<'static>, 435 + book_title: SmolStr, 436 + ) -> Result< 437 + Memo<Option<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>, 438 + RenderError, 439 + > { 440 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 441 + let ident = use_signal(|| ident); 442 + let book_title = use_signal(|| book_title); 443 + let res = use_server_future(move || { 444 + let fetcher = fetcher.clone(); 445 + async move { 446 + fetcher 447 + .get_notebook(ident(), book_title()) 448 + .await 449 + .ok() 450 + .flatten() 451 + .map(|arc| serde_json::to_value(arc.as_ref()).ok()) 452 + .flatten() 453 + } 454 + })?; 455 + Ok(use_memo(move || { 456 + if let Some(Some(value)) = &*res.read_unchecked() { 457 + jacquard::from_json_value::<( 458 + NotebookView, 459 + Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef>, 460 + )>(value.clone()) 461 + .ok() 462 + } else { 463 + None 464 + } 465 + })) 466 + } 467 + 468 + /// Fetches notebook metadata client-side only (no SSR) 469 + #[cfg(not(feature = "fullstack-server"))] 470 + pub fn use_notebook( 471 + ident: AtIdentifier<'static>, 472 + book_title: SmolStr, 473 + ) -> Result< 474 + Memo<Option<(NotebookView<'static>, Vec<weaver_api::com_atproto::repo::strong_ref::StrongRef<'static>>)>>, 475 + RenderError, 476 + > { 477 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 478 + let r = use_resource(use_reactive!(|(ident, book_title)| { 479 + let fetcher = fetcher.clone(); 480 + async move { 481 + fetcher 482 + .get_notebook(ident, book_title) 483 + .await 484 + .ok() 485 + .flatten() 486 + .map(|arc| (*arc).clone()) 487 + } 488 + })); 489 + Ok(use_memo(move || { 490 + r.read_unchecked().as_ref().and_then(|v| v.clone()) 491 + })) 492 + } 493 + 494 + /// Fetches notebook entries with SSR support in fullstack mode 495 + #[cfg(feature = "fullstack-server")] 496 + pub fn use_notebook_entries( 497 + ident: AtIdentifier<'static>, 498 + book_title: SmolStr, 499 + ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 500 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 501 + let ident = use_signal(|| ident); 502 + let book_title = use_signal(|| book_title); 503 + let res = use_server_future(move || { 504 + let fetcher = fetcher.clone(); 505 + async move { 506 + fetcher 507 + .list_notebook_entries(ident(), book_title()) 508 + .await 509 + .ok() 510 + .flatten() 511 + .map(|entries| { 512 + entries 513 + .iter() 514 + .map(|e| serde_json::to_value(e).ok()) 515 + .collect::<Option<Vec<_>>>() 516 + }) 517 + .flatten() 518 + } 519 + })?; 520 + Ok(use_memo(move || { 521 + if let Some(Some(values)) = &*res.read_unchecked() { 522 + values 523 + .iter() 524 + .map(|v| jacquard::from_json_value::<BookEntryView>(v.clone()).ok()) 525 + .collect::<Option<Vec<_>>>() 526 + } else { 527 + None 528 + } 529 + })) 530 + } 531 + 532 + /// Fetches notebook entries client-side only (no SSR) 533 + #[cfg(not(feature = "fullstack-server"))] 534 + pub fn use_notebook_entries( 535 + ident: AtIdentifier<'static>, 536 + book_title: SmolStr, 537 + ) -> Result<Memo<Option<Vec<BookEntryView<'static>>>>, RenderError> { 538 + let fetcher = use_context::<crate::fetch::CachedFetcher>(); 539 + let r = use_resource(use_reactive!(|(ident, book_title)| { 540 + let fetcher = fetcher.clone(); 541 + async move { 542 + fetcher 543 + .list_notebook_entries(ident, book_title) 544 + .await 545 + .ok() 546 + .flatten() 361 547 } 362 548 })); 363 549 Ok(use_memo(move || {
+3 -3
crates/weaver-app/src/env.rs
··· 1 1 // This file is automatically generated by build.rs 2 2 3 3 #[allow(unused)] 4 - pub const WEAVER_APP_ENV: &'static str = "prod"; 4 + pub const WEAVER_APP_ENV: &'static str = "dev"; 5 5 #[allow(unused)] 6 - pub const WEAVER_APP_HOST: &'static str = "https://alpha.weaver.sh"; 6 + pub const WEAVER_APP_HOST: &'static str = "http://localhost"; 7 7 #[allow(unused)] 8 - pub const WEAVER_APP_DOMAIN: &'static str = "https://alpha.weaver.sh"; 8 + pub const WEAVER_APP_DOMAIN: &'static str = ""; 9 9 #[allow(unused)] 10 10 pub const WEAVER_PORT: &'static str = "8080"; 11 11 #[allow(unused)]
+9 -15
crates/weaver-app/src/main.rs
··· 136 136 // Enable incremental rendering 137 137 .incremental( 138 138 dioxus::server::IncrementalRendererConfig::new() 139 - .static_dir( 140 - std::env::current_exe() 141 - .unwrap() 142 - .parent() 143 - .unwrap() 144 - .join("public"), 145 - ) 139 + .pre_render(true) 146 140 .clear_cache(false), 147 141 ) 148 142 .enable_out_of_order_streaming(), ··· 260 254 } 261 255 } 262 256 263 - #[server(endpoint = "static_routes", output = server_fn::codec::Json)] 264 - async fn static_routes() -> Result<Vec<String>, ServerFnError> { 265 - // The `Routable` trait has a `static_routes` method that returns all static routes in the enum 266 - Ok(Route::static_routes() 267 - .iter() 268 - .map(ToString::to_string) 269 - .collect()) 270 - } 257 + // #[server(endpoint = "static_routes", output = server_fn::codec::Json)] 258 + // async fn static_routes() -> Result<Vec<String>, ServerFnError> { 259 + // // The `Routable` trait has a `static_routes` method that returns all static routes in the enum 260 + // Ok(Route::static_routes() 261 + // .iter() 262 + // .map(ToString::to_string) 263 + // .collect()) 264 + // }
+22 -26
crates/weaver-app/src/views/home.rs
··· 1 - use crate::{Route, components::identity::NotebookCard, fetch}; 1 + use crate::{Route, components::identity::NotebookCard, data}; 2 2 use dioxus::prelude::*; 3 3 use jacquard::types::aturi::AtUri; 4 4 ··· 7 7 /// The Home page component that will be rendered when the current route is `[Route::Home]` 8 8 #[component] 9 9 pub fn Home() -> Element { 10 - let fetcher = use_context::<fetch::CachedFetcher>(); 11 - 12 - // Fetch notebooks from UFOS 13 - let notebooks = use_resource(move || { 14 - let fetcher = fetcher.clone(); 15 - async move { fetcher.fetch_notebooks_from_ufos().await } 16 - }); 10 + // Fetch notebooks from UFOS with SSR support 11 + let notebooks = data::use_notebooks_from_ufos().ok(); 17 12 let navigator = use_navigator(); 18 13 let mut uri_input = use_signal(|| String::new()); 19 14 ··· 49 44 } 50 45 } 51 46 div { class: "notebooks-list", 52 - match notebooks() { 53 - Some(Ok(notebook_list)) => rsx! { 54 - for notebook in notebook_list.iter() { 55 - { 56 - let view = &notebook.0; 57 - let entries = &notebook.1; 58 - rsx! { 59 - div { 60 - key: "{view.cid}", 61 - NotebookCard { 62 - notebook: view.clone(), 63 - entry_refs: entries.clone() 47 + if let Some(notebooks_memo) = &notebooks { 48 + match &*notebooks_memo.read_unchecked() { 49 + Some(notebook_list) => rsx! { 50 + for notebook in notebook_list.iter() { 51 + { 52 + let view = &notebook.0; 53 + let entries = &notebook.1; 54 + rsx! { 55 + div { 56 + key: "{view.cid}", 57 + NotebookCard { 58 + notebook: view.clone(), 59 + entry_refs: entries.clone() 60 + } 64 61 } 65 62 } 66 63 } 67 64 } 65 + }, 66 + None => rsx! { 67 + div { "Loading notebooks..." } 68 68 } 69 - }, 70 - Some(Err(_)) => rsx! { 71 - div { "Error loading notebooks" } 72 - }, 73 - None => rsx! { 74 - div { "Loading notebooks..." } 75 69 } 70 + } else { 71 + div { "Loading notebooks..." } 76 72 } 77 73 } 78 74 }
+30 -48
crates/weaver-app/src/views/notebook.rs
··· 1 1 use crate::{ 2 2 Route, 3 3 components::{EntryCard, NotebookCover, NotebookCss}, 4 - fetch, 4 + data, 5 5 }; 6 6 use dioxus::prelude::*; 7 7 use jacquard::{ ··· 28 28 ident: ReadSignal<AtIdentifier<'static>>, 29 29 book_title: ReadSignal<SmolStr>, 30 30 ) -> Element { 31 - let fetcher = use_context::<fetch::CachedFetcher>(); 32 - // Fetch full notebook to get author count 33 - let data_fetcher = fetcher.clone(); 34 - let notebook_data = use_resource(move || { 35 - let fetcher = data_fetcher.clone(); 36 - async move { 37 - fetcher 38 - .get_notebook(ident(), book_title()) 39 - .await 40 - .ok() 41 - .flatten() 42 - } 43 - }); 31 + // Fetch full notebook metadata with SSR support 32 + let notebook_data = data::use_notebook(ident(), book_title()).ok(); 44 33 45 - // Also fetch entries 46 - let entry_fetcher = fetcher.clone(); 47 - let entries_resource = use_resource(move || { 48 - let fetcher = entry_fetcher.clone(); 49 - async move { 50 - fetcher 51 - .list_notebook_entries(ident(), book_title()) 52 - .await 53 - .ok() 54 - .flatten() 55 - } 56 - }); 34 + // Fetch entries with SSR support 35 + let entries_resource = data::use_notebook_entries(ident(), book_title()).ok(); 57 36 58 37 rsx! { 59 38 document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } 60 39 61 - match (&*notebook_data.read_unchecked(), &*entries_resource.read_unchecked()) { 62 - (Some(Some(data)), Some(Some(entries))) => { 63 - let (notebook_view, _) = data.as_ref(); 64 - let author_count = notebook_view.authors.len(); 40 + if let (Some(notebook_memo), Some(entries_memo)) = (&notebook_data, &entries_resource) { 41 + match (&*notebook_memo.read_unchecked(), &*entries_memo.read_unchecked()) { 42 + (Some(data), Some(entries)) => { 43 + let (notebook_view, _) = data; 44 + let author_count = notebook_view.authors.len(); 65 45 66 - rsx! { 67 - div { class: "notebook-layout", 68 - aside { class: "notebook-sidebar", 69 - NotebookCover { 70 - notebook: notebook_view.clone(), 71 - title: book_title().to_string() 46 + rsx! { 47 + div { class: "notebook-layout", 48 + aside { class: "notebook-sidebar", 49 + NotebookCover { 50 + notebook: notebook_view.clone(), 51 + title: book_title().to_string() 52 + } 72 53 } 73 - } 74 54 75 - main { class: "notebook-main", 76 - div { class: "entries-list", 77 - for entry in entries { 78 - EntryCard { 79 - entry: entry.clone(), 80 - book_title: book_title(), 81 - author_count 55 + main { class: "notebook-main", 56 + div { class: "entries-list", 57 + for entry in entries { 58 + EntryCard { 59 + entry: entry.clone(), 60 + book_title: book_title(), 61 + author_count 62 + } 82 63 } 83 64 } 84 65 } 85 66 } 86 67 } 87 - } 88 - }, 89 - (Some(None), _) | (_, Some(None)) => rsx! { div { class: "error", "Notebook or entries not found" } }, 90 - _ => rsx! { div { class: "loading", "Loading..." } } 68 + }, 69 + _ => rsx! { div { class: "loading", "Loading..." } } 70 + } 71 + } else { 72 + div { class: "loading", "Loading..." } 91 73 } 92 74 } 93 75 }