static_cache.go 1.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. //go:build embed || unit
  2. package web
  3. import (
  4. "net/http"
  5. "path"
  6. "strings"
  7. )
  8. // Vite emits content-hashed filenames under assets/, so the backend can apply
  9. // immutable caching without relying on a reverse proxy to classify paths.
  10. const staticAssetsCacheControl = "public, max-age=31536000, immutable"
  11. // isFingerprintedEmbeddedAssetPath reports whether a cleaned URL path refers to
  12. // a Vite asset whose filename contains the default eight-character build hash.
  13. func isFingerprintedEmbeddedAssetPath(cleanPath string) bool {
  14. cleanPath = strings.TrimPrefix(cleanPath, "/")
  15. if !strings.HasPrefix(cleanPath, "assets/") {
  16. return false
  17. }
  18. filename := path.Base(cleanPath)
  19. extension := path.Ext(filename)
  20. stem := strings.TrimSuffix(filename, extension)
  21. const fingerprintLength = 8
  22. delimiterIndex := len(stem) - fingerprintLength - 1
  23. if extension == "" || delimiterIndex < 1 || stem[delimiterIndex] != '-' {
  24. return false
  25. }
  26. // Vite hashes use URL-safe characters and are stable for immutable caching.
  27. fingerprint := stem[delimiterIndex+1:]
  28. for _, char := range fingerprint {
  29. if (char >= 'a' && char <= 'z') ||
  30. (char >= 'A' && char <= 'Z') ||
  31. (char >= '0' && char <= '9') ||
  32. char == '_' || char == '-' {
  33. continue
  34. }
  35. return false
  36. }
  37. return true
  38. }
  39. // applyStaticAssetCacheHeaders sets Cache-Control for long-cacheable static paths.
  40. // index.html / SPA routes must keep no-cache and are not handled here.
  41. func applyStaticAssetCacheHeaders(header http.Header, cleanPath string) {
  42. if header == nil || !isFingerprintedEmbeddedAssetPath(cleanPath) {
  43. return
  44. }
  45. header.Set("Cache-Control", staticAssetsCacheControl)
  46. }