Construí un sitio web
Un sitio real — layout, componentes con estilo, un formulario de contacto, páginas de error como corresponde — sin framework: tu HTML, tu CSS, vanilla JS si querés. El servidor, los templates, el parseo de formularios y el pipeline de estáticos vienen incluidos.
Los archivos
app.syn el bloque serve
shop.syn rutas que viven en un módulo (opcional, para sitios grandes)
layouts/base.html el chrome (head, nav, footer)
partials/nav.html piezas compartidas
partials/card.html un componente (recibe props)
pages/home.html la página
static/app.css tu stylesheet (se recarga en caliente por request)
El layout — layouts/base.html
Un shell para todas las páginas: un { slot } para el cuerpo y un slot nombrado para extras de <head> por página.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{ title }</title>
<link rel="stylesheet" href="/assets/app.css">
{ slot "head_extra" }
</head>
<body>
{ include "partials/nav.html" }
{ slot }
<footer>hecho con synsema</footer>
</body>
</html>
Un componente — partials/card.html
include … with pasa props; el partial ve solo eso (más tus tasks):
<article class="card"><h2>{ n }. { name }</h2><p>{ blurb }</p></article>
La página — pages/home.html
El CSS inline vive en un bloque verbatim { raw } (las llaves quedan literales); la grilla loopea con índice y estado vacío:
{ layout "layouts/base.html" }
{ fill "head_extra" }<style>{ raw }
.hero { padding: 4rem 2rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
.card { border-radius: 12px; padding: 1.25rem; background: #1a1d24;
transition: transform .2s ease, box-shadow .2s ease; }
.card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgb(0 0 0 / .4); }
{ end }</style>{ end }
<main class="hero">
<h1>{ title }</h1>
<div class="grid">
{ each e in enumerate(services) }
{ include "partials/card.html" with {"n": e.index + 1, "name": e.item.name, "blurb": e.item.blurb} }
{ otherwise }
<p>Muy pronto.</p>
{ end }
</div>
<form method="post" action="/contact">
<input name="email" placeholder="vos@estudio.com"><button>Escribinos</button>
</form>
</main>
<script>window.__SERVICES__ = { raw json_for_script(services) };</script>
-- Doc example: the composition core of "Build a website" — a base layout with a named
-- slot, a nav partial, a card component with props, and a page that fills them all.
-- Self-contained: writes the templates, renders the page, asserts the assembled HTML.
intent: "doc example: website composition (layout + slots + components)"
require file.write("_doctest_site_base.html")
require file.read("_doctest_site_base.html")
require file.write("_doctest_site_nav.html")
require file.read("_doctest_site_nav.html")
require file.write("_doctest_site_card.html")
require file.read("_doctest_site_card.html")
require file.write("_doctest_site_home.html")
require file.read("_doctest_site_home.html")
-- layouts/base.html — the chrome: <head> with a named slot, nav, the page slot
write_file("_doctest_site_base.html", "<html><head><title>{ title }</title>{ slot \"head_extra\" }</head><body>{ include \"_doctest_site_nav.html\" }{ slot }</body></html>")
-- partials/nav.html
write_file("_doctest_site_nav.html", "<nav><a href=\"/\">Studio</a></nav>")
-- partials/card.html — a COMPONENT: sees only its props
write_file("_doctest_site_card.html", "<article class=\"card\"><h2>{ n }. { name }</h2></article>")
-- pages/home.html — inline CSS via the verbatim { raw } block + the grid of cards
write_file("_doctest_site_home.html", "{ layout \"_doctest_site_base.html\" }{ fill \"head_extra\" }<style>{ raw }.card { border-radius: 12px; transition: transform .2s ease; }{ end }</style>{ end }<main><h1>{ title }</h1>{ each e in enumerate(services) }{ include \"_doctest_site_card.html\" with {\"n\": e.index + 1, \"name\": e.item} }{ otherwise }<p>Coming soon.</p>{ end }</main>")
let html be body of render("_doctest_site_home.html", {"title": "Studio", "services": ["Branding", "Web"]})
print(html)
test "the page assembles: layout chrome + named slot + components with props"
let html be body of render("_doctest_site_home.html", {"title": "Studio", "services": ["Branding", "Web"]})
assert(contains(html, "<title>Studio</title>"))
assert(contains(html, ".card { border-radius: 12px")) -- verbatim CSS intact, in <head>
assert(contains(html, "<nav><a href=\"/\">Studio</a></nav>")) -- partial
assert(contains(html, "<h2>1. Branding</h2>")) -- component props + index
assert(contains(html, "<h2>2. Web</h2>"))
test "the empty state renders the { otherwise } branch"
let html be body of render("_doctest_site_home.html", {"title": "Studio", "services": []})
assert(contains(html, "Coming soon."))
La app — app.syn
require serve(8080)
task services()
give [{"name": "Branding", "blurb": "Identity systems"},
{"name": "Web", "blurb": "Sites that convert"}]
task error_page(status, message, request)
let accept be accept of (headers of request)
when accept == nothing
set accept to ""
when contains(accept, "text/html")
give render("pages/error.html", {"status": status, "message": message})
give nothing -- los agentes conservan el error JSON
serve on 8080
errors with error_page
static "/assets" from "./static" cache "1h"
route "GET /"
give render("pages/home.html", {"title": "Studio", "services": services()})
route "POST /contact" -- un form post CLÁSICO — sin fetch, sin JSON
let f be form of request
when not contains(keys(f), "email")
give fail(422, "missing email")
give render("pages/thanks.html", {"email": f.email})
Corrélo: synsema serve app.syn --watch. Templates y CSS se recargan en caliente por request — editás, refrescás, listo; --watch reinicia el server cuando cambia el .syn. Un typo en un path de render("…") falla al arranque, no como un 500.
Lo que te llevás gratis
- Auto-escape en todos lados (XSS-safe por defecto);
json_for_scripthace seguros también los datos de<script>. - Componentes de verdad —
include … with {props}es aislado; nada de globals filtrándose a los partials. - Páginas de error con status honestos — la página 404 sale CON 404 (nada de soft-404); un 401 puede dar
give redirect("/login"). - Formularios sin JavaScript —
form of requestparsea urlencoded y multipart (los archivos llegan como bytes exactos). - Estáticos de producción — ETag/304, Range, gzip, más tu política
cache "1h"; agregáfallback "index.html"para una SPA.
Cuando el sitio crece: rutas en módulos
Mové grupos enteros de rutas fuera de app.syn:
-- shop.syn
export routes shop
route "GET /shop"
give render("pages/shop.html", {"items": catalog()})
-- app.syn
use "./shop.syn" as shop
serve on 8080
mount shop.shop -- o: mount shop.shop at "/store"
Los cuerpos montados llaman a los helpers privados del módulo directo — mirá Módulos.
Siguiente
- Frontend — la referencia completa de templates (incluido
content()para páginas legibles por agentes). - Servidor HTTP — auth, sesiones, SSE, rate limiting, TLS/auto-HTTPS.
- Construí una API REST — el lado JSON del mismo servidor.