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.)
Those two fields are a RoutePaths, which is the shape this answers in: paths ready to render,
and needsData for the patterns it cannot enumerate.
renderDocument and DocumentOptions
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" |
scripts | module scripts at the end of <body> — your hydration entry |
styles | stylesheet links |
headExtra | raw markup for anything it doesn't model (favicon, analytics). Not escaped |
rootId | the element the app mounts into, default "app" |
Those options are DocumentOptions. It takes a RenderedPage — what renderPage hands back:
the app's body, the title and head a Head hook set, and portals, which is what each named
portal target collected, kept apart from head because they land somewhere
else in the document.
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.
lang defaults to "en", and that is a default rather than a recommendation — a screen reader
picks pronunciation from it and a search engine decides which audience the page is for.
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.)