Synsemadocsv0.6.xENES

Frontend

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>
website.syn
-- 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><script>window.__SERVICES__ = { raw json_for_script(services) };</script>")

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>"))
    assert(contains(html, "window.__SERVICES__ = [\"Branding\", \"Web\"];"))  -- data for client JS

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)
require file.read("./pages/**")      -- render() reads the pages from disk
require file.read("./layouts/**")    -- ... the { layout } they name
require file.read("./partials/**")   -- ... and every { include }
require file.read("./static/**")     -- the static mount serves this folder

task services()
    give [{"name": "Branding", "blurb": "Identity systems"},
          {"name": "Web", "blurb": "Sites that convert"}]

task error_page(status, message, request)
    when contains(get(headers of request, "accept", ""), "text/html")
        give render("pages/error.html", {"status": status, "message": message})
    give nothing                     -- agents keep the JSON error

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"            -- a CLASSIC form post — no fetch, no JSON
        let f be form of request
        when "email" not in f
            give fail(422, "missing email")
        give render("pages/thanks.html", {"email": f.email})

Renderizar un template desde disco es una lectura de archivo, y también lo es cada { layout } e { include } que trae (engine v0.6.45+), así que la entrada declara las tres carpetas de templates — ./pages/, ./layouts/, ./partials/; es la carpeta y todo lo que tiene abajo. Sin ellas una página es un 500 Capability not granted: file_read("pages/home.html") (o "partials/nav.html": el error nombra el archivo que no se declaró). Servir ./static también es leerla: sin require file.read("./static/") el servidor no arranca, y el error nombra esa línea. (Si ponés las tres bajo una carpeta templates/, es una sola línea: require file.read("./templates/").) get(map, key, default) (v0.6.29+) lee un header que puede no venir, y "email" not in f le pregunta al form si el campo llegó.

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§

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              -- or: mount shop.shop at "/store"

Los cuerpos montados llaman a los helpers privados del módulo directo — mirá Módulos.

Siguiente§