Server-Side Pre-Rendering and Client-Side Rehydration

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>.

# What Are Ireland Components?

Ireland components are a special type of component that:

# How Ireland Components Work

The Ireland component system consists of several parts working together:

  1. Server-side rendering: Components are pre-rendered during the documentation build process
  2. Component registration: Source files are copied to the documentation output directory
  3. Client-side scripting: JavaScript code is generated to load and render components

# Implementation Architecture

The core of the Ireland system is implemented in docs/components/ireland.html.js. It integrates with the SSR build process using the registerClientFile function from docs/ssr.js.

// Basic usage of an ireland component el(ireland, { src: fileURL("./components/examples/path/to/component.js"), exportName: "NamedExport", // optional, defaults to "default", })

During the build process (bs/docs.js), the following happens:

  1. Component source code is loaded and displayed with syntax highlighting
  2. Source files are registered to be copied to the output directory
  3. Client-side scripts are generated for each page with ireland components
  4. The component is rerendered on the page ready

# Core Implementation Details

Let's look at the key parts of the ireland component implementation:

Building SSR

// From bs/docs.js - Server-side rendering engine import { createHTMl } from "./docs/jsdom.js"; import { register, queue } from "../jsdom.js"; import { path_target, dispatchEvent } from "../docs/ssr.js"; // For each page, render it on the server for(const { id, info } of pages) { // Create a virtual DOM environment for server-side rendering const serverDOM = createHTMl(""); serverDOM.registerGlobally("HTMLScriptElement"); // Register dd<el> with the virtual DOM const { el } = await register(serverDOM.dom); // Import and render the page component const { page } = await import(`../docs/${id}.html.js`); serverDOM.document.body.append( el(page, { pkg, info }), ); // Process the queue of asynchronous operations await queue(); // Trigger render event handlers dispatchEvent("oneachrender", document); // Write the HTML to the output file s.echo(serverDOM.serialize()).to(path_target.root+id+".html"); } // Final build step - trigger SSR end event dispatchEvent("onssrend");

File Registration

// From docs/ssr.js - File registration system export function registerClientFile(url, { head, folder = "", replacer } = {}) { // Ensure folder path ends with a slash if(folder && !folder.endsWith("/")) folder += "/"; // Extract filename from URL const file_name = url.pathname.split("/").pop(); // Create target directory if needed s.mkdir("-p", path_target.root+folder); // Get file content and apply optional replacer function let content = s.cat(url); if(replacer) content = s.echo(replacer(content.toString())); // Write content to the output directory content.to(path_target.root+folder+file_name); // If a head element was provided, add it to the document if(!head) return; head[head instanceof HTMLScriptElement ? "src" : "href"] = file_name; document.head.append(head); }

Server-Side Rendering

// From docs/components/ireland.html.js - Server-side component implementation export function ireland({ src, exportName = "default", props = {} }) { // Calculate relative path for imports const path = "./"+relative(dir, src.pathname); // Generate unique ID for this component instance const id = "ireland-" + generateComponentId(src); // Create placeholder element const element = el.mark({ type: "later", name: ireland.name }); // Import and render the component during SSR queue(import(path).then(module => { const component = module[exportName]; element.replaceWith(el(component, props, mark(id))); })); // Register client-side hydration on first component if(!componentsRegistry.size) addEventListener("oneachrender", registerClientPart); // Store component info for client-side hydration componentsRegistry.set(id, { src, path: dirFE+"/"+path.split("/").pop(), exportName, props, }); return element; } // Register client-side resources function registerClientPart() { // Process all component registrations const todo = Array.from(componentsRegistry.entries()) .map(([ id, d ]) => { // Copy the component source file to output directory registerClientFile(d.src, { folder: dirFE, // Replace bare imports for browser compatibility replacer(file) { return file.replaceAll( / from "deka-dom-el(/signals)?";/g, ` from "./esm-with-signals.js";` ); } }); return [ id, d ]; }); // Serialize the component registry for client-side use const store = JSON.stringify(JSON.stringify(todo)); // Copy client-side scripts to output registerClientFile(new URL("./ireland.js.js", import.meta.url)); registerClientFile(new URL("../../dist/esm-with-signals.js", import.meta.url), { folder: dirFE }); // Add import map for package resolution document.head.append( el("script", { type: "importmap" }).append(` { "imports": { "deka-dom-el": "./${dirFE}/esm-with-signals.js", "deka-dom-el/signals": "./${dirFE}/esm-with-signals.js" } } `.trim()) ); // Add bootstrap script to load components document.body.append( el("script", { type: "module" }).append(` import { loadIrelands } from "./ireland.js.js"; loadIrelands(new Map(JSON.parse(${store}))); `.trim()) ); }

Client-Side Hydration

// From docs/components/ireland.js.js - Client-side hydration import { el } from "./irelands/esm-with-signals.js"; export function loadIrelands(store) { // Find all marked components in the DOM document.body.querySelectorAll("[data-dde-mark]").forEach(ireland => { const { ddeMark } = ireland.dataset; // Skip if this component isn’t in our registry if(!store.has(ddeMark)) return; // Get component information const { path, exportName, props } = store.get(ddeMark); // Dynamically import the component module import("./" + path).then(module => { // Replace the server-rendered element with the client-side version ireland.replaceWith(el(module[exportName], props)); }); }); }

# Live Example

Here’s a live example of an Ireland component showing a standard counter. The component is defined in docs/components/examples/ireland-test/counter.js and rendered with the Ireland component system:

import { el } from "deka-dom-el"; import { S } from "deka-dom-el/signals"; const className = "client-side-counter"; document.body.append( el("style").append(` .${className} { border: 1px dashed #ccc; padding: 1em; margin: 1em; } `.trim()) ); export function CounterStandard() { // Create reactive state with a signal const count = S(0); // Create UI components that react to state changes return el("div", { className }).append( el("h4", "Client-Side Counter"), el("div", { // The textContent updates automatically when count changes textContent: S(() => `Count: ${count.get()}`), }), el("div", { className: "controls" }).append( el("button", { onclick: () => count.set(count.get() - 1), textContent: "-", }), el("button", { onclick: () => count.set(count.get() + 1), textContent: "+", }) ) ); }

Client-Side Counter

Count: 0

When the page is loaded, the component is also loaded and rendered. The counter state is maintained using signals, allowing for reactive updates as you click the buttons to increment and decrement the value.

# Practical Considerations and Limitations

When implementing Ireland components in real documentation, there are several important considerations to keep in mind:

Module Resolution and Bundling

The examples shown here use bare module specifiers like import { el } from "deka-dom-el" which aren’t supported in all browsers without importmaps. In a production implementation, you would need to:

  1. Replace bare import paths with actual paths during the build process
  2. Bundle component dependencies to avoid multiple requests
  3. Ensure all module dependencies are properly resolved and copied to the output directory

In this documentation, we replace the paths with ./esm-with-signals.js and provide a bundled version of the library, but more complex components might require a dedicated bundling step.

Component Dependencies

Real-world components typically depend on multiple modules and assets. The Ireland system would need to be extended to:

  • Detect and analyze all dependencies of a component
  • Bundle these dependencies together or ensure they're properly copied to the output directory
  • Handle non-JavaScript assets like CSS, images, or data files

# Advanced Usage

The Ireland system can be extended in several ways to address these limitations:

This documentation site itself is built using the techniques described here, showcasing how dd<el> can be used to create both the documentation and the interactive examples within it. The implementation here is simplified for clarity, while a production-ready system would need to address the considerations above.