maomi
Introduction
Guide
Documents

Server Side Rendering

How server side rendering works

Normally, maomi generates HTML elements in browsers. At startup, it takes an empty DOM node, a.k.a. a mount-point, and then repeatedly insert or modify other nodes.

The server side rendering can generate static HTML segments in the server. The HTML segment can be inserted into the mount-point before startup. Then maomi will reuse the generated HTML nodes at startup.

This allows static contents embed in HTML directly, and be understood by search engines better.

Because maomi is in rust, the component code can be compiled to native binary which can be used to generate HTML segments. It means the components are compiled twice: one is to the native binary which is used to generate the static HTML, the other is to the WebAssembly binary which is used in browser runtime.

To enable server side rendering, some features need to be specified in Cargo.toml. The "prerendering" feature is to enable the server side support, and the "prerendering-apply" feature is to enable the reuse of the server side generated nodes.

[dependencies]
maomi = { version = "0.4", features = ["prerendering", "prerendering-apply"] }
maomi-dom = { version = "0.4", features = ["prerendering", "prerendering-apply"] }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
bincode = "1.3"

Note that these features slightly increase the runtime overhead. Do not enable them if server side rendering is not needed.

Prerenderable Components

To be rendered in server side, the component must implement "PrerenderableComponent". This trait has two associated types and two functions.

The "QueryData" type refers to the query, e.g. the URL params, the POST data, or some other related data. For static contents, it can simply be "()".

The "PrerenderingData" type is converted from the "QueryData", containing some useful parts of the query for the component startup. It should be serializable and transferable from server side to the client side.

When doing server side rendering, firstly, the "prerendering_data" function is called in server side to convert "QueryData" to "PrerenderingData". (It may also be called in client side when doing client side rendering.)

Secondly, the component starts in the server side, and the "apply_prerendering_data" is called. This function can modify component fields according to the "PrerenderingData". The HTML segment can be generated when the component created.

Thirdly, the generated HTML should be embed into the mount-point, and the "PrerenderingData" should also be transferred to the client side.

Fourthly, the component starts in the client side, and the "apply_prerendering_data" is also called. This function must do the same thing as it did in the server side. Thus this component can reuse the generated HTML nodes.

#[component(Backend = DomBackend)]
struct MyWebsite {
template: template! {
<div> "My Website" </div>
},
content: String,
}
impl Component for MyWebsite {
fn new() -> Self {
Self {
template: Default::default(),
content: String::new(),
}
}
}
#[async_trait::async_trait]
impl PrerenderableComponent for MyWebsite {
type QueryData = ();
type PrerenderingData = ();
async fn prerendering_data(_: &Self::QueryData) -> Self::PrerenderingData {
()
}
fn apply_prerendering_data(&mut self, _: Self::PrerenderingData) {
self.content = "Prerendering String".to_string();
}
}

Note that these features slightly increase the runtime overhead. Do not enable them if server side rendering is not needed.

Generate HTML in server side

To generate HTML in server side, a special kind of backend context is used.

use maomi::template::ComponentTemplate;
async fn prerendering<C: PrerenderableComponent + ComponentTemplate<DomBackend>>(
query_data: &C::QueryData,
) -> (Vec<u8>, Vec<u8>)
where
C::PrerenderingData: serde::Serialize,
{
// generate the `PrerenderingData`
let prerendering_data = maomi::BackendContext::<DomBackend>::prerendering_data::<C>(&query_data).await;
// serialize the `PrerenderingData` to transfer to the client side
let prerendering_data_bin = bincode::serialize(prerendering_data.get()).unwrap();
// initialize a backend context for HTML generation
let prerendering_dom_backend = DomBackend::prerendering();
let backend_context = maomi::BackendContext::new(prerendering_dom_backend);
// generate HTML segment
let (_mount_point, html_buffer) = backend_context
.enter_sync(move |ctx| {
let mount_point = ctx.prerendering_attach(prerendering_data).unwrap();
let mut html_buffer = vec![];
ctx.write_prerendering_html(&mut html_buffer).unwrap();
(mount_point, html_buffer)
})
.map_err(|_| "Cannot init mount point")
.unwrap();
// returns the HTML and the `PrerenderingData`
// the HTML should finally be placed in the HTML <body />
// the `PrerenderingData` can be base64 embed to HTML or transfer in other forms
(html_buffer, prerendering_data_bin)
}

Reuse the generated HTML in client side

To reuse the generated HTML, the backend context should be initialized in with "PrerenderingData".

use maomi::template::ComponentTemplate;
fn prerendering_apply<C: PrerenderableComponent + ComponentTemplate<DomBackend>>(
prerendering_data_bin: Vec<u8>,
)
where
for<'de> C::PrerenderingData: serde::Deserialize<'de>,
{
// deserialize the `PrerenderingData`
let data = bincode::deserialize(&prerendering_data_bin).unwrap();
// construct the `PrerenderingData`
let prerendering_data = maomi::PrerenderingData::<C>::new(data);
// initialize a backend context for reuse generated HTML
let dom_backend = DomBackend::new_prerendered();
let backend_context = maomi::BackendContext::new(dom_backend);
// create the mount point
let mount_point = backend_context
.enter_sync(move |ctx| {
let mount_point = ctx.prerendering_attach(prerendering_data).unwrap();
ctx.apply_prerendered_document_body().unwrap();
mount_point
})
.map_err(|_| "Cannot init mount point")
.unwrap();
// leak the mount point and the backend context to keep working forever
std::mem::forget(mount_point);
std::mem::forget(backend_context);
}