Server-Side Rendering with dd<el>

This part of the documentation is primarily intended for technical enthusiasts and authors of 3rd-party libraries. It describes an advanced feature, not a core part of the library. Most users will not need to implement this functionality directly in their applications. This capability will hopefully be covered by third-party libraries or frameworks that provide simpler SSR integration using dd<el>.

dd<el> isn’t limited to browser environments. Thanks to its flexible architecture, it can be used for server-side rendering (SSR) to generate static HTML files. This is achieved through integration with for example jsdom, a JavaScript implementation of web standards for Node.js.

Additionally, you might consider using these alternative solutions:

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { register, unregister, queue } from "deka-dom-el/jsdom";

# Why Server-Side Rendering?

SSR offers several benefits:

# How jsdom Integration Works

The jsdom export in dd<el> provides the necessary tools to use the library in Node.js by integrating with jsdom. Here’s what it does:

  1. Creates a virtual DOM environment in Node.js using jsdom
  2. Registers DOM globals like HTMLElement, document, etc. for dd<el> to use
  3. Sets an SSR flag in the environment to enable SSR-specific behaviors
  4. Provides a promise queue system for managing async operations during rendering
  5. Handles DOM property/attribute mapping differences between browsers and jsdom
// Basic jsdom integration example import { JSDOM } from "jsdom"; import { register, unregister, queue } from "deka-dom-el/jsdom"; // Create a jsdom instance const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>"); // Register the dom with deka-dom-el const { el } = await register(dom); // Use deka-dom-el normally dom.window.document.body.append( el("div", { className: "container" }).append( el("h1", "Hello, SSR World!"), el("p", "This content was rendered on the server.") ) ); // Wait for any async operations to complete await queue(); // Get the rendered HTML const html = dom.serialize(); console.log(html); // Clean up when done unregister();

# Basic SSR Example

Here’s a simple example of how to use dd<el> for server-side rendering in a Node.js script:

// Basic SSR Example import { JSDOM } from "jsdom"; import { register, queue } from "deka-dom-el/jsdom"; import { writeFileSync } from "node:fs"; async function renderPage() { // Create a jsdom instance const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>"); // Register with deka-dom-el and get the el function const { el } = await register(dom); // Create a simple header component // can be separated into a separate file and use `import { el } from "deka-dom-el"` function Header({ title }) { return el("header").append( el("h1", title), el("nav").append( el("ul").append( el("li").append(el("a", { href: "/" }, "Home")), el("li").append(el("a", { href: "/about" }, "About")), el("li").append(el("a", { href: "/contact" }, "Contact")) ) ) ); } // Create the page content dom.window.document.body.append( el(Header, { title: "My Static Site" }), el("main").append( el("h2", "Welcome!"), el("p", "This page was rendered with deka-dom-el on the server.") ), el("footer", "© 2025 My Company") ); // Wait for any async operations await queue(); // Get the HTML and write it to a file const html = dom.serialize(); writeFileSync("index.html", html); console.log("Page rendered successfully!"); } renderPage().catch(console.error);

# Building a Static Site Generator

You can build a complete static site generator with dd<el>. In fact, this documentation site is built using dd<el> for server-side rendering! Here’s how the documentation build process works:

// Building a simple static site generator import { JSDOM } from "jsdom"; import { register, queue } from "deka-dom-el/jsdom"; import { writeFileSync, mkdirSync } from "node:fs"; async function buildSite() { // Define pages to build const pages = [ { id: "index", title: "Home", component: "./pages/home.js" }, { id: "about", title: "About", component: "./pages/about.js" }, { id: "docs", title: "Documentation", component: "./pages/docs.js" } ]; // Create output directory mkdirSync("./dist/docs", { recursive: true }); // Build each page for (const page of pages) { // Create a fresh jsdom instance for each page const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>"); // Register with deka-dom-el const { el } = await register(dom); // Import the page component // use `import { el } from "deka-dom-el"` const { default: PageComponent } = await import(page.component); // Render the page with its metadata dom.window.document.body.append( el(PageComponent, { title: page.title, pages }) ); // Wait for any async operations await queue(); // Write the HTML to a file const html = dom.serialize(); writeFileSync(`./dist/docs/${page.id}.html`, html); console.log(`Built page: ${page.id}.html`); } } buildSite().catch(console.error);

