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

JS
// 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.

JS
// 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/ → dist/
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.

JS
<!-- 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.