Served the frontend's build output from server.frontend.dir

internal/frontend serves a Vite build from an fs.FS: hashed files under
/assets/ are cached for a year, HTML and other files get no-cache, ETags are
computed (and cached for a build that can't change), precompressed variants
are served when the client accepts them, and a path without an extension
falls back to index.html for client-side routes. Only GET and HEAD are
allowed.

server.WithFrontend serves it for every path outside /api and /ws, behind
compression and the frontend group's hooks. job-search serve passes the
directory in server.frontend.dir, checked for an index.html at start; empty
means no frontend. Its option building moved into serverOptions, with the
access log.
This commit is contained in:
bwilliams committed 2026-10-16 16:00:34 +01:00
1 parent 155b84caea
commit 9905ffb4cb
7 files changed
+430 -36

No files matched your search

+17 -3
View File
@@ -6,6 +6,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"io/fs"
"log"
"log/slog"
"net"
@@ -38,6 +39,8 @@ const (
GroupAPI
// GroupWebSocket is /ws upgrade requests.
GroupWebSocket
// GroupFrontend is the static frontend (see WithFrontend).
GroupFrontend
)
// Middleware is the standard net/http middleware shape.
@@ -83,6 +86,15 @@ func WithEvents(bus *events.Bus) Option {
}
}
// WithFrontend makes the server serve the frontend's build output, files,
// for every path outside /api and /ws. Without it there is no frontend: such
// paths get 404.
func WithFrontend(files fs.FS) Option {
return func(s *Server) {
s.frontend = files
}
}
// WithMiddleware appends middleware to a group. This is the generic hook
// (auth, metrics, tracing, custom header rules, ...).
func WithMiddleware(group Group, middlewares ...Middleware) Option {
@@ -120,8 +132,10 @@ type Server struct {
log *slog.Logger
// accessLog is where the access log is written, or nil for none.
accessLog *log.Logger
hooks map[Group][]Middleware
limiters map[Group]limiterHook
// frontend is the frontend's build output, or nil for none.
frontend fs.FS
hooks map[Group][]Middleware
limiters map[Group]limiterHook
// trusted are the proxies whose forwarding headers are believed.
trusted []netip.Prefix
@@ -145,7 +159,7 @@ type Server struct {
}
// New builds the server and its router. It logs to slog.Default() unless
// given WithLogger.
// given WithLogger, and serves no frontend unless given WithFrontend.
func New(conf *config.JSCServer, opts ...Option) (*Server, error) {
server := &Server{
conf: conf,