Synsemadocsv0.6.xENES

Frontend

Build a website

A real website — layout, styled components, a contact form, proper error pages — with no framework: your HTML, your CSS, vanilla JS if you want it. The server, the templates, the form parsing and the static pipeline are built in.

The files§

app.syn                 the serve block
shop.syn                routes that live in a module (optional, for bigger sites)
layouts/base.html       the chrome (head, nav, footer)
partials/nav.html       shared pieces
partials/card.html      a component (takes props)
pages/home.html         the page
static/app.css          your stylesheet   (hot-reloads per request)

The layout — layouts/base.html§

One shell for every page: a { slot } for the body and a named slot for per-page <head> extras.

<!DOCTYPE html>
<html lang="en">
<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>made with synsema</footer>
</body>
</html>

A component — partials/card.html§

include … with passes props; the partial sees only them (plus your tasks):

<article class="card"><h2>{ n }. { name }</h2><p>{ blurb }</p></article>

The page — pages/home.html§

Inline CSS lives in a verbatim { raw } block (braces stay literal); the grid loops with an index and an empty state:

{ 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>Coming soon.</p>
    { end }
  </div>
  <form method="post" action="/contact">
    <input name="email" placeholder="you@studio.com"><button>Get in touch</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."))

The 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})

Rendering a template from disk is a file read, and so is every { layout } and { include } it pulls in (engine v0.6.45+), so the entry declares the three template folders — ./pages/, ./layouts/, ./partials/; is the folder and everything below it. Without them a page is a 500 Capability not granted: file_read("pages/home.html") (or "partials/nav.html": the error names the file that was not declared). Serving ./static is reading it too: without require file.read("./static/") the server does not start, and the error names that line. (Keeping the three under one templates/ folder makes it one line: require file.read("./templates/").) get(map, key, default) (v0.6.29+) reads a header that may be absent, and "email" not in f asks the form whether the field came.

Run it: synsema serve app.syn --watch. Templates and CSS hot-reload per request — edit, refresh, done; --watch restarts the server when the .syn changes. A typo in a render("…") path fails at startup, not as a 500.

What you got for free§

When the site grows: routes in modules§

Move whole route groups out of 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"

Mounted bodies call the module's private helpers directly — see Modules.

Next§