//! Server backend — SSR: render to HTML string, served by axum. //! //! This is the primary SSR backend. An axum handler calls `render_to_string()` //! on the component tree and returns the result as an HTTP response. //! //! The same component code runs server-side without any changes. Only the //! backend (chosen by `el.toml`) differs. use crate::{EventHandler, PlatformBackend, PlatformError, PlatformNode, PlatformResult}; /// Server-side rendering backend. /// /// Renders component trees to full HTML strings. No DOM, no browser APIs. /// An axum handler uses this backend to generate the initial page HTML. pub struct ServerBackend { /// Whether to emit hydration markers (`data-el-hydrate`) for client takeover. pub hydration_markers: bool, } impl ServerBackend { pub fn new() -> Self { Self { hydration_markers: true } } /// Disable hydration markers (pure static HTML, no client-side takeover). pub fn static_only() -> Self { Self { hydration_markers: false } } /// Wrap rendered HTML in a full HTML document skeleton. pub fn render_page( &self, node: &PlatformNode, title: &str, runtime_script: &str, ) -> PlatformResult { let body = self.render_to_string(node)?; Ok(format!( r#" {title}
{body}
"# )) } } impl Default for ServerBackend { fn default() -> Self { Self::new() } } impl PlatformBackend for ServerBackend { fn name(&self) -> &'static str { "server" } fn create_element(&self, tag: &str) -> PlatformResult { Ok(PlatformNode::element(tag)) } fn create_text(&self, content: &str) -> PlatformResult { Ok(PlatformNode::text(content)) } fn set_attribute( &self, node: &mut PlatformNode, name: &str, value: &str, ) -> PlatformResult<()> { node.attributes.retain(|a| a.name != name); node.attributes.push(crate::Attribute::new(name, value)); Ok(()) } fn remove_attribute(&self, node: &mut PlatformNode, name: &str) -> PlatformResult<()> { node.attributes.retain(|a| a.name != name); Ok(()) } fn append_child( &self, parent: &mut PlatformNode, child: PlatformNode, ) -> PlatformResult<()> { parent.children.push(child); Ok(()) } fn remove_child(&self, parent: &mut PlatformNode, child_index: usize) -> PlatformResult<()> { if child_index >= parent.children.len() { return Err(PlatformError::Render(format!( "server: child index {} out of bounds", child_index ))); } parent.children.remove(child_index); Ok(()) } fn replace_child( &self, parent: &mut PlatformNode, index: usize, new_child: PlatformNode, ) -> PlatformResult<()> { if index >= parent.children.len() { return Err(PlatformError::Render(format!( "server: replace_child index {} out of bounds", index ))); } parent.children[index] = new_child; Ok(()) } fn bind_event( &self, node: &mut PlatformNode, event: &str, _handler: EventHandler, ) -> PlatformResult<()> { // On the server, event handlers are emitted as data attributes. // The client-side hydration pass picks them up and binds real listeners. if self.hydration_markers { node.attributes .push(crate::Attribute::new(format!("data-el-{}", event), "hydrate")); } Ok(()) } fn render_to_string(&self, node: &PlatformNode) -> PlatformResult { Ok(node.to_html()) } fn mount(&self, _root: PlatformNode, _container_id: &str) -> PlatformResult<()> { // Server has no mount concept — rendering is one-shot. Ok(()) } fn patch(&self, _old: &PlatformNode, _new: &PlatformNode) -> PlatformResult<()> { // Server rendering is stateless — no patch needed. Ok(()) } fn supports_ssr(&self) -> bool { true } }