diff --git a/docs/components/examples/ssr/async-data.js b/docs/components/examples/ssr/async-data.js new file mode 100644 index 0000000..f10523b --- /dev/null +++ b/docs/components/examples/ssr/async-data.js @@ -0,0 +1,43 @@ +// Handling async data in SSR +import { JSDOM } from "jsdom"; +import { S } from "deka-dom-el/signals"; +import { register, queue } from "deka-dom-el/jsdom"; + +async function renderWithAsyncData() { + const dom = new JSDOM(""); + const { el } = await register(dom); + + // Create a component that fetches data + 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) + ); + } + + // 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(); diff --git a/docs/components/examples/ssr/basic-example.js b/docs/components/examples/ssr/basic-example.js new file mode 100644 index 0000000..e01bb80 --- /dev/null +++ b/docs/components/examples/ssr/basic-example.js @@ -0,0 +1,47 @@ +// 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(""); + + // Register with deka-dom-el and get the el function + const { el } = await register(dom); + + // Create a simple header component + 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); diff --git a/docs/components/examples/ssr/intro.js b/docs/components/examples/ssr/intro.js new file mode 100644 index 0000000..643c2a8 --- /dev/null +++ b/docs/components/examples/ssr/intro.js @@ -0,0 +1,2 @@ +// 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"; diff --git a/docs/components/examples/ssr/pages.js b/docs/components/examples/ssr/pages.js new file mode 100644 index 0000000..9d92d21 --- /dev/null +++ b/docs/components/examples/ssr/pages.js @@ -0,0 +1,35 @@ +// ❌ 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(``); + + // 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 + 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(); +} diff --git a/docs/components/examples/ssr/start.js b/docs/components/examples/ssr/start.js new file mode 100644 index 0000000..787c03d --- /dev/null +++ b/docs/components/examples/ssr/start.js @@ -0,0 +1,27 @@ +// Basic jsdom integration example +import { JSDOM } from "jsdom"; +import { register, unregister, queue } from "deka-dom-el/jsdom.js"; + +// Create a jsdom instance +const dom = new JSDOM(""); + +// 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(); diff --git a/docs/components/examples/ssr/static-site-generator.js b/docs/components/examples/ssr/static-site-generator.js new file mode 100644 index 0000000..a93925e --- /dev/null +++ b/docs/components/examples/ssr/static-site-generator.js @@ -0,0 +1,44 @@ +// 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", { recursive: true }); + + // Build each page + for (const page of pages) { + // Create a fresh jsdom instance for each page + const dom = new JSDOM(""); + + // Register with deka-dom-el + const { el } = await register(dom); + + // Import the page component + 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/${page.id}.html`, html); + + console.log(`Built page: ${page.id}.html`); + } +} + +buildSite().catch(console.error); diff --git a/docs/p08-ssr.html.js b/docs/p08-ssr.html.js new file mode 100644 index 0000000..2b86e2d --- /dev/null +++ b/docs/p08-ssr.html.js @@ -0,0 +1,129 @@ + import { T, t } from "./utils/index.js"; +export const info= { + title: t`Server-Side Rendering (SSR)`, + description: t`Using deka-dom-el for server-side rendering with jsdom to generate static HTML.`, +}; + +import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; +import { h3 } from "./components/pageUtils.html.js"; +import { code } from "./components/code.html.js"; +/** @param {string} url */ +const fileURL= url=> new URL(url, import.meta.url); + +/** @param {import("./types.d.ts").PageAttrs} attrs */ +export function page({ pkg, info }){ + const page_id= info.id; + return el(simplePage, { info, pkg }).append( + el("h2", t`Server-Side Rendering with deka-dom-el`), + el("p").append(...T` + deka-dom-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 ${el("a", { href: "https://github.com/tmpvar/jsdom", + textContent: "jsdom" })}, a JavaScript implementation of web standards for Node.js. + `), + el(code, { src: fileURL("./components/examples/ssr/intro.js"), page_id }), + + el(h3, t`Why Server-Side Rendering?`), + el("p").append(...T` + SSR offers several benefits: + `), + el("ul").append( + el("li", t`Improved SEO - Search engines can easily index fully rendered content`), + el("li", t`Faster initial page load - Users see content immediately without waiting for JavaScript to load`), + el("li", t`Better performance on low-powered devices - Less JavaScript processing on the client`), + el("li", t`Content available without JavaScript - Useful for users who have disabled JavaScript`), + el("li", t`Static site generation - Build files once, serve them many times`) + ), + + el(h3, t`How jsdom Integration Works`), + el("p").append(...T` + The jsdom export in deka-dom-el provides the necessary tools to use the library in Node.js + by integrating with jsdom. Here's what it does: + `), + el("ol").append( + el("li", t`Creates a virtual DOM environment in Node.js using jsdom`), + el("li", t`Registers DOM globals like HTMLElement, document, etc. for deka-dom-el to use`), + el("li", t`Sets an SSR flag in the environment to enable SSR-specific behaviors`), + el("li", t`Provides a promise queue system for managing async operations during rendering`), + el("li", t`Handles DOM property/attribute mapping differences between browsers and jsdom`) + ), + el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }), + + el(h3, t`Basic SSR Example`), + el("p").append(...T` + Here's a simple example of how to use deka-dom-el for server-side rendering in a Node.js script: + `), + el(code, { src: fileURL("./components/examples/ssr/basic-example.js"), page_id }), + + el(h3, t`Building a Static Site Generator`), + el("p").append(...T` + You can build a complete static site generator with deka-dom-el. In fact, this documentation site + is built using deka-dom-el for server-side rendering! Here's how the documentation build process works: + `), + el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }), + + el(h3, t`Working with Async Content in SSR`), + el("p").append(...T` + 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. + `), + el(code, { src: fileURL("./components/examples/ssr/async-data.js"), page_id }), + + el(h3, t`Working with Dynamic Imports for SSR`), + el("p").append(...T` + 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. + `), + el("p").append(...T` + Why is this important? + `), + el("ul").append( + el("li").append(...T` + ${el("strong", "Static imports are hoisted:")} JavaScript hoists import statements to the top of the file, + executing them before any other code + `), + el("li").append(...T` + ${el("strong", "Environment registration timing:")} The jsdom module auto-registers the DOM environment + when imported, which must happen ${el("em", "after")} you've created your JSDOM instance and + ${el("em", "before")} you import your components using ${el("code", "import { el } from \"deka-dom-el\";")}. + `), + el("li").append(...T` + ${el("strong", "Correct initialization order:")} You need to control the exact sequence of: + create JSDOM → register environment → import components + `) + ), + el("p").append(...T` + Follow this pattern when creating server-side rendered pages: + `), + el(code, { src: fileURL("./components/examples/ssr/pages.js"), page_id }), + + el(h3, t`SSR Considerations and Limitations`), + el("p").append(...T` + When using deka-dom-el for SSR, keep these considerations in mind: + `), + el("ul").append( + el("li", t`Browser-specific APIs like window.localStorage are not available in jsdom by default`), + el("li", t`Event listeners added during SSR won't be functional in the final HTML unless hydrated on the client`), + el("li", t`Some DOM features may behave differently in jsdom compared to real browsers`), + el("li", t`For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page`) + ), + el("p").append(...T` + For advanced SSR applications, consider implementing hydration on the client-side to restore + interactivity after the initial render. + `), + + el(h3, t`Real Example: How This Documentation is Built`), + el("p").append(...T` + This documentation site itself is built using deka-dom-el's SSR capabilities. + The build process collects all page components, renders them with jsdom, and outputs static HTML files. + `), + el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }), + + el("p").append(...T` + 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. + `), + ); +}