SERVER
Server rendering (ssr)
amo build ssg renders pages when you build. To render them when a request arrives, build with ssr: pages become server modules that render to strings on node, src/islands/ is compiled for the browser in the same run, and amo serve ssr maps urls to page modules and renders each request.
Run it
$ amo build ssr
$ amo serve ssr
amo serve ssr maps / to dist/pages/index.js, /about to dist/pages/about.js, /docs/x to dist/pages/docs/x.js or docs/x/index.js. Static files are served for islands, _amo/ and public assets; every other compiled module is server code and is never handed to a browser.
A page receives the request
A page is (props) => template. The server passes { url } — a URL, so url.pathname and url.searchParams are per-request data. (amo build ssg calls the same page with {}; a page that uses url is an ssr page — that is the whole difference.)
// src/pages/product.js — the same shape as a static page, with props this time
export default async ({ url }) => {
const product = await db.find(url.searchParams.get('id'));
return html`<html lang="en">
<head><title>${product.name} — Shop</title></head>
<body>
<h1>${product.name}</h1>
<div id="cart" data-stock="${String(product.stock)}"></div>
<script type="module" src="/islands/cart.js"></script>
</body>
</html>`;
};
Your own server
You never have to use amo serve. A template stringifies to its HTML, so the entire API is one await, and no AmoJS package is in the request path at all — the compiler is a build-time tool.
import { createServer } from 'node:http';
const page = (await import('./dist/pages/product.js')).default;
createServer(async (req, res) => {
const url = new URL(req.url, 'http://x');
res.setHeader('content-type', 'text/html; charset=utf-8');
res.end('<!doctype html>\n' + (await page({ url }))); // ← the whole of SSR
}).listen(3000);
amo serve ssr is that same loop with static-file serving around it: a convenience, not a dependency.
Four things before you deploy
Serve the client build to the browser, never the server one. A mix-up cannot pass silently in either direction: DOM-target code calls document.createElement and node throws, and a server-only template fails the client build. A dynamic <title> works on the server only — the client answer is document.title. All islands on a page share one core url, which the islands pass guarantees. There is no hydration and never will be: the server sends static HTML, islands build their own DOM, and state does not survive a navigation — a link is a real document load, as in any static site.