fix: embed HTML templates into binary via go:embed
All checks were successful
CI / lint (push) Successful in 17s
CI / test (push) Successful in 18s
CI / build (push) Successful in 18s
CI / docker (push) Successful in 1m1s

Switched from runtime file-based template loading (os.Stat + ParseGlob)
to compile-time embedding (embed.FS + ParseFS). Templates are now
bundled into the binary — no need to COPY them into Docker image.

This fixes the fallback response issue where h.loaded was always false
because templates weren't present in the container filesystem.
This commit is contained in:
2026-06-04 06:51:11 +03:00
parent 394003d8c0
commit 2f1f1ef7d6

View File

@@ -2,16 +2,18 @@
package templates package templates
import ( import (
"embed"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
) )
//go:embed html/*.html
var templateFS embed.FS
// pageData is passed to all templates. // pageData is passed to all templates.
type pageData struct { type pageData struct {
Title string Title string
@@ -24,10 +26,9 @@ type Handler struct {
db *database.DB db *database.DB
cfg *config.Config cfg *config.Config
tmpl *template.Template tmpl *template.Template
loaded bool
} }
// NewHandler creates a new templates handler and parses on-disk templates. // NewHandler creates a new templates handler and parses embedded templates.
func NewHandler(db *database.DB, cfg *config.Config) *Handler { func NewHandler(db *database.DB, cfg *config.Config) *Handler {
h := &Handler{db: db, cfg: cfg} h := &Handler{db: db, cfg: cfg}
h.parseTemplates() h.parseTemplates()
@@ -49,79 +50,43 @@ func (h *Handler) index(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if !h.loaded {
fallback(w, "MrixsCraft")
return
}
h.render(w, "index.html", pageData{Title: "Главная"}) h.render(w, "index.html", pageData{Title: "Главная"})
} }
func (h *Handler) loginPage(w http.ResponseWriter, r *http.Request) { func (h *Handler) loginPage(w http.ResponseWriter, r *http.Request) {
if !h.loaded {
fallback(w, "Login")
return
}
h.render(w, "login.html", pageData{Title: "Вход"}) h.render(w, "login.html", pageData{Title: "Вход"})
} }
func (h *Handler) registerPage(w http.ResponseWriter, r *http.Request) { func (h *Handler) registerPage(w http.ResponseWriter, r *http.Request) {
if !h.loaded {
fallback(w, "Register")
return
}
h.render(w, "register.html", pageData{Title: "Регистрация"}) h.render(w, "register.html", pageData{Title: "Регистрация"})
} }
func (h *Handler) profilePage(w http.ResponseWriter, r *http.Request) { func (h *Handler) profilePage(w http.ResponseWriter, r *http.Request) {
if !h.loaded {
fallback(w, "Profile")
return
}
h.render(w, "profile.html", pageData{Title: "Профиль"}) h.render(w, "profile.html", pageData{Title: "Профиль"})
} }
// render executes the base layout template with the given content page. // render executes the base layout template which calls {{template "content" .}}
// The base.html layout calls {{template "content" .}} which is defined in each page file. // to inject the named page content.
func (h *Handler) render(w http.ResponseWriter, page string, data pageData) { func (h *Handler) render(w http.ResponseWriter, page string, data pageData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
if h.tmpl == nil {
http.Error(w, "Templates not loaded", http.StatusInternalServerError)
return
}
if err := h.tmpl.ExecuteTemplate(w, "base.html", data); err != nil { if err := h.tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
log.Printf("Template error (base.html → %s): %v", page, err) log.Printf("Template error (base.html → %s): %v", page, err)
// Fallback: render the page without layout
if err2 := h.tmpl.ExecuteTemplate(w, page, data); err2 != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }
}
// fallback writes a minimal placeholder when templates are missing.
func fallback(w http.ResponseWriter, title string) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("<h1>" + title + "</h1>"))
}
// ── Template parsing ─────────────────────────────────────────── // ── Template parsing ───────────────────────────────────────────
func (h *Handler) parseTemplates() { func (h *Handler) parseTemplates() {
dirs := []string{ tmpl, err := template.New("").Funcs(template.FuncMap{}).ParseFS(templateFS, "html/*.html")
filepath.Join("internal", "templates", "html"),
"templates",
}
for _, dir := range dirs {
if _, err := os.Stat(dir); err != nil {
continue
}
pattern := filepath.Join(dir, "*.html")
tmpl, err := template.New("").Funcs(template.FuncMap{}).ParseGlob(pattern)
if err != nil { if err != nil {
log.Printf("Template parse error in %s: %v", dir, err) log.Printf("Template parse error: %v", err)
continue
}
if tmpl != nil {
h.tmpl = tmpl
h.loaded = true
log.Printf("Loaded templates from %s", dir)
return return
} }
} h.tmpl = tmpl
log.Println("No HTML templates found; using placeholder responses") log.Println("Loaded embedded HTML templates")
} }