diff --git a/internal/app/server.go b/internal/app/server.go index 3f91a2e..ee7734a 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -267,8 +267,26 @@ func (s *Server) ListenAndServe() error { func (s *Server) Close() error { return s.store.Close() } +// staticHandler wraps a file server so font files always get the correct +// Content-Type — Go's http.DetectContentType can return application/octet-stream +// for .ttf files on older Go versions. +func staticHandler(fs http.FileSystem) http.Handler { + h := http.FileServer(fs) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/static/") + switch { + case strings.HasSuffix(path, ".ttf"): + w.Header().Set("Content-Type", "font/ttf") + case strings.HasSuffix(path, ".woff"), strings.HasSuffix(path, ".woff2"): + w.Header().Set("Content-Type", "font/woff2") + } + h.ServeHTTP(w, r) + }) +} + func (s *Server) routes(mux *http.ServeMux) { - mux.Handle("GET /static/", http.FileServer(http.FS(assets))) + fs := http.FS(assets) + mux.Handle("GET /static/", staticHandler(fs)) mux.Handle("GET /uploads/avatars/", http.StripPrefix("/uploads/avatars/", http.FileServer(http.Dir(filepath.Join(filepath.Dir(s.cfg.DatabasePath), "avatars"))))) mux.HandleFunc("GET /healthz", s.health) mux.HandleFunc("GET /login", s.loginPage) diff --git a/internal/app/server_test.go b/internal/app/server_test.go index 50c3ed9..6136956 100644 --- a/internal/app/server_test.go +++ b/internal/app/server_test.go @@ -934,3 +934,36 @@ func extractCSRF(t *testing.T, body string) string { } return match[1] } + +func TestStaticHandlerSetsFontContentTypes(t *testing.T) { + s, err := New(Config{ + Addr: ":0", + BaseURL: "http://example.test", + DatabasePath: filepath.Join(t.TempDir(), "font.db"), + }) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + tests := []struct { + path string + want string + }{ + {"/static/vendor/fonts/webfonts/Vazirmatn-400.ttf", "font/ttf"}, + {"/static/vendor/fonts/webfonts/Vazirmatn-Bold.woff2", "font/woff2"}, + {"/static/vendor/fontawesome.min.css", "text/css; charset=utf-8"}, + } + for _, tt := range tests { + req := httptest.NewRequest(http.MethodGet, tt.path, nil) + rec := httptest.NewRecorder() + s.http.Handler.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Errorf("%s: status %d", tt.path, rec.Code) + continue + } + if ct := rec.Header().Get("Content-Type"); ct != tt.want { + t.Errorf("%s: Content-Type = %q, want %q", tt.path, ct, tt.want) + } + } +} diff --git a/internal/app/static/vendor/Vazirmatn-font-face.css b/internal/app/static/vendor/Vazirmatn-font-face.css index 4d4dcfa..031aa02 100644 --- a/internal/app/static/vendor/Vazirmatn-font-face.css +++ b/internal/app/static/vendor/Vazirmatn-font-face.css @@ -1,10 +1,13 @@ /* Vazirmatn faces downloaded from Google Fonts and bundled for offline use. */ -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-100.ttf') format('truetype'); font-weight: 100; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-200.ttf') format('truetype'); font-weight: 200; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-300.ttf') format('truetype'); font-weight: 300; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-400.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-500.ttf') format('truetype'); font-weight: 500; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-600.ttf') format('truetype'); font-weight: 600; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-700.ttf') format('truetype'); font-weight: 700; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-800.ttf') format('truetype'); font-weight: 800; font-style: normal; font-display: swap; } -@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-900.ttf') format('truetype'); font-weight: 900; font-style: normal; font-display: swap; } +/* Only WOFF2 referenced — all named-weight WOFF2 files exist; + numeric-weight TTF fallbacks exist but the named weights + (Regular/Bold/etc.) don't have TTF versions on disk. */ +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-Thin.woff2') format('woff2'); font-weight: 100; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-ExtraLight.woff2') format('woff2'); font-weight: 200; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-Light.woff2') format('woff2'); font-weight: 300; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-Medium.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-SemiBold.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-Bold.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-ExtraBold.woff2') format('woff2'); font-weight: 800; font-style: normal; font-display: swap; } +@font-face { font-family: Vazirmatn; src: url('fonts/webfonts/Vazirmatn-Black.woff2') format('woff2'); font-weight: 900; font-style: normal; font-display: swap; }