Code

The view layer — layout, home, render

Three files in the code column, as tabs, in dependency order: layout.templhome.templrender.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.