Code

The middleware and the nav header

Four files as tabs, in dependency order: middleware.gonav.golayout.templhome.templ (the request plumbing, the header's data model, then the two templates that use it). The last two are modifications.

1. internal/handlers/middleware.go (new) — the three pieces:

func (h *Handlers) LoadUser(next http.Handler) http.Handler { /* cookie → context, never blocks */ }
func (h *Handlers) RequireAuth(next http.Handler) http.Handler { /* no user → redirect /login */ }
func UserFrom(ctx context.Context) (db.User, bool) { /* read the user back out */ }

The user rides the request in a typed context key (userCtxKey), a private ctxKey type so nothing else can collide with it. LoadUser logs a real DB error but still serves the request as anonymous — one flaky query shouldn't 500 the whole site.

2. internal/web/nav.go (new) — a tiny model the header needs:

type Nav struct {
	LoggedIn bool
	Email    string
}

(A CSRFToken field joins it in lesson 6.)

3. internal/web/layout.templ (modify) — the header's static tagline becomes a real nav that branches on nav.LoggedIn:

if nav.LoggedIn {
	<a href="/dashboard">Dashboard</a>
	<span class="who">{ nav.Email }</span>
	<form method="post" action="/logout"><button>Log out</button></form>
} else {
	<a href="/login">Log in</a>
	<a href="/register">Register</a>
}

Layout now takes a second parameter, nav Nav — so every page threads it through.

4. internal/web/home.templ (modify) — the one consequence of the layout change: Home(userCount int64) becomes Home(nav Nav, userCount int64) and passes nav to @Layout("Home", nav). The page body is untouched.

Nothing new renders yet — no handler builds a nav or calls LoadUser. That's the next step, where login and logout arrive and the header comes alive.