SERVER
Static sites (ssg)
Static pages from the same components — islands, never hydration. Every module under src/pages/ is a page: its default export returns a template, which is rendered on node to dist/<same path>.html. A page with no interactivity ships zero script bytes; this page is one.
A page module
// src/pages/index.js → dist/index.html
import { html } from '@amojs.dev/core';
import { head } from '../lib/head.js';
export default () => html`<html lang="en">
<head>${head()}<title>hi</title></head>
<body><h1>${'hello'}</h1></body>
</html>`;
Signals evaluate once, text and attribute holes are escaped, no comment markers are emitted. The build prepends <!doctype html>. A page is called with {} — the same calling convention as request-time rendering, where it receives the request's props instead.
Data at build time
The default export may be async; read files, query an API, whatever node can do. Render lists with .map() — each() is a client-side reconciler and throws on the server.
// a page may be async: the render waits
export default async () => {
const posts = await readPosts();
return html`<html lang="en"><body>
<ul>${posts.map((p) => html`<li><a href="${p.url}">${p.title}</a></li>`)}</ul>
</body></html>`;
};
What goes where
src/
pages/index.js → dist/index.html
pages/docs/index.js → dist/docs/index.html (/docs/ on any static host)
pages/docs/signal.js → dist/docs/signal.html (/docs/signal only where the host maps it)
islands/cart.js → dist/islands/cart.js (compiled for the browser)
lib/… server code — never reaches dist/
styles/site.css → dist/styles/site.css (copied verbatim)
public/favicon.svg → dist/favicon.svg (served from the root)
Non-module files are copied to the same relative path; modules under lib/ are server code and never ship. Write pages as pages/x/index.js when the host is a dumb static server — x/index.html answers /x/ everywhere, while x.html answers /x only where the host maps it (amo serve and amo dev do).
Adding an island
An island is nothing special: a static <script type="module"> you write in the page that imports a normal AmoJS component and mounts it into its container. If src/islands/ exists, the build compiles it for the browser and vendors core next to it; no import map needed. Give the container intrinsic size — it is empty until the island mounts.
<!-- in a page: a container with its data, and the script that mounts into it -->
<div id="cart" data-stock="${String(product.stock)}"></div>
<script type="module" src="/islands/cart.js"></script>
Server data reaches an island through data-* attributes on the container, not through the script: <script> content is static (a hole in it is a build error), and interpolating server data into a script is an injection vector anyway.
Server-side notes
Stated rather than hidden: ref holes are skipped (no element exists to hand over); a value hole on <textarea> renders as its content; value on <select> is a build error — put selected=${…} on the matching option; a dynamic <title>${…}</title> works on the server only, which is where a crawler needs it.