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:
- happy-dom — A JavaScript implementation of a web browser without its graphical user interface that’s faster than jsdom
- linkedom — A lightweight DOM implementation specifically designed for SSR with significantly better performance than jsdom
// 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:
- Improved SEO — Search engines can easily index fully rendered content
- Faster initial page load — Users see content immediately without waiting for JavaScript to load
- Better performance on low-powered devices — Less JavaScript processing on the client
- Content available without JavaScript — Useful for users who have disabled JavaScript
- Static site generation — Build files once, serve them many times
# 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:
- Creates a virtual DOM environment in Node.js using jsdom
- Registers DOM globals like HTMLElement, document, etc. for dd<el> to use
- Sets an SSR flag in the environment to enable SSR-specific behaviors
- Provides a promise queue system for managing async operations during rendering
- 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?
- Static imports are hoisted: JavaScript hoists import statements to the top of the file, executing them before any other code
- Environment registration timing: The jsdom module auto-registers the DOM environment when imported, which must happen after you’ve created your JSDOM instance and before you import your components using
import { el } from "deka-dom-el";
. - Correct initialization order: You need to control the exact sequence of: create JSDOM → register environment → import components
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:
- Browser-specific APIs like window.localStorage are not available in jsdom by default
- Event listeners added during SSR won’t be functional in the final HTML unless hydrated on the client
- Some DOM features may behave differently in jsdom compared to real browsers
- For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page
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.