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
@@ -21,13 +23,12 @@ type pageData struct {
// Handler serves template-rendered pages. // Handler serves template-rendered pages.
type Handler struct { 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 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
if err2 := h.tmpl.ExecuteTemplate(w, page, data); err2 != nil {
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"), if err != nil {
"templates", log.Printf("Template parse error: %v", err)
return
} }
for _, dir := range dirs { h.tmpl = tmpl
if _, err := os.Stat(dir); err != nil { log.Println("Loaded embedded HTML templates")
continue
}
pattern := filepath.Join(dir, "*.html")
tmpl, err := template.New("").Funcs(template.FuncMap{}).ParseGlob(pattern)
if err != nil {
log.Printf("Template parse error in %s: %v", dir, err)
continue
}
if tmpl != nil {
h.tmpl = tmpl
h.loaded = true
log.Printf("Loaded templates from %s", dir)
return
}
}
log.Println("No HTML templates found; using placeholder responses")
} }