Middleware Usage
MiddlewareFunc is standard Go middleware that also receives the route's *PageNode, so middleware can inspect page metadata:
type MiddlewareFunc func(next http.Handler, pn *structpages.PageNode) http.Handler
Global middleware
Apply middleware to all routes:
mux := http.NewServeMux()
sp, err := structpages.Mount(mux, pages{}, "/", "My App",
structpages.WithMiddlewares(
loggingMiddleware,
authMiddleware,
),
)
if err != nil {
log.Fatal(err)
}
Page middlewares
Implement the Middlewares() method to add middleware to a specific page; it also applies to all descendant routes:
type protectedPages struct {
// children pages will be protected
}
func (p protectedPages) Middlewares() []structpages.MiddlewareFunc {
return []structpages.MiddlewareFunc{
requireAuth,
checkPermissions,
}
}
Middlewares() can take injected dependencies (matched by type from WithArgs):
func (p protectedPages) Middlewares(sm *SessionManager) []structpages.MiddlewareFunc {
return []structpages.MiddlewareFunc{
func(next http.Handler, pn *structpages.PageNode) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !sm.Exists(r.Context(), "user") {
redirectToLogin(w, r)
return
}
next.ServeHTTP(w, r)
})
},
}
}
// Middleware is outside the error-return path, so do the HTMX check here:
// a 3xx during an HTMX request would be swapped into the partial's target.
func redirectToLogin(w http.ResponseWriter, r *http.Request) {
loginURL, err := structpages.URLFor(r.Context(), loginPage{})
if err != nil {
// http.Error is acceptable here only because middleware sits outside
// structpages' error handling.
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Location", loginURL) // ajax navigation; status must stay 2xx
return
}
http.Redirect(w, r, loginURL, http.StatusSeeOther)
}
Note the login URL comes from URLFor, not a string literal — when the login route moves, this middleware follows. Handler methods themselves should redirect via the Redirect control-flow signal instead; the inline check is only needed here because middleware runs outside the error-return path.
Example logging middleware using the PageNode:
func loggingMiddleware(next http.Handler, pn *structpages.PageNode) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s (%s) took %v", r.Method, r.URL.Path, pn.Title, time.Since(start))
})
}
Middleware execution order
The framework prepends two implicit middlewares to every route, then layers the user-supplied chain on top. The final order, from outermost (runs first on the request, last on the response) to innermost:
- Framework:
withPcCtx— injects the parse context intor.Context()soURLFor/ID/IDTargetwork in handlers. - Framework:
extractURLParams— pre-extracts the current request's path params into context forURLForauto-fill. - Global middlewares from
WithMiddlewares(...)— first item is outermost. - Page-specific middlewares from
Middlewares()— accumulate down the page tree (parent's middlewares wrap children's). - The page handler — innermost.
Middleware execution forms an "onion": the outermost middleware sees the request first and the response last.
Because of the auto-injected middlewares, you don't need to do anything to make URLFor and ID/IDTarget work from inside your handlers and middleware — the parse context and current-route params are already in r.Context().