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

+16
View File
@@ -7,6 +7,7 @@ import (
"github.com/go-chi/chi/v5"
"git.avsdev.uk/Go/job-search/internal/config"
"git.avsdev.uk/Go/job-search/internal/frontend"
"git.avsdev.uk/Go/job-search/internal/httpx"
"git.avsdev.uk/Go/job-search/internal/middleware"
)
@@ -18,6 +19,7 @@ import (
// /api : NoStore -> Compress -> [api rate limit + hooks] -> CSRF
// -> RequireJSON
// /ws : [ws rate limit + hooks] -> Origin check (hub)
// /* : Compress -> [frontend hooks] -> static files (if WithFrontend)
func (s *Server) routes() (http.Handler, error) {
conf := s.conf
router := chi.NewRouter()
@@ -67,6 +69,20 @@ func (s *Server) routes() (http.Handler, error) {
webSocketStack := s.groupStack(GroupWebSocket, conf.WebSocket.ConnectionLimit)
router.With(webSocketStack...).Get("/ws", s.hub.ServeHTTP)
// Frontend
if s.frontend != nil {
frontendHandler := frontend.New(s.frontend, frontend.Options{
ImmutablePrefix: conf.Frontend.ImmutablePrefix,
IndexCacheControl: conf.Frontend.IndexCacheControl,
// An embedded build never changes; a directory on disk may.
CacheETags: conf.Frontend.Dir == "",
})
frontendStack := append(
[]Middleware{compress}, s.hooks[GroupFrontend]...,
)
router.With(frontendStack...).Handle("/*", frontendHandler)
}
return router, nil
}