Skip to content

feat(static): add browser cache-control for static files#111

Merged
appleboy merged 2 commits intomainfrom
feat/static-cache-control
Mar 19, 2026
Merged

feat(static): add browser cache-control for static files#111
appleboy merged 2 commits intomainfrom
feat/static-cache-control

Conversation

@appleboy
Copy link
Copy Markdown
Member

Summary

  • Add Cache-Control headers to static file and favicon responses to reduce unnecessary re-fetches
  • Content-hashed files in /static/dist/ get immutable caching (max-age=31536000, immutable)
  • Other static files use configurable STATIC_CACHE_MAX_AGE env var (default: 24h, set to 0 to disable)
  • Add 7 tests covering dist/non-dist/custom/zero-value cache header configurations

Test plan

  • make generate && make build — builds successfully
  • make test — all tests pass (21 bootstrap tests, full suite green)
  • make lint — 0 issues
  • Manual: start server, open browser DevTools Network tab, verify Cache-Control headers on static assets

🤖 Generated with Claude Code

- Add Cache-Control headers to static file and favicon responses
- Content-hashed files in /static/dist/ get immutable caching (1 year)
- Other static files use configurable STATIC_CACHE_MAX_AGE (default: 24h)
- Setting STATIC_CACHE_MAX_AGE=0 disables caching for non-hashed files
- Add tests for cache header behavior across all configurations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 18, 2026 16:29
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 80.64516% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/bootstrap/router.go 80.00% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds configurable Cache-Control behavior for static assets served by the Gin router, aiming to reduce browser re-fetches while keeping content-hashed bundles highly cacheable.

Changes:

  • Introduces StaticCacheMaxAge config loaded from STATIC_CACHE_MAX_AGE (default 24h, 0 disables for non-hashed assets).
  • Replaces gin.StaticFS with a custom static handler that applies immutable caching for /static/dist/* and configurable caching elsewhere.
  • Adds tests + embedded testdata fixtures to validate cache header behavior for dist/non-dist and zero/custom max-age cases.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/config/config.go Adds StaticCacheMaxAge to config and loads it from STATIC_CACHE_MAX_AGE.
internal/bootstrap/router.go Implements Cache-Control header logic for /static/* and /favicon.ico.
internal/bootstrap/static_cache_test.go Adds tests around static/favicons cache headers using embedded testdata.
internal/bootstrap/testdata/internal/templates/static/images/favicon.ico Adds favicon fixture for tests.
internal/bootstrap/testdata/internal/templates/static/dist/main-62KIAYER.css Adds dist asset fixture for tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +106 to +115
fileServer := http.StripPrefix("/static", http.FileServer(http.FS(staticFS)))
r.GET("/static/*filepath", func(c *gin.Context) {
path := c.Param("filepath")
if strings.HasPrefix(path, "/dist/") {
c.Header("Cache-Control", "public, max-age=31536000, immutable")
} else if cacheMaxAge > 0 {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(cacheMaxAge.Seconds())))
}
fileServer.ServeHTTP(c.Writer, c.Request)
})
Comment on lines +108 to +114
path := c.Param("filepath")
if strings.HasPrefix(path, "/dist/") {
c.Header("Cache-Control", "public, max-age=31536000, immutable")
} else if cacheMaxAge > 0 {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(cacheMaxAge.Seconds())))
}
fileServer.ServeHTTP(c.Writer, c.Request)
Comment on lines +92 to +106
func TestFaviconCacheControl(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
// createFaviconHandler reads from embed.FS at a fixed path;
// use the testdata FS which has the file at the adjusted path.
faviconData, err := testdataFS.ReadFile("testdata/internal/templates/static/images/favicon.ico")
require.NoError(t, err)

cacheMaxAge := 24 * time.Hour
r.GET("/favicon.ico", func(c *gin.Context) {
if cacheMaxAge > 0 {
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(cacheMaxAge.Seconds())))
}
c.Data(http.StatusOK, "image/x-icon", faviconData)
})
…iles

- Only set Cache-Control on successful responses to prevent caching 404s
- Register HEAD handler for static files to support CDN cache validation
- Extract createFaviconHandlerFromBytes for direct test coverage
- Add tests for 404 no-cache and HEAD request scenarios

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy merged commit d632434 into main Mar 19, 2026
15 of 16 checks passed
@appleboy appleboy deleted the feat/static-cache-control branch March 19, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants