The view layer — layout, home, render
Three files in the code column, as tabs, in dependency order: layout.templ → home.templ → render.go (the shell, then the page that fills it, then the Go helper that renders a page).
1. internal/web/layout.templ — the shared HTML shell every page reuses. The key feature is { children... }: the slot where a page's body goes. It also links the CSS and the vendored htmx.
templ Layout(title string) {
<!DOCTYPE html>
<html lang="en">
<head>
<title>{ title } · SignFlow</title>
<link rel="stylesheet" href="/static/css/style.css"/>
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
<main class="container">{ children... }</main>
</body>
</html>
}
{ title } is interpolated as text, escaped automatically — Templ never lets a raw string become markup by accident (that's an XSS defence you get for free).
2. internal/web/home.templ — the landing page. It calls the layout and passes its body as the children:
templ Home() {
@Layout("Home") {
<section class="hero">
<h1>Sign documents. Keep the receipts.</h1>
...
</section>
}
}
@Layout("Home") { ... } is composition: Home nests its content inside Layout. For now Home() takes no arguments — lesson 3 gives it a userCount int64 parameter to display a live count from the database.
3. internal/handlers/render.go — one small helper so handlers stay terse:
func render(w http.ResponseWriter, r *http.Request, status int, c templ.Component) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_ = c.Render(r.Context(), w)
}
It sets the content type, writes the status, and streams the component. One place to change if we ever add, say, a caching header. (The Render error is intentionally dropped: the status line is already written, so there's nothing left to do but let the connection close.)
Nothing renders yet — the handler still writes the lesson-1 string. That's the next step.