Synsema docsENES

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>")

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

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