Skip to content

Commit 16cb69a

Browse files
feat: add SSR head rendering to ssr_router example and document the pattern (#4011)
1 parent d4480da commit 16cb69a

File tree

14 files changed

+206
-56
lines changed

14 files changed

+206
-56
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/function_router/src/app.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ use crate::pages::page_not_found::PageNotFound;
1212
use crate::pages::post::Post;
1313
use crate::pages::post_list::PostList;
1414

15+
pub fn route_meta(route: &Route) -> (&'static str, &'static str) {
16+
match route {
17+
Route::Home => ("Home", "The best yew content on the web"),
18+
Route::Posts => ("Posts", "Browse all posts"),
19+
Route::Post { .. } => ("Post", "Read a post"),
20+
Route::Authors => ("Authors", "Meet the authors"),
21+
Route::Author { .. } => ("Author", "Author profile"),
22+
Route::NotFound => ("Not Found", "Page not found"),
23+
}
24+
}
25+
1526
#[derive(Routable, PartialEq, Eq, Clone, Debug)]
1627
pub enum Route {
1728
#[at("/posts/:id")]
@@ -29,7 +40,7 @@ pub enum Route {
2940
NotFound,
3041
}
3142

32-
#[function_component]
43+
#[component]
3344
pub fn App() -> Html {
3445
html! {
3546
<BrowserRouter>
@@ -56,7 +67,7 @@ pub struct ServerAppProps {
5667
pub queries: HashMap<String, String>,
5768
}
5869

59-
#[function_component]
70+
#[component]
6071
pub fn ServerApp(props: &ServerAppProps) -> Html {
6172
let history = AnyHistory::from(MemoryHistory::new());
6273
history

examples/ssr_router/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ wasm-bindgen-futures.workspace = true
2525
wasm-logger.workspace = true
2626

2727
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
28+
yew-router = { path = "../../packages/yew-router" }
2829
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "fs"] }
2930
axum = "0.8"
3031
tower = { version = "0.5", features = ["make"] }

examples/ssr_router/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<link data-trunk rel="rust" data-bin="ssr_router_hydrate" data-cargo-features="hydration" />
77

8-
<title>Yew SSR Router</title>
8+
<title>Yew SSR Router</title>
99
<link
1010
rel="stylesheet"
1111
href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"

examples/ssr_router/src/bin/ssr_router_server.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use axum::response::IntoResponse;
1212
use axum::routing::get;
1313
use axum::Router;
1414
use clap::Parser;
15-
use function_router::{ServerApp, ServerAppProps};
15+
use function_router::{route_meta, Route, ServerApp, ServerAppProps};
1616
use futures::stream::{self, StreamExt};
1717
use hyper::body::Incoming;
1818
use hyper_util::rt::TokioIo;
@@ -21,6 +21,7 @@ use tokio::net::TcpListener;
2121
use tower::Service;
2222
use tower_http::services::ServeDir;
2323
use yew::platform::Runtime;
24+
use yew_router::prelude::Routable;
2425

2526
// We use jemalloc as it produces better performance.
2627
#[cfg(unix)]
@@ -35,20 +36,32 @@ struct Opt {
3536
dir: PathBuf,
3637
}
3738

39+
fn head_tags_for(path: &str) -> String {
40+
let route = Route::recognize(path).unwrap_or(Route::NotFound);
41+
let (title, description) = route_meta(&route);
42+
format!(
43+
"<title>{title} | Yew SSR Router</title><meta name=\"description\" \
44+
content=\"{description}\" />"
45+
)
46+
}
47+
3848
async fn render(
3949
url: Uri,
4050
Query(queries): Query<HashMap<String, String>>,
4151
State((index_html_before, index_html_after)): State<(String, String)>,
4252
) -> impl IntoResponse {
43-
let url = url.path().to_owned();
53+
let path = url.path().to_owned();
54+
55+
// Inject route-specific <head> tags before </head>, outside of Yew rendering.
56+
let before = index_html_before.replace("</head>", &format!("{}</head>", head_tags_for(&path)));
4457

4558
let renderer = yew::ServerRenderer::<ServerApp>::with_props(move || ServerAppProps {
46-
url: url.into(),
59+
url: path.into(),
4760
queries,
4861
});
4962

5063
Body::from_stream(
51-
stream::once(async move { index_html_before })
64+
stream::once(async move { before })
5265
.chain(renderer.render_stream())
5366
.chain(stream::once(async move { index_html_after }))
5467
.map(Result::<_, Infallible>::Ok),

website/docs/advanced-topics/portals.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ once targeting the element inside the shadow root and once targeting the host el
5959
in mind that **open** shadow roots work fine. If this impacts you, feel free to open a bug report
6060
about it.
6161

62+
## SSR limitation
63+
64+
Portals are **not rendered during server-side rendering**. They require a live
65+
DOM host element (`web_sys::Element`) which is unavailable on the server.
66+
If you need to render content into `<head>` during SSR, see the
67+
[head rendering section](./server-side-rendering#rendering-head-tags)
68+
in the SSR documentation.
69+
6270
## Further reading
6371

6472
- [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals)

website/docs/advanced-topics/server-side-rendering.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ suspended.
128128
With this approach, developers can build a client-agnostic, SSR-ready
129129
application with data fetching with very little effort.
130130

131+
## Rendering `<head>` Tags
132+
133+
A common need with SSR is rendering dynamic `<head>` content (e.g. `<title>`,
134+
`<meta>`) so that crawlers and social previews see the right metadata on first
135+
load.
136+
137+
`ServerRenderer` only renders your component tree (typically at the body of the document),
138+
it has no access to `<head>`. Head tags must therefore be generated **on the server, outside of
139+
Yew**, and spliced into the HTML template before it is sent to the client.
140+
141+
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
142+
route from the request URL, generates the appropriate `<title>` and `<meta>`
143+
tags, and injects them into the Trunk-generated `index.html` before
144+
`</head>`.
145+
146+
:::info
147+
148+
For a fully SSR-compatible third-party solution, use [the `<Helmet/>` component from Bounce](https://docs.rs/bounce/latest/bounce/helmet/index.html).
149+
150+
:::
151+
131152
## SSR Hydration
132153

133154
Hydration is the process that connects a Yew application to the

website/i18n/ja/docusaurus-plugin-content-docs/current/advanced-topics/server-side-rendering.mdx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,19 @@ Yewは、`<Suspense />` を使用してこの問題を解決する異なるア
8484

8585
この方法により、開発者はサーバーサイドレンダリングに対応したクライアント非依存のアプリケーションを簡単に構築し、データ取得を行うことができます。
8686

87-
## SSR ハイドレーション
87+
## `<head>` タグのレンダリング
88+
89+
SSR でよく必要とされるのは、クローラーやソーシャルプレビューが最初のロード時に正しいメタデータを参照できるよう、動的な `<head>` コンテンツ(`<title>``<meta>` など)をレンダリングすることです。
90+
91+
`ServerRenderer` はコンポーネントツリー(通常はドキュメントの body 部分)のみをレンダリングし、`<head>` にはアクセスできません。そのため、head タグは **Yew の外部でサーバー側に**生成し、クライアントに送信する前に HTML テンプレートに埋め込む必要があります。
92+
93+
[`ssr_router` サンプル](https://github.com/yewstack/yew/blob/master/examples/ssr_router/src/bin/ssr_router_server.rs) はこのパターンを示しています:サーバーはリクエスト URL からルートを判別し、適切な `<title>` および `<meta>` タグを生成して、Trunk が生成した `index.html``</head>` の前に挿入します。
94+
95+
:::info
96+
97+
完全に SSR 互換のサードパーティソリューションとして、[Bounce の `<Helmet/>` コンポーネント](https://docs.rs/bounce/latest/bounce/helmet/index.html) が利用できます。
98+
99+
:::
88100

89101
## サーバーサイドレンダリングハイドレーション(SSR Hydration)
90102

website/i18n/ja/docusaurus-plugin-content-docs/version-0.22/advanced-topics/server-side-rendering.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ Yewは、`<Suspense />` を使用してこの問題を解決する異なるア
8484

8585
この方法により、開発者はサーバーサイドレンダリングに対応したクライアント非依存のアプリケーションを簡単に構築し、データ取得を行うことができます。
8686

87+
## `<head>` タグのレンダリング
88+
89+
SSR でよく必要とされるのは、クローラーやソーシャルプレビューが最初のロード時に正しいメタデータを参照できるよう、動的な `<head>` コンテンツ(`<title>``<meta>` など)をレンダリングすることです。
90+
91+
`ServerRenderer` はコンポーネントツリー(通常はドキュメントの body 部分)のみをレンダリングし、`<head>` にはアクセスできません。そのため、head タグは **Yew の外部でサーバー側に**生成し、クライアントに送信する前に HTML テンプレートに埋め込む必要があります。
92+
93+
[`ssr_router` サンプル](https://github.com/yewstack/yew/blob/master/examples/ssr_router/src/bin/ssr_router_server.rs) はこのパターンを示しています:サーバーはリクエスト URL からルートを判別し、適切な `<title>` および `<meta>` タグを生成して、Trunk が生成した `index.html``</head>` の前に挿入します。
94+
95+
:::info
96+
97+
完全に SSR 互換のサードパーティソリューションとして、[Bounce の `<Helmet/>` コンポーネント](https://docs.rs/bounce/latest/bounce/helmet/index.html) が利用できます。
98+
99+
:::
100+
87101
## SSR ハイドレーション
88102

89103
## サーバーサイドレンダリングハイドレーション(SSR Hydration)

website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/advanced-topics/server-side-rendering.mdx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,45 +66,57 @@ async fn no_main() {
6666

6767
## 服务端渲染期间的数据获取
6868

69-
数据获取是服务端渲染和注水(hydration)期间的难点之一。
69+
数据获取是服务端渲染和水合(hydration)期间的难点之一。
7070

7171
传统做法中,当一个组件渲染时,它会立即可用(输出一个虚拟 DOM 以进行渲染)。当组件不需要获取任何数据时,这种方式是有效的。但是如果组件在渲染时想要获取一些数据会发生什么呢?
7272

7373
过去,Yew 没有机制来检测组件是否仍在获取数据。数据获取客户端负责实现一个解决方案,以检测在初始渲染期间请求了什么,并在请求完成后触发第二次渲染。服务器会重复这个过程,直到在返回响应之前没有在渲染期间添加更多的挂起请求。
7474

75-
这不仅浪费了 CPU 资源,因为重复渲染组件,而且数据客户端还需要提供一种方法,在注水过程中使在服务端获取的数据可用,以确保初始渲染返回的虚拟 DOM 与服务端渲染的 DOM 树一致,这可能很难实现。
75+
这不仅浪费了 CPU 资源,因为重复渲染组件,而且数据客户端还需要提供一种方法,在水合过程中使在服务端获取的数据可用,以确保初始渲染返回的虚拟 DOM 与服务端渲染的 DOM 树一致,这可能很难实现。
7676

7777
Yew 采用了一种不同的方法,通过 `<Suspense />` 来解决这个问题。
7878

7979
`<Suspense />` 是一个特殊的组件,当在客户端使用时,它提供了一种在组件获取数据(挂起)时显示一个回退 UI 的方法,并在数据获取完成后恢复到正常 UI。
8080

8181
当应用程序在服务端渲染时,Yew 会等待组件不再挂起,然后将其序列化到字符串缓冲区中。
8282

83-
在注水过程中`<Suspense />` 组件中的元素保持未注水状态,直到所有子组件不再挂起。
83+
在水合过程中`<Suspense />` 组件中的元素保持未水合状态,直到所有子组件不再挂起。
8484

8585
通过这种方法,开发人员可以轻松构建一个准备好进行服务端渲染的、与客户端无关的应用程序,并进行数据获取。
8686

87-
## SSR Hydration
87+
## 渲染 `<head>` 标签
8888

89-
## 服务端渲染注水(SSR Hydration)
89+
SSR 中的一个常见需求是渲染动态 `<head>` 内容(例如 `<title>``<meta>`),使爬虫和社交预览在首次加载时能看到正确的元数据。
9090

91-
注水是将 Yew 应用程序连接到服务端生成的 HTML 文件的过程。默认情况下,`ServerRender` 打印可注水的 HTML 字符串,其中包含额外的信息以便于注水。当调用 `Renderer::hydrate` 方法时,Yew 不会从头开始渲染,而是将应用程序生成的虚拟 DOM 与服务器渲染器生成的 HTML 字符串进行协调。
91+
`ServerRenderer` 只渲染组件树(通常对应文档的 body 部分),无法访问 `<head>`。因此,head 标签必须**在服务端、Yew 之外**生成,并在发送给客户端之前拼接到 HTML 模板中。
92+
93+
[`ssr_router` 示例](https://github.com/yewstack/yew/blob/master/examples/ssr_router/src/bin/ssr_router_server.rs) 演示了这一模式:服务端从请求 URL 识别路由,生成适当的 `<title>``<meta>` 标签,并将它们注入到 Trunk 生成的 `index.html``</head>` 之前。
94+
95+
:::info
96+
97+
如需完全兼容 SSR 的第三方解决方案,请使用 [Bounce 的 `<Helmet/>` 组件](https://docs.rs/bounce/latest/bounce/helmet/index.html)
98+
99+
:::
100+
101+
## SSR 水合(SSR Hydration)
102+
103+
水合是将 Yew 应用程序连接到服务端生成的 HTML 文件的过程。默认情况下,`ServerRender` 打印可水合的 HTML 字符串,其中包含额外的信息以便于水合。当调用 `Renderer::hydrate` 方法时,Yew 不会从头开始渲染,而是将应用程序生成的虚拟 DOM 与服务器渲染器生成的 HTML 字符串进行协调。
92104

93105
:::caution
94106

95-
为了成功对由 `ServerRenderer` 创建的 HTML 标记注水,客户端必须生成一个虚拟 DOM 布局,它与用于 SSR 的布局完全匹配,包括不包含任何元素的组件。如果您有任何只在一个实现中有用的组件,您可能希望使用 `PhantomComponent` 来填充额外组件的位置。
107+
要成功对由 `ServerRenderer` 创建的 HTML 标记进行水合,客户端必须生成一个虚拟 DOM 布局,它与用于 SSR 的布局完全匹配,包括不包含任何元素的组件。如果您有任何只在一个实现中有用的组件,您可能希望使用 `PhantomComponent` 来填充额外组件的位置。
96108
:::
97109

98110
:::warning
99111

100-
只有在浏览器初始渲染 SSR 输出(静态 HTML)后,真实 DOM 与预期 DOM 匹配时,注水才能成功。如果您的 HTML 不符合规范,注水可能会失败。浏览器可能会更改不正确的 HTML 的 DOM 结构,导致实际 DOM 与预期 DOM 不同。例如,[如果您有一个没有 `<tbody>``<table>`,浏览器可能会向 DOM 添加一个 `<tbody>`](https://github.com/yewstack/yew/issues/2684)
112+
只有在浏览器初始渲染 SSR 输出(静态 HTML)后,真实 DOM 与预期 DOM 匹配时,水合才能成功。如果您的 HTML 不符合规范,水合可能会失败。浏览器可能会更改不正确的 HTML 的 DOM 结构,导致实际 DOM 与预期 DOM 不同。例如,[如果您有一个没有 `<tbody>``<table>`,浏览器可能会向 DOM 添加一个 `<tbody>`](https://github.com/yewstack/yew/issues/2684)
101113
:::
102114

103-
## 注水期间的组件生命周期
115+
## 水合期间的组件生命周期
104116

105-
在注水期间,组件在创建后安排了 2 次连续的渲染。任何效果都是在第二次渲染完成后调用的。确保您的组件的渲染函数没有副作用是很重要的。它不应该改变任何状态或触发额外的渲染。如果您的组件当前改变状态或触发额外的渲染,请将它们移动到 `use_effect` 钩子中。
117+
在水合期间,组件在创建后安排了 2 次连续的渲染。任何效果都是在第二次渲染完成后调用的。确保您的组件的渲染函数没有副作用是很重要的。它不应该改变任何状态或触发额外的渲染。如果您的组件当前改变状态或触发额外的渲染,请将它们移动到 `use_effect` 钩子中。
106118

107-
在注水过程中,可以使用结构化组件进行服务端渲染,视图函数将在渲染函数之前被调用多次。直到调用渲染函数之前,DOM 被认为是未连接的,您应该防止在调用 `rendered()` 方法之前访问渲染节点。
119+
在水合过程中,可以使用结构化组件进行服务端渲染,视图函数将在渲染函数之前被调用多次。直到调用渲染函数之前,DOM 被认为是未连接的,您应该防止在调用 `rendered()` 方法之前访问渲染节点。
108120

109121
## 示例
110122

@@ -120,7 +132,7 @@ fn App() -> Html {
120132
fn main() {
121133
let renderer = Renderer::<App>::new();
122134

123-
// 对 body 元素下的所有内容进行注水,移除尾部元素(如果有)。
135+
// 对 body 元素下的所有内容进行水合,并移除尾部元素(如果有)。
124136
renderer.hydrate();
125137
}
126138
```

0 commit comments

Comments
 (0)