server

simple-server

One file serves the app (lib/simple-server.js), one file starts it (server.js), and public/ is the app. There is nothing to npm install: the server, its tests, the page build and the checks are plain Node. The repository ships a demo page (sscss + Minim, a request inspector, a live /api/health panel, a PWA that updates itself) that is also the shell a project replaces.

1. Serve a project

git clone https://github.com/hertonwork/server.git my-app && cd my-app
rm -rf public/* && cp -r ../dist/* public/      # your build; keep a 404.html if you want a styled not-found page
npm start                                        # http://127.0.0.1:2211

Configuration is environment variables, nothing else:

Variable Default  
PORT 2211  
HOST 127.0.0.1 0.0.0.0 in a container or to reach it from another machine
PUBLIC_DIR public  
VERBOSE off 1 logs one line per request
TLS_KEY, TLS_CERT key.pem, cert.pem when both files exist the server speaks HTTPS; npm run cert makes a localhost pair

Behind nginx or Caddy leave TLS off: the proxy terminates it and talks HTTP to 127.0.0.1:2211. Pass X-Forwarded-Proto through; HSTS is only set when the socket itself is TLS.

Or use it as a module, in your own process:

import { createServer } from './lib/simple-server.js';
createServer({
  publicDir: 'dist',
  maxAge: 3600,                                                    // assets that are not fingerprinted
  headers: { 'Permissions-Policy': 'camera=()', 'X-Frame-Options': null },   // add one, drop one
  routes: {
    'GET /api/time': () => ({ now: Date.now() }),                  // return an object → JSON, Cache-Control: no-store
    'POST /api/echo': (req, res) => { req.pipe(res); },            // or write the response yourself
  },
}).listen(2211);

createHandler(options) gives you just the (req, res) function. Every option and its default is at the top of lib/simple-server.js; the behaviour, with the reasons, is in the comment block above it.

What you get without asking

2. Build a page for it

The bundled page is the reference. It is built from site/ by scripts/build.mjs with three vendored files in vendor/ — Minim, the sscss generator, the Minim page preflight — so the build has no dependencies either.

Work in this order:

  1. State in site/index.html: one JSON object in <script type="application/json" data-state="app" data-root="root"> with every value the page shows, at its first-paint value.
  2. Markup with sscss classes and Minim bindings (data-watch, data-show + hidden, data-model, data-each, data-class, command="--inc"). Pre-render initial values so nothing moves when Minim binds. Follow the minim-page skill for the attributes and its references/design.md for the look; the sscss skill for the classes (node vendor/sscss.mjs --list prints the cheat sheet).
  3. Page-own CSS in the plain <style> in the head: type ramps, the .field section, inputs, token overrides. Short. Any class you invent there is “the page’s own”; any other unknown class fails the build.
  4. Script in site/app.js: a few dozen lines that fetch and write into app; Minim renders. Mark requests that must bypass the service worker with an x-probe header. Start work after load, in requestIdleCallback.
  5. npm run build → public/index.html, public/404.html, public/sw.js (its VERSION is a hash of everything precached, so an unchanged deploy leaves installed workers alone).
  6. npm run check → tests, build, preflight, Lighthouse mobile + desktop against this server (install lighthouse globally; set CHROME_PATH if Chrome isn’t found). Fix every line it prints. Commit site/ and public/ together.

The PWA

Publishing the page

.github/workflows/pages.yml uploads public/ to GitHub Pages on every push to main (Settings → Pages → Source “GitHub Actions”; the custom domain is set there too). Pages is a static host: /api/* is a 404 there, the page shows its static-host notes, and 404.html is served for unknown paths (including client-side routes — the SPA fallback is the server’s, not Pages’). The server itself runs wherever npm start runs.

3. Checklist before a PR

Pitfalls we hit