diff --git a/site/nextrouter/nextrouter.go b/site/nextrouter/nextrouter.go index adf9a00cd2..caa84c4065 100644 --- a/site/nextrouter/nextrouter.go +++ b/site/nextrouter/nextrouter.go @@ -49,10 +49,7 @@ func Handler(fileSystem fs.FS, options *Options) (http.Handler, error) { } // Fallback to static file server for non-HTML files - // Non-HTML files don't have special routing rules, so we can just leverage - // the built-in http.FileServer for it. - fileHandler := http.FileServer(http.FS(fileSystem)) - router.NotFound(fileHandler.ServeHTTP) + router.NotFound(FileHandler(fileSystem)) // Finally, if there is a 404.html available, serve that err = register404(fileSystem, router, *options) @@ -64,6 +61,31 @@ func Handler(fileSystem fs.FS, options *Options) (http.Handler, error) { return router, nil } +// FileHandler serves static content, additionally adding immutable +// cache-control headers for Next.js content +func FileHandler(fileSystem fs.FS) func(writer http.ResponseWriter, request *http.Request) { + // Non-HTML files don't have special routing rules, so we can just leverage + // the built-in http.FileServer for it. + fileHandler := http.FileServer(http.FS(fileSystem)) + + return func(writer http.ResponseWriter, request *http.Request) { + // From the Next.js documentation: + // + // "Caching improves response times and reduces the number + // of requests to external services. Next.js automatically + // adds caching headers to immutable assets served from + // /_next/static including JavaScript, CSS, static images, + // and other media." + // + // See: https://nextjs.org/docs/going-to-production + if strings.HasPrefix(request.URL.Path, "/_next/static/") { + writer.Header().Add("Cache-Control", "public, max-age=31536000, immutable") + } + + fileHandler.ServeHTTP(writer, request) + } +} + // registerRoutes recursively traverses the file-system, building routes // as appropriate for respecting NextJS dynamic rules. func registerRoutes(rtr chi.Router, fileSystem fs.FS, options Options) error { diff --git a/site/nextrouter/nextrouter_test.go b/site/nextrouter/nextrouter_test.go index 8e0a807fe1..2f7c9c14d9 100644 --- a/site/nextrouter/nextrouter_test.go +++ b/site/nextrouter/nextrouter_test.go @@ -376,6 +376,42 @@ func TestNextRouter(t *testing.T) { require.EqualValues(t, "test-create", body) }) + t.Run("Caching headers for _next resources", func(t *testing.T) { + t.Parallel() + + rootFS := fstest.MapFS{ + "index.html": &fstest.MapFile{ + Data: []byte("test-root"), + }, + "_next/static/test.js": &fstest.MapFile{ + Data: []byte("test.js cached forever"), + }, + "_next/static/chunks/app/test.css": &fstest.MapFile{ + Data: []byte("test.css cached forever"), + }, + } + + router, err := nextrouter.Handler(rootFS, nil) + require.NoError(t, err) + + server := httptest.NewServer(router) + t.Cleanup(server.Close) + + res, err := request(server, "/index.html") + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + require.Equal(t, http.StatusOK, res.StatusCode) + require.Empty(t, res.Header.Get("Cache-Control")) + + res, err = request(server, "/_next/static/test.js") + require.NoError(t, err) + require.NoError(t, res.Body.Close()) + + require.Equal(t, http.StatusOK, res.StatusCode) + require.Equal(t, "public, max-age=31536000, immutable", res.Header.Get("Cache-Control")) + }) + t.Run("Injects template parameters", func(t *testing.T) { t.Parallel()