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:
- Pre-render components on the server during SSR build
- Provide client-side rehydration for the component
# How Ireland Components Work
The Ireland component system consists of several parts working together:
- Server-side rendering: Components are pre-rendered during the documentation build process
- Component registration: Source files are copied to the documentation output directory
- 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:
- Component source code is loaded and displayed with syntax highlighting
- Source files are registered to be copied to the output directory
- Client-side scripts are generated for each page with ireland components
- 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
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:
- Replace bare import paths with actual paths during the build process
- Bundle component dependencies to avoid multiple requests
- 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:
- Integrate with a bundler like esbuild, Rollup, or Webpack
- Add props support for configuring components at runtime
- Implement module caching to reduce network requests
- Add code editing capabilities for interactive experimentation
- Support TypeScript and other languages through transpilation
- Implement state persistence between runs
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.