dioxus_fullstack/document/
server.rs

1//! On the server, we collect any elements that should be rendered into the head in the first frame of SSR.
2//! After the first frame, we have already sent down the head, so we can't modify it in place. The web client
3//! will hydrate the head with the correct contents once it loads.
4
5use std::cell::RefCell;
6
7use dioxus_lib::{document::*, prelude::*};
8use dioxus_ssr::Renderer;
9use once_cell::sync::Lazy;
10use parking_lot::RwLock;
11
12static RENDERER: Lazy<RwLock<Renderer>> = Lazy::new(|| RwLock::new(Renderer::new()));
13
14#[derive(Default)]
15struct ServerDocumentInner {
16    streaming: bool,
17    title: Option<String>,
18    meta: Vec<Element>,
19    link: Vec<Element>,
20    script: Vec<Element>,
21}
22
23/// A Document provider that collects all contents injected into the head for SSR rendering.
24#[derive(Default)]
25pub struct ServerDocument(RefCell<ServerDocumentInner>);
26
27impl ServerDocument {
28    pub(crate) fn title(&self) -> Option<String> {
29        let myself = self.0.borrow();
30        myself.title.as_ref().map(|title| {
31            RENDERER
32                .write()
33                .render_element(rsx! { title { "{title}" } })
34        })
35    }
36
37    pub(crate) fn render(&self, to: &mut impl std::fmt::Write) -> std::fmt::Result {
38        let myself = self.0.borrow();
39        let element = rsx! {
40            {myself.meta.iter().map(|m| rsx! { {m} })}
41            {myself.link.iter().map(|l| rsx! { {l} })}
42            {myself.script.iter().map(|s| rsx! { {s} })}
43        };
44
45        RENDERER.write().render_element_to(to, element)?;
46
47        Ok(())
48    }
49
50    pub(crate) fn start_streaming(&self) {
51        self.0.borrow_mut().streaming = true;
52    }
53
54    pub(crate) fn warn_if_streaming(&self) {
55        if self.0.borrow().streaming {
56            tracing::warn!("Attempted to insert content into the head after the initial streaming frame. Inserting content into the head only works during the initial render of SSR outside before resolving any suspense boundaries.");
57        }
58    }
59
60    /// Write the head element into the serialized context for hydration
61    /// We write true if the head element was written to the DOM during server side rendering
62    #[track_caller]
63    pub(crate) fn serialize_for_hydration(&self) {
64        // We only serialize the head elements if the web document feature is enabled
65        #[cfg(feature = "document")]
66        {
67            let serialize = crate::html_storage::serialize_context();
68            serialize.push(&!self.0.borrow().streaming, std::panic::Location::caller());
69        }
70    }
71}
72
73impl Document for ServerDocument {
74    fn eval(&self, js: String) -> Eval {
75        NoOpDocument.eval(js)
76    }
77
78    fn set_title(&self, title: String) {
79        self.warn_if_streaming();
80        self.0.borrow_mut().title = Some(title);
81    }
82
83    fn create_meta(&self, props: MetaProps) {
84        self.0.borrow_mut().meta.push(rsx! {
85            meta {
86                name: props.name,
87                charset: props.charset,
88                http_equiv: props.http_equiv,
89                content: props.content,
90                property: props.property,
91                ..props.additional_attributes,
92            }
93        });
94    }
95
96    fn create_script(&self, props: ScriptProps) {
97        let children = props.script_contents().ok();
98        self.0.borrow_mut().script.push(rsx! {
99            script {
100                src: props.src,
101                defer: props.defer,
102                crossorigin: props.crossorigin,
103                fetchpriority: props.fetchpriority,
104                integrity: props.integrity,
105                nomodule: props.nomodule,
106                nonce: props.nonce,
107                referrerpolicy: props.referrerpolicy,
108                r#type: props.r#type,
109                ..props.additional_attributes,
110                {children}
111            }
112        });
113    }
114
115    fn create_style(&self, props: StyleProps) {
116        let contents = props.style_contents().ok();
117        self.0.borrow_mut().script.push(rsx! {
118            style {
119                media: props.media,
120                nonce: props.nonce,
121                title: props.title,
122                ..props.additional_attributes,
123                {contents}
124            }
125        })
126    }
127
128    fn create_link(&self, props: LinkProps) {
129        self.0.borrow_mut().link.push(rsx! {
130            link {
131                rel: props.rel,
132                media: props.media,
133                title: props.title,
134                disabled: props.disabled,
135                r#as: props.r#as,
136                sizes: props.sizes,
137                href: props.href,
138                crossorigin: props.crossorigin,
139                referrerpolicy: props.referrerpolicy,
140                fetchpriority: props.fetchpriority,
141                hreflang: props.hreflang,
142                integrity: props.integrity,
143                r#type: props.r#type,
144                blocking: props.blocking,
145                ..props.additional_attributes,
146            }
147        })
148    }
149
150    fn create_head_component(&self) -> bool {
151        self.warn_if_streaming();
152        self.serialize_for_hydration();
153        true
154    }
155}