Skip to main content

dioxus_server/
document.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_core::Element;
8use dioxus_core_macro::rsx;
9use dioxus_document::{
10    Document, Eval, LinkProps, MetaProps, NoOpDocument, ScriptProps, StyleProps,
11};
12use dioxus_html::elements::prelude::*;
13use dioxus_html::*;
14use dioxus_ssr::Renderer;
15use parking_lot::RwLock;
16use std::sync::LazyLock;
17
18static RENDERER: LazyLock<RwLock<Renderer>> = LazyLock::new(|| RwLock::new(Renderer::new()));
19
20/// Reset the static renderer to a fresh state, clearing its cache.
21pub(crate) fn reset_renderer() {
22    RENDERER.write().clear();
23}
24
25#[derive(Default)]
26struct ServerDocumentInner {
27    streaming: bool,
28    title: Option<String>,
29    meta: Vec<Element>,
30    link: Vec<Element>,
31    script: Vec<Element>,
32}
33
34/// A Document provider that collects all contents injected into the head for SSR rendering.
35#[derive(Default)]
36pub struct ServerDocument(RefCell<ServerDocumentInner>);
37
38impl ServerDocument {
39    pub(crate) fn title(&self) -> Option<String> {
40        let myself = self.0.borrow();
41        myself.title.as_ref().map(|title| {
42            RENDERER
43                .write()
44                .render_element(rsx! { title { "{title}" } })
45        })
46    }
47
48    pub(crate) fn render(&self, to: &mut impl std::fmt::Write) -> std::fmt::Result {
49        let myself = self.0.borrow();
50        let element = rsx! {
51            {myself.meta.iter().map(|m| rsx! { {m} })}
52            {myself.link.iter().map(|l| rsx! { {l} })}
53            {myself.script.iter().map(|s| rsx! { {s} })}
54        };
55
56        RENDERER.write().render_element_to(to, element)?;
57
58        Ok(())
59    }
60
61    pub(crate) fn start_streaming(&self) {
62        self.0.borrow_mut().streaming = true;
63    }
64
65    pub(crate) fn warn_if_streaming(&self) {
66        if self.0.borrow().streaming {
67            tracing::warn!(
68                "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."
69            );
70        }
71    }
72
73    /// Write the head element into the serialized context for hydration
74    /// We write true if the head element was written to the DOM during server side rendering
75    #[track_caller]
76    pub(crate) fn serialize_for_hydration(&self) {
77        // We only serialize the head elements if the web document feature is enabled
78        #[cfg(feature = "document")]
79        {
80            dioxus_fullstack_core::head_element_hydration_entry()
81                .insert(&!self.0.borrow().streaming, std::panic::Location::caller());
82        }
83    }
84}
85
86impl Document for ServerDocument {
87    fn eval(&self, js: String) -> Eval {
88        NoOpDocument.eval(js)
89    }
90
91    fn set_title(&self, title: String) {
92        self.warn_if_streaming();
93        self.0.borrow_mut().title = Some(title);
94    }
95
96    fn create_meta(&self, props: MetaProps) {
97        self.0.borrow_mut().meta.push(rsx! {
98            meta {
99                name: props.name,
100                charset: props.charset,
101                http_equiv: props.http_equiv,
102                content: props.content,
103                property: props.property,
104                "data": props.data,
105                ..props.additional_attributes,
106            }
107        });
108    }
109
110    fn create_script(&self, props: ScriptProps) {
111        let children = props.script_contents().ok();
112        self.0.borrow_mut().script.push(rsx! {
113            script {
114                src: props.src,
115                defer: props.defer,
116                crossorigin: props.crossorigin,
117                fetchpriority: props.fetchpriority,
118                integrity: props.integrity,
119                nomodule: props.nomodule,
120                nonce: props.nonce,
121                referrerpolicy: props.referrerpolicy,
122                r#type: props.r#type,
123                ..props.additional_attributes,
124                {children}
125            }
126        });
127    }
128
129    fn create_style(&self, props: StyleProps) {
130        let contents = props.style_contents().ok();
131        self.0.borrow_mut().script.push(rsx! {
132            style {
133                media: props.media,
134                nonce: props.nonce,
135                title: props.title,
136                ..props.additional_attributes,
137                {contents}
138            }
139        })
140    }
141
142    fn create_link(&self, props: LinkProps) {
143        self.0.borrow_mut().link.push(rsx! {
144            link {
145                rel: props.rel,
146                media: props.media,
147                title: props.title,
148                disabled: props.disabled,
149                r#as: props.r#as,
150                sizes: props.sizes,
151                href: props.href,
152                crossorigin: props.crossorigin,
153                referrerpolicy: props.referrerpolicy,
154                fetchpriority: props.fetchpriority,
155                hreflang: props.hreflang,
156                integrity: props.integrity,
157                r#type: props.r#type,
158                blocking: props.blocking,
159                ..props.additional_attributes,
160            }
161        })
162    }
163
164    fn create_head_component(&self) -> bool {
165        self.warn_if_streaming();
166        self.serialize_for_hydration();
167        true
168    }
169}