a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
1# Server-Side Rendering 2 3VoltX can render HTML on the server and hydrate it on the client so that the initial paint is fast and SEO-friendly without sacrificing interactivity. 4 5## When SSR Helps 6 7- Marketing and content-heavy pages that rely on search indexing. 8- Dashboards that must show current data immediately on first paint. 9- Progressive enhancement flows where the page should work without JavaScript. 10- Latency-sensitive experiences served to slow devices or connections. 11 12## When CSR Is Enough 13 14- Highly interactive applications dominated by client-side state. 15- Authenticated surfaces hidden from crawlers. 16- Rapid prototypes where deployment speed outweighs initial paint. 17- Workloads where duplicating rendering logic on the server adds complexity without user benefit. 18 19## Rendering Flow 20 211. Render HTML on the server and embed serialized state. 222. Ship that HTML to the browser. 233. Call `hydrate()` to attach VoltX bindings without re-rendering. 24 25### Produce Markup on the Server 26 27Use `serializeScope()` to convert reactive state into JSON before embedding it in the HTML you return: 28 29```ts 30import { serializeScope, signal } from "@volt/volt"; 31 32export function renderCounter() { 33 const scope = { 34 count: signal(0), 35 label: "Visitors", 36 }; 37 38 const serialized = serializeScope(scope); 39 40 return ` 41 <div id="counter" data-volt> 42 <script type="application/json" id="volt-state-counter"> 43 ${serialized} 44 </script> 45 46 <h2 data-volt-text="label">${scope.label}</h2> 47 <button data-volt-on:click="count++">Clicked <span data-volt-text="count">${scope.count.get()}</span> times</button> 48 </div> 49 `; 50} 51``` 52 53Guidelines: 54 55- Every server-rendered root must have an `id`. VoltX looks for `<script id="volt-state-{id}">` next to it. 56- The script tag must use `type="application/json"` and contain valid JSON. Pretty printing is fine; whitespace is ignored. 57- Keep serialized data minimal. Fetch large collections after hydration. 58 59### Send HTML to the Client 60 61The HTML you return should already contain the serialized state script tag. VoltX will reuse the DOM structure; do **not** re-render the same tree on the client. 62 63### Hydrate in the Browser 64 65Call `hydrate()` once the page loads. It discovers `[data-volt]` roots automatically. 66 67```ts 68import { hydrate } from "@volt/volt"; 69 70document.addEventListener("DOMContentLoaded", () => { 71 hydrate({ 72 rootSelector: "[data-volt]", 73 skipHydrated: true, // defaults to true; repeat calls ignore already hydrated roots 74 }); 75}); 76``` 77 78If you only hydrate a specific block, pass a narrower selector or manually select elements and call `hydrate({ rootSelector: "#counter" })`. 79 80## Serialized State 81 82VoltX exposes helpers that mirror the runtime's internal behavior: 83 84| Helper | Action | 85| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | 86| `serializeScope(scope)` | Converts signals into their raw values before you embed them | 87| `deserializeScope(data)` | Restores a JSON payload into a fresh scope. Useful for streaming responses or server actions that return HTML partials | 88| `isServerRendered(element)` | Tells you if VoltX found a matching serialized state block | 89| `isHydrated(element)` | Detects whether a root has already been hydrated, which is handy when mixing SSR content with dynamic client mounts | 90| `getSerializedState(element)` | Reads the JSON payload for debugging or custom hydration flows. | 91 92```ts 93import { deserializeScope, getSerializedState } from "@volt/volt"; 94 95const root = document.querySelector("#counter")!; 96const state = getSerializedState(root); 97 98if (state) { 99 const scope = deserializeScope(state); 100 console.log(scope.count.get()); // -> 0 101} 102``` 103 104## Avoiding Flash of Unstyled Content 105 106Hydrated markup should look identical before and after VoltX runs. When CSS or font loading causes flicker, consider these patterns: 107 108### Hide Until Hydrated 109 110```html 111<style> 112 [data-volt]:not([data-volt-hydrated]) { 113 visibility: hidden; 114 } 115 116 [data-volt][data-volt-hydrated] { 117 visibility: visible; 118 } 119</style> 120``` 121 122VoltX sets `data-volt-hydrated="true"` once `hydrate()` completes, so you can safely reveal content at that point. 123 124### Use a Loading Overlay 125 126```html 127<div id="app" data-volt> 128 <!-- server-rendered content --> 129</div> 130<div class="loading-overlay">Loading…</div> 131 132<script type="module"> 133 import { hydrate } from "@volt/volt"; 134 135 hydrate(); 136</script> 137``` 138 139```css 140.loading-overlay { 141 position: fixed; 142 inset: 0; 143 background: white; 144 display: grid; 145 place-items: center; 146} 147 148[data-volt-hydrated] ~ .loading-overlay { 149 display: none; 150} 151``` 152 153### Progressive Enhancement 154 155Render functional HTML that works without JavaScript, then let VoltX enhance it: 156 157```html 158<form id="contact" method="POST" action="/submit" data-volt> 159 <script type="application/json" id="volt-state-contact"> 160 { "submitted": false } 161 </script> 162 163 <input type="email" name="email" required /> 164 <p data-volt-if="submitted" data-volt-text="'Thank you!'"></p> 165 <button type="submit">Submit</button> 166</form> 167``` 168 169## Security Checklist 170 171- Escape user-generated content in the HTML you render. 172- Validate JSON before embedding it in `<script type="application/json">` tags. 173- Apply a strict Content Security Policy (CSP) so inline scripts are controlled. 174- Never serialize secrets. Treat the hydrated payload as public data. 175 176Pair these guidelines with the lifecycle hooks documented in [lifecycle](./lifecycle) to coordinate mount-time work across SSR and client renders.