Ramonda

Building a static site

A static build renders every page to an HTML file at build time. The whole loop is a few lines:

import { renderPage, renderDocument } from "@ramonda/core";
import { routePaths } from "@ramonda/router";

const { paths, needsData } = routePaths(routes);

for (const path of paths) {
  window.history.pushState(null, "", path);
  const page = await renderPage(<App />);
  write(path, renderDocument(page, { scripts: ["/assets/client.js"] }));
}

This site is built exactly like that.

routePaths — the pages it can list, and the ones it can't

A route table is a set of patterns, and only some are single pages. /guide is one page; /players/:id is one route but as many pages as there are players.

const { paths, needsData } = routePaths(routes, ["/players/1", "/players/2"]);

needsData is the list of patterns whose values live in your data — you supply the concrete paths (the second argument). A build that ignored needsData would silently ship a site missing everything dynamic, so it is worth failing on:

if (needsData.length && extra.length === 0) {
  throw new Error(`These routes need concrete paths: ${needsData.join(", ")}`);
}

("*" is left out — it matches what nothing else did, so it has no URL. Render a 404 page explicitly at whatever path your host serves for missing files.)

renderDocument

Wraps a rendered page in a complete HTML document — doctype, charset, viewport, title, the page's own head, a root element, your stylesheets and scripts.

option
lang<html lang>, default "en"
scriptsmodule scripts at the end of <body> — your hydration entry
stylesstylesheet links
headExtraraw markup for anything it doesn't model (favicon, analytics). Not escaped
rootIdthe element the app mounts into, default "app"

It is deliberately small: the document shell is the one part every project wants slightly differently, so it does the parts that are always the same and leaves the rest to headExtra.

Your bundler needs two builds

A client bundle (which calls hydrateRoot) and a server bundle for the build loop. Node can't parse TC39 decorators, so the build script runs transpiled output — a dev server's on-the-fly transform won't do. (And if you use AsyncLoad, split both bundles — see lazy loading.)

Next