Frontend
Synsema sirve HTML desde el servidor (SSR) — sin framework ni CSS impuestos. Dos caminos: render() (control total del diseño) y content() (negociable por agentes).
-- 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() — templates libres
render("page.html", data) devuelve una respuesta HTML; body of render(...) es el string. Los templates son HTML con huecos { ... }:
route "GET /"
give render("pages/home.html", {"title": "My App", "items": items})
{ name }interpola (HTML-escapeado);{ raw html }lo desactiva.{ each x in xs } … { end }y{ when c } … { otherwise } … { end }reutilizan el flujo de Synsema.- Componé:
{ include "partials/nav.html" }y{ layout "layouts/base.html" }con{ slot }.
Renderizá los partials compartidos una vez al arrancar: let nav be body of render("partials/nav.html", {}).
content() — páginas negociables por agentes
Construí un árbol semántico una vez; el runtime sirve HTML a humanos y Markdown/JSON a agentes (por header Accept o sufijo .md/.json) — ideal para docs/blogs que un LLM deba leer.
route "GET /docs/:slug"
give content(page([heading(1, "Title"), prose("…"), code("let x be 1", "synsema")], {
"title": "Title", "description": "…", "stylesheet": "/assets/app.css"
}))
Assets estáticos y JS de cliente
Lo default es un mount estático: static "/assets" from "./static" sirve tu CSS/JS/imágenes con ETag, Range y gzip. El cliente no tiene restricciones — JS vanilla, htmx, o cualquier framework.
Ojo — las rutas declaradas ganan sobre los static mounts. Si tu app tiene rutas comodín (p. ej. GET /:lang/:slug), el mount static nunca se alcanza, así que servís los assets desde una ruta declarada. Serví texto (css/js/svg) con respond, y binario (imágenes, fuentes) con read_file_bytes + binary — el read_file a secas decodifica UTF-8 y corrompería un 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-exacto
give respond(read_file(p), "text/css; charset=utf-8")
Este mismo sitio de docs está hecho exactamente así: sus rutas comodín (/:lang/:version/:slug) obligan a una ruta declarada /assets/*path, y la imagen de preview Open Graph se sirve con binary(read_file_bytes(...), "image/png").