Frontend
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). Para el recorrido completo (layout, estilos, formularios, páginas de error), mirá Construí un sitio web.
-- Doc example: render() templates. HTML with { holes }, loops with an empty branch,
-- chained conditionals, VERBATIM blocks for inline CSS/JS, components with props,
-- named slots, and safe JSON-for-<script>. Auto-escaped by default (XSS-safe).
-- Self-contained: writes tiny templates, then renders them (runs anywhere, sandboxed).
intent: "doc example: frontend render()"
require file.write("_doctest_card.html")
require file.read("_doctest_card.html")
require file.write("_doctest_page.html")
require file.read("_doctest_page.html")
require file.write("_doctest_base.html")
require file.read("_doctest_base.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
test "verbatim { raw } block: inline CSS/JS with literal braces"
write_file("_doctest_page.html", "<style>{ raw }.card { color: red; }{ end }</style>{ when n == 1 }one{ otherwise when n == 2 }two{ otherwise }many{ end }")
let html be body of render("_doctest_page.html", {"n": 2})
assert(contains(html, ".card { color: red; }")) -- braces intact (no holes inside)
assert(contains(html, "two")) -- { otherwise when } chains
test "each empty branch + enumerate for indexes"
write_file("_doctest_page.html", "{ each e in enumerate(xs) }[{ e.index }:{ e.item }]{ otherwise }empty{ end }")
assert_eq(body of render("_doctest_page.html", {"xs": ["a", "b"]}), "[0:a][1:b]")
assert_eq(body of render("_doctest_page.html", {"xs": []}), "empty")
test "include with props = a component (isolated scope)"
write_file("_doctest_card.html", "<b>{ label }</b>")
write_file("_doctest_page.html", "{ include \"_doctest_card.html\" with {\"label\": item} }")
assert_eq(body of render("_doctest_page.html", {"item": "Props!"}), "<b>Props!</b>")
test "layout + named slot: { slot \"x\" } filled by { fill \"x\" }"
write_file("_doctest_base.html", "<head>{ slot \"extra\" }</head><main>{ slot }</main>")
write_file("_doctest_page.html", "{ layout \"_doctest_base.html\" }{ fill \"extra\" }<meta n=\"{ t }\">{ end }<h1>{ t }</h1>")
let html be body of render("_doctest_page.html", {"t": "Hi"})
assert(contains(html, "<head><meta n=\"Hi\"></head>"))
assert(contains(html, "<main>") and contains(html, "<h1>Hi</h1>"))
test "json_for_script: data into an inline <script> without XSS"
let js be json_for_script([{"name": "a</script>"}])
assert(contains(js, "a\\u003c/script\\u003e")) -- cannot close the tag
assert(not contains(js, "</script>"))
-- json_encode is for APIs/storage; json_for_script is for <script> embedding.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 }/{ expr }interpolan (HTML-escapeado; las expresiones llaman builtins y tus propias tasks — definí las tasks antes del bloqueserve);{ raw expr }lo desactiva para HTML confiable.{ raw }…{ end }es un bloque VERBATIM — CSS/JS inline con llaves literales:<style>{ raw } .card { color: red; } { end }</style>.{ each x in xs } … { otherwise } … { end }— loop con rama de lista vacía opcional; índices conenumerate(xs)({ e.index }/{ e.item }). Una no-lista es error fuerte.{ when c } … { otherwise when c2 } … { otherwise } … { end }— encadenado, como en el lenguaje.- Componé:
{ include "partials/nav.html" }(scope actual) o{ include "partials/card.html" with {"title": t} }(un componente con props aislados);{ layout "layouts/base.html" }con{ slot }, más slots nombrados —{ slot "head_extra" }en el layout,{ fill "head_extra" }…{ end }en la página. { -- comentario }no emite nada. Los paths de template (incluidosinclude/layout) se resuelven contra el directorio de trabajo.- Datos en un
<script>inline:{ raw json_for_script(x) }— jamásjson_encodeahí (un valor con</script>rompería el tag).
Los templates se cachean parseados y se recargan en caliente por request (editar → refrescar, sin reiniciar); los paths de render("literal.html") se validan al arranque y con synsema check. Para el .syn, synsema serve app.syn --watch reinicia al cambiar.
content() — páginas negociables por agentes§
Construís el á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§
El default es un mount estático con comportamiento de producción (ETag/304, Range, gzip) más tu política de cache y soporte SPA:
static "/assets" from "./static" cache "1h" -- Cache-Control por mount ("immutable" para fingerprinteados)
static "/app" from "./dist" fallback "index.html" -- history-fallback de SPA (try_files)
El cliente no tiene restricciones — vanilla JS, htmx o el framework que quieras. Un <form method="post"> clásico llega como form of request (urlencoded y multipart, archivos como bytes exactos) — sin fetch ni JSON. Las respuestas dinámicas (render/html/content/JSON) se comprimen con gzip automáticamente.
Gotcha — las rutas declaradas le ganan a los mounts static. Si tu app tiene rutas wildcard (p.ej. GET /:lang/:slug), un 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 — un read_file plano 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á construido exactamente así: sus rutas wildcard (/:lang/:version/:slug) fuerzan una ruta declarada /assets/*path, y la imagen de preview Open Graph se sirve con binary(read_file_bytes(...), "image/png").
Páginas de error y rutas en módulos§
errors with <task>en el bloque serve da forma a 401/404/405/500 — páginas HTML para navegadores, el JSON default para agentes,redirect("/login")para un 401. El status del error siempre se conserva (nada de soft-404). Mirá Servidor HTTP.- Las rutas de un sitio pueden vivir en módulos:
export routes shopenshop.syn, ymount shop.shop(opcionalmenteat "/store") en el serve — los cuerpos llaman helpers privados del módulo directo. Mirá Módulos.
Estructura de proyecto sugerida§
app.syn bloque serve: mounts, static, errors with
shop.syn un módulo con `export routes`
layouts/ base.html (chrome con { slot } / { slot "head_extra" })
partials/ nav.html, card.html (componentes; card recibe props vía `with`)
pages/ home.html, … (usan un layout + { fill })
static/ app.css, app.js, img/
Charts§
Los charts SVG server-side y los nodos chart() legibles por agentes vienen incluidos — mirá CSV, stats & charts.