Routing

Routes come straight from the filesystem under src/.

src/
  index.ht.js              → /
  about.ht.js              → /about
  blog/
    index.ht.js            → /blog
    [slug].ht.js           → /blog/:slug
  docs/
    [...path]?.ht.js       → /docs, /docs/a, /docs/a/b, ...
  (admin)/
    users.ht.js            → /users
  404.ht.js                → dist/404.html

Route table

FeatureFileURL
Staticindex.ht.js/
Nestedblog/index.ht.js/blog
Dynamicblog/[slug].ht.js/blog/my-post
Multiple paramsblog/[year]/[slug].ht.js/blog/2026/my-post
Catch-alldocs/[...path].ht.js/docs/api/auth
Optional catch-alldocs/[...path]?.ht.js/docs + deeper
Route groups(admin)/users.ht.js/users

More specific routes win: static beats dynamic, dynamic beats catch-alls. Two files generating the same URL is a build error.

generateStaticParams

Dynamic routes declare which pages to emit at build time. In sitelo (dev), dynamic routes still render on demand without listing every param.

import { html, body, h1 } from 'javascript-to-html'

export function generateStaticParams() {
  return [
    { slug: 'hello-world' },
    { slug: 'my-first-post' },
  ]
}

export default ({ params }) =>
  html(
    body(h1(params.slug))
  )

Values can be strings, numbers, or booleans — they are stringified and URL-encoded. Catch-all params accept arrays ({ path: ['a', 'b'] }) or slash-separated strings ({ path: 'a/b' }).

A dynamic page that generates zero routes prints a warning so it cannot silently vanish from your site.