# Working with Async Content in SSR

The jsdom export includes a queue system to handle asynchronous operations during rendering. This is crucial for components that fetch data or perform other async tasks.

// Handling async data in SSR import { JSDOM } from "jsdom"; import { register, queue } from "deka-dom-el/jsdom"; async function renderWithAsyncData() { const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>"); const { el } = await register(dom); // Create a component that fetches data const { AsyncComponent } = await import("./components/AsyncComponent.js"); // Render the page dom.window.document.body.append( el("h1", "Page with Async Data"), el(AsyncComponent) ); // IMPORTANT: Wait for all queued operations to complete await queue(); // Now the HTML includes all async content const html = dom.serialize(); console.log(html); } renderWithAsyncData(); // file: components/AsyncComponent.js import { el } from "deka-dom-el"; import { S } from "deka-dom-el/signals"; function AsyncComponent() { const title= S("-"); const description= S("-"); // Use the queue to track the async operation queue(fetch("https://api.example.com/data") .then(response => response.json()) .then(data => { title.set(data.title); description.set(data.description); })); return el("div", { className: "async-content" }).append( el("h2", title), el("p", description) ); }

# Working with Dynamic Imports for SSR

When structuring server-side rendering code, a crucial pattern to follow is using dynamic imports for both the deka-dom-el/jsdom module and your page components.

Why is this important?

Follow this pattern when creating server-side rendered pages:

// ❌ WRONG: Static imports are hoisted and will register before JSDOM is created import { register } from "deka-dom-el/jsdom"; import { el } from "deka-dom-el"; import { Header } from "./components/Header.js"; // ✅ CORRECT: Use dynamic imports to ensure proper initialization order import { JSDOM } from "jsdom"; async function renderPage() { // 1. Create JSDOM instance first const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`); // 2. Dynamically import jsdom module const { register, queue } = await import("deka-dom-el/jsdom"); // 3. Register and get el function const { el } = await register(dom); // 4. Dynamically import page components // use `import { el } from "deka-dom-el"` const { Header } = await import("./components/Header.js"); const { Content } = await import("./components/Content.js"); // 5. Render components const body = dom.window.document.body; el(body).append( el(Header, { title: "My Page" }), el(Content, { text: "This is server-rendered content" }) ); // 6. Wait for async operations await queue(); // 7. Get HTML and clean up return dom.serialize(); }

# SSR Considerations and Limitations

When using dd<el> for SSR, keep these considerations in mind:

For advanced SSR applications, consider implementing hydration on the client-side to restore interactivity after the initial render.

# Real Example: How This Documentation is Built

This documentation site itself is built using dd<el>’s SSR capabilities. The build process collects all page components, renders them with jsdom, and outputs static HTML files.

// Building a simple static site generator import { JSDOM } from "jsdom"; import { register, queue } from "deka-dom-el/jsdom"; import { writeFileSync, mkdirSync } from "node:fs"; async function buildSite() { // Define pages to build const pages = [ { id: "index", title: "Home", component: "./pages/home.js" }, { id: "about", title: "About", component: "./pages/about.js" }, { id: "docs", title: "Documentation", component: "./pages/docs.js" } ]; // Create output directory mkdirSync("./dist/docs", { recursive: true }); // Build each page for (const page of pages) { // Create a fresh jsdom instance for each page const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>"); // Register with deka-dom-el const { el } = await register(dom); // Import the page component // use `import { el } from "deka-dom-el"` const { default: PageComponent } = await import(page.component); // Render the page with its metadata dom.window.document.body.append( el(PageComponent, { title: page.title, pages }) ); // Wait for any async operations await queue(); // Write the HTML to a file const html = dom.serialize(); writeFileSync(`./dist/docs/${page.id}.html`, html); console.log(`Built page: ${page.id}.html`); } } buildSite().catch(console.error);

The resulting static files can be deployed to any static hosting service, providing fast loading times and excellent SEO without the need for client-side JavaScript to render the initial content.