Frontend
Synsema serves HTML from the server (SSR) — no imposed framework or CSS. Two paths: render() templates (full design control) and content() (agent-negotiable).
-- Doc example: render() templates. HTML with { holes }, { each } loops, auto-escaped.
-- Self-contained: writes a tiny template, then renders it (so it runs anywhere, sandboxed).
intent: "doc example: frontend render()"
require file.write("_doctest_card.html")
require file.read("_doctest_card.html")
write_file("_doctest_card.html", "<div class=\"card\"><h2>{ title }</h2><ul>{ each tag in tags }<li>{ tag }</li>{ end }</ul></div>")
print(body of render("_doctest_card.html", {"title": "Synsema", "tags": ["fast", "secure"]}))
test "render fills holes, loops, and HTML-escapes by default (XSS-safe)"
let html be body of render("_doctest_card.html", {"title": "A <b> tag", "tags": ["x", "y"]})
assert(contains(html, "<h2>A <b> tag</h2>")) -- auto-escaped
assert(contains(html, "<li>x</li><li>y</li>")) -- { each } loop
render() — free-form templates
render("page.html", data) returns an HTML response; body of render(...) is the string. Templates are HTML with { ... } holes:
route "GET /"
give render("pages/home.html", {"title": "My App", "items": items})
{ name }interpolates (HTML-escaped);{ raw html }opts out.{ each x in xs } … { end }and{ when c } … { otherwise } … { end }reuse Synsema flow.- Compose:
{ include "partials/nav.html" }and{ layout "layouts/base.html" }with{ slot }.
Render shared partials once at startup: let nav be body of render("partials/nav.html", {}).
content() — agent-negotiable pages
Build a semantic tree once; the runtime serves HTML to humans and Markdown/JSON to agents (by Accept header or a .md/.json suffix) — ideal for docs/blogs an LLM should read.
route "GET /docs/:slug"
give content(page([heading(1, "Title"), prose("…"), code("let x be 1", "synsema")], {
"title": "Title", "description": "…", "stylesheet": "/assets/app.css"
}))
Static assets & client JS
The default is a static mount: static "/assets" from "./static" serves your CSS/JS/images with ETag, Range and gzip. The client is unrestricted — vanilla JS, htmx, or any framework.
Gotcha — declared routes beat static mounts. If your app has wildcard routes (e.g. GET /:lang/:slug), a static mount is never reached, so you serve the assets from a declared route instead. Serve text (css/js/svg) with respond, and binary (images, fonts) with read_file_bytes + binary — plain read_file decodes UTF-8 and would corrupt a PNG:
route "GET /assets/*path"
let p be "static/" + params.path
when not file_exists(p)
give not_found("asset not found")
when ends_with(p, ".png")
give binary(read_file_bytes(p), "image/png") -- byte-exact
give respond(read_file(p), "text/css; charset=utf-8")
This very docs site is built exactly this way: its wildcard routes (/:lang/:version/:slug) force a declared /assets/*path route, and the Open Graph preview image is served with binary(read_file_bytes(...), "image/png").