+ { "Meet the definitely real people behind your favourite Yew content" }
+
+
+
+
+
+ { "It wouldn't be fair " }
+ { "(or possible :P)" }
+ {" to list each and every author in alphabetical order."}
+
+ { "So instead we chose to put more focus on the individuals by introducing you to two people at a time" }
+
+
+
+ for seed in seeds.iter().copied() {
+
+
+
{"Loading..."}
}}>
+
+
+
+
+ }
+
+
+
+
+ }
+}
+
+fn switch(routes: function_router::Route) -> Html {
+ use function_router::Route;
+
+ match routes {
+ Route::Post { id } => html! {
+ {"Loading post..."}
") && !html.contains("Loading post...")
- },
- 5000,
- "post page content after SSR hydration",
- )
- .await;
-
- let link_fetches = resource_request_count(LINK_ENDPOINT);
- let title = get_title_text();
-
- assert_eq!(
- link_fetches, 0,
- "direct SSR visit to /posts/0 should not trigger any fetch to {LINK_ENDPOINT}"
- );
- let title = title.expect("h1.title should be present on the SSR post page");
- assert!(!title.is_empty(), "SSR post title should not be empty");
-
- // -- Part 2: Navigate to /posts within the same app, then to /posts/0 --
-
- yew::scheduler::flush().await;
-
- clear_resource_timings();
-
- let posts_link: web_sys::HtmlElement = output_element()
- .query_selector("a.navbar-item[href='/posts']")
- .unwrap()
- .expect("Posts navbar link should exist")
- .dyn_into()
- .unwrap();
- posts_link.click();
- yew::scheduler::flush().await;
-
- wait_for(
- || {
- document()
- .query_selector("a.title.is-block")
- .ok()
- .flatten()
- .is_some()
- && get_title_text().as_deref() == Some("Posts")
- },
- 15000,
- "posts list after client-side navigation to /posts",
- )
- .await;
-
- clear_resource_timings();
-
- wait_for(
- || {
- output_element()
- .query_selector("a.title.is-block[href='/posts/0']")
- .ok()
- .flatten()
- .is_some()
- },
- 15000,
- "post 0 card link on posts list",
- )
- .await;
-
- let post_link: web_sys::HtmlElement = output_element()
- .query_selector("a.title.is-block[href='/posts/0']")
- .unwrap()
- .unwrap()
- .dyn_into()
- .unwrap();
- post_link.click();
- yew::scheduler::flush().await;
-
- wait_for(
- || {
- document()
- .query_selector("h2.subtitle")
- .ok()
- .flatten()
- .map(|el| el.text_content().unwrap_or_default())
- .is_some_and(|text| text.starts_with("by "))
- },
- 15000,
- "post page content after client-side navigation to /posts/0",
- )
- .await;
-
- // -- Part 3: Verify fetch happened and content matches SSR --
-
- let nav_link_fetches = resource_request_count(LINK_ENDPOINT);
- let nav_title = get_title_text();
- let nav_body = post_body_text();
-
- assert!(
- nav_link_fetches >= 1,
- "client-side navigation to /posts/0 should trigger at least one fetch to {LINK_ENDPOINT}, \
- got {nav_link_fetches}"
- );
-
- let nav_title = nav_title.expect("h1.title should be present after client-side navigation");
- assert_eq!(
- ssr_title, nav_title,
- "post title should match between SSR and client-side navigation"
- );
- assert_eq!(
- ssr_body, nav_body,
- "post body should match between SSR and client-side navigation"
- );
-
- app.destroy();
- yew::scheduler::flush().await;
-}
-
-#[wasm_bindgen_test]
-async fn hydrate_home() {
- setup_ssr_page(SERVER_BASE, "/").await;
- let app = make_renderer().hydrate();
-
- wait_for(
- || output_element().inner_html().contains("Welcome"),
- 5000,
- "home page content after hydration",
- )
- .await;
-
- app.destroy();
- yew::scheduler::flush().await;
-}
diff --git a/packages/yew-link/Cargo.toml b/packages/yew-link/Cargo.toml
index a9a99149b32..6b801a6c854 100644
--- a/packages/yew-link/Cargo.toml
+++ b/packages/yew-link/Cargo.toml
@@ -20,12 +20,14 @@ wasm-bindgen-futures = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
axum = { workspace = true, optional = true }
+actix-web = { workspace = true, optional = true }
[features]
default = []
ssr = ["yew/ssr"]
hydration = ["yew/hydration"]
axum = ["dep:axum"]
+actix = ["dep:actix-web"]
[lints]
workspace = true
diff --git a/packages/yew-link/src/lib.rs b/packages/yew-link/src/lib.rs
index c3d412fd881..f889f5291c9 100644
--- a/packages/yew-link/src/lib.rs
+++ b/packages/yew-link/src/lib.rs
@@ -289,7 +289,7 @@ impl Eq for CacheKey {}
fn eq_inputs(a: &dyn Any, b: &dyn Any) -> bool {
a.downcast_ref::()
.zip(b.downcast_ref::())
- .map_or(false, |(a, b)| a == b)
+ .is_some_and(|(a, b)| a == b)
}
#[cfg(target_arch = "wasm32")]
@@ -511,6 +511,7 @@ pub fn use_linked_state(input: T::Input) -> SuspensionResult().expect("use_linked_state requires a LinkProvider");
+ #[cfg(any(feature = "ssr", target_arch = "wasm32"))]
type Prepared = Result>;
#[cfg(feature = "ssr")]
@@ -540,9 +541,7 @@ pub fn use_linked_state(input: T::Input) -> SuspensionResult, T::Input>(
- input,
- )?;
+ let _ = input;
Ok(LinkedStateHandle {
result: Err(LinkError::Internal(
"yew-link requires the `ssr` feature (server) or a wasm32 target (client)".into(),
@@ -678,51 +677,8 @@ pub fn use_linked_state(input: T::Input) -> SuspensionResult(|id| async move { db::get_post(id).await })
- /// );
- ///
- /// let app = axum::Router::new()
- /// .route("/api/link", axum::routing::post(linked_state_handler))
- /// .with_state(resolver);
- /// ```
- pub async fn linked_state_handler(
- State(resolver): State>,
- Json(req): Json,
- ) -> impl IntoResponse {
- match resolver.resolve_request(&req).await {
- Ok(val) => (
- StatusCode::OK,
- Json(LinkResponse {
- ok: Some(val),
- error: None,
- }),
- ),
- Err(err_val) => (
- StatusCode::UNPROCESSABLE_ENTITY,
- Json(LinkResponse {
- ok: None,
- error: Some(err_val),
- }),
- ),
- }
- }
-}
+#[cfg(all(not(target_arch = "wasm32"), any(feature = "axum", feature = "actix")))]
+mod services;
-#[cfg(all(feature = "axum", not(target_arch = "wasm32")))]
-pub use service::linked_state_handler;
+#[cfg(all(not(target_arch = "wasm32"), any(feature = "axum", feature = "actix")))]
+pub use services::*;
diff --git a/packages/yew-link/src/services.rs b/packages/yew-link/src/services.rs
new file mode 100644
index 00000000000..a06f15706ee
--- /dev/null
+++ b/packages/yew-link/src/services.rs
@@ -0,0 +1,134 @@
+#[cfg(feature = "axum")]
+pub mod axum {
+ use std::sync::Arc;
+
+ use axum::Json;
+ use axum::extract::State;
+ use axum::http::StatusCode;
+ use axum::response::IntoResponse;
+
+ use crate::{LinkRequest, LinkResponse, Resolver};
+
+ /// Axum handler that resolves [`LinkRequest`]s.
+ ///
+ /// ```
+ /// use std::sync::Arc;
+ ///
+ /// use serde::{Deserialize, Serialize};
+ /// use yew_link::axum::linked_state_handler;
+ /// use yew_link::{LinkedState, Never, Resolver};
+ ///
+ /// #[derive(Clone, Debug, Serialize, Deserialize)]
+ /// struct Post {
+ /// title: String,
+ /// }
+ ///
+ /// impl LinkedState for Post {
+ /// type Error = Never;
+ /// type Input = u32;
+ ///
+ /// const TYPE_KEY: &'static str = "Post";
+ /// }
+ ///
+ /// async fn get_post(_id: u32) -> Result {
+ /// Ok(Post {
+ /// title: String::new(),
+ /// })
+ /// }
+ ///
+ /// let resolver = Arc::new(Resolver::new().register::(|id| get_post(id)));
+ ///
+ /// let app: axum::Router = axum::Router::new().route(
+ /// "/api/link",
+ /// axum::routing::post(linked_state_handler).with_state(resolver),
+ /// );
+ /// # let _ = app;
+ /// ```
+ pub async fn linked_state_handler(
+ State(resolver): State>,
+ Json(req): Json,
+ ) -> impl IntoResponse {
+ match resolver.resolve_request(&req).await {
+ Ok(val) => (
+ StatusCode::OK,
+ Json(LinkResponse {
+ ok: Some(val),
+ error: None,
+ }),
+ ),
+ Err(err_val) => (
+ StatusCode::UNPROCESSABLE_ENTITY,
+ Json(LinkResponse {
+ ok: None,
+ error: Some(err_val),
+ }),
+ ),
+ }
+ }
+}
+
+#[cfg(feature = "actix")]
+pub mod actix {
+ use actix_web::HttpResponse;
+ use actix_web::web::{Data, Json};
+
+ use crate::{LinkRequest, LinkResponse, Resolver};
+
+ /// Actix handler that resolves [`LinkRequest`]s.
+ ///
+ /// ```no_run
+ /// use actix_web::web::{Data, post};
+ /// use actix_web::{App, HttpServer};
+ /// use serde::{Deserialize, Serialize};
+ /// use yew_link::actix::linked_state_handler;
+ /// use yew_link::{LinkedState, Never, Resolver};
+ ///
+ /// #[derive(Clone, Debug, Serialize, Deserialize)]
+ /// struct Post {
+ /// title: String,
+ /// }
+ ///
+ /// impl LinkedState for Post {
+ /// type Error = Never;
+ /// type Input = u32;
+ ///
+ /// const TYPE_KEY: &'static str = "Post";
+ /// }
+ ///
+ /// async fn get_post(_id: u32) -> Result {
+ /// Ok(Post {
+ /// title: String::new(),
+ /// })
+ /// }
+ ///
+ /// #[actix_web::main]
+ /// async fn main() -> std::io::Result<()> {
+ /// let resolver = Data::new(Resolver::new().register::(|id| get_post(id)));
+ ///
+ /// HttpServer::new(move || {
+ /// App::new()
+ /// .app_data(resolver.clone())
+ /// .route("/api/link", post().to(linked_state_handler))
+ /// })
+ /// .bind(("0.0.0.0", 8080))?
+ /// .run()
+ /// .await
+ /// }
+ /// ```
+ pub async fn linked_state_handler(
+ resolver: Data,
+ Json(req): Json,
+ ) -> HttpResponse {
+ match resolver.resolve_request(&req).await {
+ Ok(val) => HttpResponse::Ok().json(LinkResponse {
+ ok: Some(val),
+ error: None,
+ }),
+
+ Err(err_val) => HttpResponse::UnprocessableEntity().json(LinkResponse {
+ ok: None,
+ error: Some(err_val),
+ }),
+ }
+ }
+}
diff --git a/tools/ssr-e2e-harness/Cargo.toml b/tools/ssr-e2e-harness/Cargo.toml
index 5318504a890..3c1ec539385 100644
--- a/tools/ssr-e2e-harness/Cargo.toml
+++ b/tools/ssr-e2e-harness/Cargo.toml
@@ -8,3 +8,4 @@ rust-version.workspace = true
gloo = { workspace = true, features = ["futures"] }
web-sys = { workspace = true, features = ["Performance", "PerformanceEntry"] }
wasm-bindgen = { workspace = true }
+yew = { path = "../../packages/yew", features = ["csr", "hydration", "test"] }
diff --git a/tools/ssr-e2e-harness/src/lib.rs b/tools/ssr-e2e-harness/src/lib.rs
index 59f4dda5146..b36dd9a0fef 100644
--- a/tools/ssr-e2e-harness/src/lib.rs
+++ b/tools/ssr-e2e-harness/src/lib.rs
@@ -2,6 +2,8 @@ use std::time::Duration;
use gloo::utils::document;
use wasm_bindgen::prelude::*;
+use yew::Renderer;
+use yew::html::BaseComponent;
/// Returns the `
` element used by wasm-bindgen-test as the
/// test output container.
@@ -82,3 +84,202 @@ pub fn resource_request_count(needle: &str) -> u32 {
pub fn clear_resource_timings() {
performance().clear_resource_timings();
}
+
+/// Returns the text content of the first `
` in the document.
+pub fn get_title_text() -> Option {
+ document()
+ .query_selector("h1.title")
+ .ok()
+ .flatten()
+ .map(|el| el.text_content().unwrap_or_default())
+}
+
+/// Returns the text content of the first `.section.container` inside the
+/// test output element.
+pub fn post_body_text() -> String {
+ output_element()
+ .query_selector(".section.container")
+ .ok()
+ .flatten()
+ .map(|el| el.text_content().unwrap_or_default())
+ .unwrap_or_default()
+}
+
+/// Parses `html` into a detached container element and returns the text
+/// content of the first element matching `selector`, if any.
+pub fn extract_text_from_html(html: &str, selector: &str) -> Option {
+ let container = document().create_element("div").unwrap();
+ container.set_inner_html(html);
+ container
+ .query_selector(selector)
+ .ok()
+ .flatten()
+ .and_then(|el| el.text_content())
+}
+
+/// Shared e2e scenario used by the yew-link SSR router examples.
+///
+/// Phases:
+/// 1. Directly visit `/posts/0` by fetching its SSR HTML, hydrate, and assert that hydration did
+/// not trigger any fetch to `link_endpoint`.
+/// 2. Click the "Posts" navbar link, then the post 0 card, and wait for the post page to render.
+/// 3. Assert at least one fetch to `link_endpoint` happened during the client-side navigation and
+/// that the rendered title/body match the original SSR HTML.
+///
+/// `make_renderer` is a closure that builds a `Renderer` rooted at
+/// [`output_element()`]. It is invoked after the SSR HTML has been injected
+/// so that hydration picks it up.
+pub async fn assert_ssr_hydration_and_client_navigation(
+ make_renderer: impl FnOnce() -> Renderer,
+ server_base: &str,
+ link_endpoint: &str,
+) where
+ COMP: BaseComponent,
+{
+ // -- Part 1: Direct SSR visit to /posts/0 triggers no fetch to link_endpoint --
+
+ let ssr_html = fetch_ssr_html(server_base, "/posts/0").await;
+ let ssr_title = extract_text_from_html(&ssr_html, "h1.title")
+ .expect("SSR HTML for /posts/0 should contain h1.title");
+ let ssr_body = extract_text_from_html(&ssr_html, ".section.container").unwrap_or_default();
+
+ clear_resource_timings();
+
+ output_element().set_inner_html(&ssr_html);
+ push_route("/posts/0");
+ let app = make_renderer().hydrate();
+
+ wait_for(
+ || {
+ let html = output_element().inner_html();
+ html.contains("
") && !html.contains("Loading post...")
+ },
+ 5000,
+ "post page content after SSR hydration",
+ )
+ .await;
+
+ let link_fetches = resource_request_count(link_endpoint);
+ let title = get_title_text();
+
+ assert_eq!(
+ link_fetches, 0,
+ "direct SSR visit to /posts/0 should not trigger any fetch to {link_endpoint}"
+ );
+ let title = title.expect("h1.title should be present on the SSR post page");
+ assert!(!title.is_empty(), "SSR post title should not be empty");
+
+ // -- Part 2: Navigate to /posts within the same app, then to /posts/0 --
+
+ yew::scheduler::flush().await;
+
+ clear_resource_timings();
+
+ let posts_link: web_sys::HtmlElement = output_element()
+ .query_selector("a.navbar-item[href='/posts']")
+ .unwrap()
+ .expect("Posts navbar link should exist")
+ .dyn_into()
+ .unwrap();
+ posts_link.click();
+ yew::scheduler::flush().await;
+
+ wait_for(
+ || {
+ document()
+ .query_selector("a.title.is-block")
+ .ok()
+ .flatten()
+ .is_some()
+ && get_title_text().as_deref() == Some("Posts")
+ },
+ 15000,
+ "posts list after client-side navigation to /posts",
+ )
+ .await;
+
+ clear_resource_timings();
+
+ wait_for(
+ || {
+ output_element()
+ .query_selector("a.title.is-block[href='/posts/0']")
+ .ok()
+ .flatten()
+ .is_some()
+ },
+ 15000,
+ "post 0 card link on posts list",
+ )
+ .await;
+
+ let post_link: web_sys::HtmlElement = output_element()
+ .query_selector("a.title.is-block[href='/posts/0']")
+ .unwrap()
+ .unwrap()
+ .dyn_into()
+ .unwrap();
+ post_link.click();
+ yew::scheduler::flush().await;
+
+ wait_for(
+ || {
+ document()
+ .query_selector("h2.subtitle")
+ .ok()
+ .flatten()
+ .map(|el| el.text_content().unwrap_or_default())
+ .is_some_and(|text| text.starts_with("by "))
+ },
+ 15000,
+ "post page content after client-side navigation to /posts/0",
+ )
+ .await;
+
+ // -- Part 3: Verify fetch happened and content matches SSR --
+
+ let nav_link_fetches = resource_request_count(link_endpoint);
+ let nav_title = get_title_text();
+ let nav_body = post_body_text();
+
+ assert!(
+ nav_link_fetches >= 1,
+ "client-side navigation to /posts/0 should trigger at least one fetch to {link_endpoint}, \
+ got {nav_link_fetches}"
+ );
+
+ let nav_title = nav_title.expect("h1.title should be present after client-side navigation");
+ assert_eq!(
+ ssr_title, nav_title,
+ "post title should match between SSR and client-side navigation"
+ );
+ assert_eq!(
+ ssr_body, nav_body,
+ "post body should match between SSR and client-side navigation"
+ );
+
+ app.destroy();
+ yew::scheduler::flush().await;
+}
+
+/// Shared e2e scenario that asserts hydrating the home page ("/") produces
+/// HTML containing the word "Welcome".
+pub async fn assert_hydrate_home(
+ make_renderer: impl FnOnce() -> Renderer,
+ server_base: &str,
+) where
+ COMP: BaseComponent,
+{
+ setup_ssr_page(server_base, "/").await;
+ let app = make_renderer().hydrate();
+
+ wait_for(
+ || output_element().inner_html().contains("Welcome"),
+ 5000,
+ "home page content after hydration",
+ )
+ .await;
+
+ app.destroy();
+ yew::scheduler::flush().await;
+}
diff --git a/tools/website-test/Cargo.toml b/tools/website-test/Cargo.toml
index f8e6deb75d3..193a921bec9 100644
--- a/tools/website-test/Cargo.toml
+++ b/tools/website-test/Cargo.toml
@@ -18,11 +18,17 @@ serde = { workspace = true, features = ["derive"] }
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
weblog = "0.3.0"
-yew = { path = "../../packages/yew/", features = ["ssr", "csr", "serde"] }
+yew = { path = "../../packages/yew/", features = ["ssr", "csr", "hydration", "serde"] }
yew-autoprops = "0.5.0"
yew-router = { path = "../../packages/yew-router/" }
tokio = { workspace = true, features = ["rt", "macros"] }
+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+actix-web = { workspace = true }
+axum = { workspace = true }
+serde_json = { workspace = true }
+yew-link = { path = "../../packages/yew-link/", features = ["actix", "axum"] }
+
[dev-dependencies.web-sys]
workspace = true
features = [
diff --git a/tools/website-test/build.rs b/tools/website-test/build.rs
index b3b4b462306..217fe1e7714 100644
--- a/tools/website-test/build.rs
+++ b/tools/website-test/build.rs
@@ -183,7 +183,7 @@ impl Level {
if should_combine_code_blocks(file)? {
let res = combined_code_blocks(file)?;
self.write_space(dst, level);
- writeln!(dst, "/// ```rust, no_run")?;
+ writeln!(dst, "/// ```no_run")?;
for line in res.lines() {
self.write_space(dst, level);
writeln!(dst, "/// {line}")?;
diff --git a/website/docs/advanced-topics/server-side-rendering.mdx b/website/docs/advanced-topics/server-side-rendering.mdx
index ea544b0d34a..0189f1e2d4a 100644
--- a/website/docs/advanced-topics/server-side-rendering.mdx
+++ b/website/docs/advanced-topics/server-side-rendering.mdx
@@ -148,7 +148,10 @@ The `yew-link` crate provides a higher-level abstraction that unifies all three
3. **Wrap** your app in ``.
4. **Call** `use_linked_state::(input)` in any component.
-```rust ,ignore
+```rust ,ignore-wasm32
+# use serde::{Serialize, Deserialize};
+# pub struct DbPool;
+# impl DbPool { async fn get_post(&self, _id: u32) -> Post { unreachable!() } }
use yew_link::{linked_state, LinkedState};
#[derive(Clone, Serialize, Deserialize)]
@@ -171,7 +174,20 @@ The macro generates the `LinkedState` and (server-only) `LinkedStateResolve` tra
If `resolve` can fail, declare `type Error`:
-```rust ,ignore
+```rust ,ignore-wasm32
+# use serde::{Serialize, Deserialize};
+# use yew_link::{linked_state, LinkedState};
+# #[derive(Clone, Debug, Serialize, Deserialize)]
+# pub struct ApiError;
+# impl std::fmt::Display for ApiError {
+# fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ApiError") }
+# }
+# pub struct DbPool;
+# impl DbPool {
+# async fn get_post(&self, _id: u32) -> Result { unreachable!() }
+# }
+# #[derive(Clone, Serialize, Deserialize)]
+# pub struct Post { pub title: String, pub body: String }
#[linked_state]
impl LinkedState for Post {
type Context = DbPool;
@@ -199,22 +215,147 @@ Multiple components requesting the same `(T, Input)` concurrently share a single
#### Server setup
-```rust ,ignore
-use yew_link::{Resolver, linked_state_handler};
+```rust ,ignore-wasm32
+# use std::sync::Arc;
+# use serde::{Serialize, Deserialize};
+# use yew_link::{linked_state, LinkedState};
+# #[derive(Clone)]
+# pub struct DbPool;
+# impl DbPool {
+# async fn get_post(&self, _id: u32) -> Post { unreachable!() }
+# }
+# #[derive(Clone, Serialize, Deserialize)]
+# pub struct Post { pub title: String, pub body: String }
+# #[linked_state]
+# impl LinkedState for Post {
+# type Context = DbPool;
+# type Input = u32;
+# async fn resolve(ctx: &DbPool, id: &u32) -> Self {
+# ctx.get_post(*id).await
+# }
+# }
+# fn main() {
+# let db_pool = DbPool;
+use yew_link::{Resolver, axum::linked_state_handler};
let resolver = Arc::new(
Resolver::new()
.register_linked::(db_pool.clone())
);
-let app = axum::Router::new()
- .route("/api/link", axum::routing::post(linked_state_handler))
- .with_state(resolver);
+let app: axum::Router = axum::Router::new().route(
+ "/api/link",
+ axum::routing::post(linked_state_handler).with_state(resolver),
+);
+# let _ = app;
+# }
+```
+
+The example above uses the `axum` feature. `yew-link` also ships an `actix`
+feature that exposes the same handler under `yew_link::actix::linked_state_handler`:
+
+```rust ,no_run,ignore-wasm32
+# use serde::{Serialize, Deserialize};
+# use yew_link::{linked_state, LinkedState};
+# #[derive(Clone)]
+# pub struct DbPool;
+# impl DbPool {
+# async fn get_post(&self, _id: u32) -> Post { unreachable!() }
+# }
+# #[derive(Clone, Serialize, Deserialize)]
+# pub struct Post { pub title: String, pub body: String }
+# #[linked_state]
+# impl LinkedState for Post {
+# type Context = DbPool;
+# type Input = u32;
+# async fn resolve(ctx: &DbPool, id: &u32) -> Self {
+# ctx.get_post(*id).await
+# }
+# }
+# async fn run() -> std::io::Result<()> {
+# let db_pool = DbPool;
+use actix_web::{App, HttpServer, web::{Data, post}};
+use yew_link::{Resolver, actix::linked_state_handler};
+
+let resolver = Data::new(
+ Resolver::new()
+ .register_linked::(db_pool.clone())
+);
+
+HttpServer::new(move || {
+ App::new()
+ .app_data(resolver.clone())
+ .route("/api/link", post().to(linked_state_handler))
+})
+.bind(("0.0.0.0", 8080))?
+.run()
+.await
+# }
+# fn main() {}
```
+
+Why `actix-web` is pinned to 4.12, and how to implement a handler for other server frameworks
+
+The `actix` feature is pinned to `actix-web 4.12.x` because Yew's MSRV is
+`1.85`, while `actix-web 4.13` and newer require `rustc 1.88`. This pin lives
+inside `yew-link` only so the rest of the workspace stays buildable on the
+supported toolchain; it does not constrain what version of `actix-web` you
+use elsewhere in your own application.
+
+If you need a newer `actix-web` (or any other web framework) and do not want
+to enable the bundled feature, the handler is small enough to inline
+yourself. The only public surface you need is `Resolver::resolve_request`
+and the wire-format type `LinkRequest`:
+
+```rust ,ignore-wasm32
+use actix_web::HttpResponse;
+use actix_web::web::{Data, Json};
+use serde_json::json;
+use yew_link::{LinkRequest, Resolver};
+
+pub async fn linked_state_handler(
+ resolver: Data,
+ Json(req): Json,
+) -> HttpResponse {
+ match resolver.resolve_request(&req).await {
+ Ok(val) => HttpResponse::Ok().json(json!({ "ok": val })),
+ Err(err) => HttpResponse::UnprocessableEntity().json(json!({ "error": err })),
+ }
+}
+```
+
+The same shape works for `axum`, `warp`, `rocket`, or any framework that can
+deserialize JSON into `LinkRequest`, call an async function, and serialize a
+JSON response. Construct the response wire format with `serde_json::json!`
+to keep yew-link's internal `LinkResponse` type out of your dependency
+surface.
+
+
+
#### Component usage
-```rust ,ignore
+```rust ,ignore-wasm32
+# use serde::{Serialize, Deserialize};
+# use yew::prelude::*;
+# use yew_link::{linked_state, LinkedState};
+# pub struct DbPool;
+# impl DbPool {
+# async fn get_post(&self, _id: u32) -> Post { unreachable!() }
+# }
+# #[derive(Clone, Serialize, Deserialize)]
+# pub struct Post { pub title: String, pub body: String }
+# #[linked_state]
+# impl LinkedState for Post {
+# type Context = DbPool;
+# type Input = u32;
+# async fn resolve(ctx: &DbPool, id: &u32) -> Self {
+# ctx.get_post(*id).await
+# }
+# }
+# #[derive(Properties, PartialEq)]
+# pub struct PostPageProps { pub id: u32 }
+#[allow(unused_imports)]
use yew_link::{use_linked_state, LinkProvider};
#[component]
@@ -226,7 +367,7 @@ fn PostPage(props: &PostPageProps) -> HtmlResult {
During SSR the state is resolved locally via the `Resolver` and embedded in the HTML through `use_prepared_state`. On hydration the client reads the embedded state with zero network requests. On subsequent client-side navigations the hook fetches from the `LinkProvider`'s endpoint URL automatically.
-See the [`ssr_router` example](https://github.com/yewstack/yew/tree/master/examples/ssr_router) for a full working demo.
+See the [`axum_ssr_router`](https://github.com/yewstack/yew/tree/master/examples/axum_ssr_router) and [`actix_ssr_router`](https://github.com/yewstack/yew/tree/master/examples/actix_ssr_router) examples for full working demos.
## Rendering `` Tags
@@ -238,7 +379,7 @@ load.
it has no access to ``. Head tags must therefore be generated **on the server, outside of
Yew**, and spliced into the HTML template before it is sent to the client.
-The [`ssr_router` example](https://github.com/yewstack/yew/blob/master/examples/ssr_router/src/bin/ssr_router_server.rs) demonstrates this pattern: the server recognizes the
+The [`axum_ssr_router` example](https://github.com/yewstack/yew/blob/master/examples/axum_ssr_router/src/bin/ssr_router_server.rs) demonstrates this pattern: the server recognizes the
route from the request URL, generates the appropriate `` and ``
tags, and injects them into the Trunk-generated `index.html` before
``.
@@ -295,7 +436,7 @@ until `rendered()` method is called.
## Example
-```rust ,ignore
+```rust ,no_run
use yew::prelude::*;
use yew::Renderer;
@@ -314,7 +455,8 @@ fn main() {
```
Example: [simple_ssr](https://github.com/yewstack/yew/tree/master/examples/simple_ssr)
-Example: [ssr_router](https://github.com/yewstack/yew/tree/master/examples/ssr_router)
+Example: [axum_ssr_router](https://github.com/yewstack/yew/tree/master/examples/axum_ssr_router)
+Example: [actix_ssr_router](https://github.com/yewstack/yew/tree/master/examples/actix_ssr_router)
## Single thread mode
diff --git a/website/docs/concepts/html/fragments.mdx b/website/docs/concepts/html/fragments.mdx
index 21a6ca3388a..2612a041f8e 100644
--- a/website/docs/concepts/html/fragments.mdx
+++ b/website/docs/concepts/html/fragments.mdx
@@ -27,7 +27,7 @@ html! {
-```rust, compile_fail
+```rust ,compile_fail
use yew::prelude::*;
// error: only one root html element allowed
diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/current/advanced-topics/server-side-rendering.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/current/advanced-topics/server-side-rendering.mdx
index 6496ca5962c..0e78401625c 100644
--- a/website/i18n/ja/docusaurus-plugin-content-docs/current/advanced-topics/server-side-rendering.mdx
+++ b/website/i18n/ja/docusaurus-plugin-content-docs/current/advanced-topics/server-side-rendering.mdx
@@ -84,13 +84,242 @@ Yewは、`` を使用してこの問題を解決する異なるア
この方法により、開発者はサーバーサイドレンダリングに対応したクライアント非依存のアプリケーションを簡単に構築し、データ取得を行うことができます。
+### 低レベルフック
+
+Yew は、サーバー側で計算した状態をクライアントへ運ぶための低レベルフックを 2 つ提供しています:
+
+- **`use_prepared_state!`** は SSR 中に(必要であれば async な)クロージャを実行し、結果をシリアライズして、ハイドレーション中にクライアントへ届けます。コンポーネントが最初のレンダリング時に必要とするデータの取得に向いています。
+- **`use_transitive_state!`** も同様ですが、クロージャはコンポーネントの SSR 出力が生成された _後_ に実行されます。キャッシュや集約状態の収集に向いています。
+
+どちらも内部で `bincode` と `base64` を使い、HTML に `