From 2fea2e6ac591177c17e3d2023dfe7cf6bebc91de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Fri, 23 May 2025 14:31:23 +0200 Subject: [PATCH] fixes and improvements in broker and render --- app.go | 4 ++ broker.go | 22 +++++--- render.go | 13 +++-- router.go | 28 ++++++++++ templates.go | 143 +++++++++++++++++++++++---------------------------- 5 files changed, 116 insertions(+), 94 deletions(-) diff --git a/app.go b/app.go index 2b43b10..244602b 100644 --- a/app.go +++ b/app.go @@ -226,6 +226,10 @@ func NewApp(config ...Config) *App { if cfg.CreateTemplates { slog.Debug("creating templates") app.Templates = NewHTMLRender() + + if cfg.EnvMode == EnvironmentProduction { + app.Templates.EnableCache = true + } } if cfg.CreateSession { diff --git a/broker.go b/broker.go index fd57aa0..bfbde45 100644 --- a/broker.go +++ b/broker.go @@ -1,10 +1,13 @@ package goblocks import ( + "bufio" "encoding/json" "fmt" + "html/template" "log/slog" "net/http" + "strings" "sync" "time" @@ -129,17 +132,13 @@ func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) { for { select { case message := <-client.Send: + slog.Info("message", "message", message) var data string switch v := message.Data.(type) { case string: data = v - case map[string]any, []any: - jsonData, err := json.Marshal(v) - if err != nil { - slog.Error("error marshaling message", "error", err) - continue - } - data = string(jsonData) + case template.HTML: + data = string(v) default: jsonData, err := json.Marshal(v) if err != nil { @@ -148,7 +147,14 @@ func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) { } data = string(jsonData) } - fmt.Fprintf(w, "event: %s\ndata: %s\n\n", message.Event, data) + fmt.Fprintf(w, "event: %s\n", message.Event) + + scanner := bufio.NewScanner(strings.NewReader(data)) + for scanner.Scan() { + fmt.Fprintf(w, "data: %s\n", scanner.Text()) + } + + fmt.Fprint(w, "\n") case <-client.Close: slog.Info("client closed", "client_id", clientID) return diff --git a/render.go b/render.go index f0b4e34..870accc 100644 --- a/render.go +++ b/render.go @@ -34,10 +34,10 @@ func (a *App) JSON(w http.ResponseWriter, code int, v any) { json.NewEncoder(w).Encode(v) } -func (a *App) HTML(w http.ResponseWriter, code int, name string, td *TemplateData) { +func (a *App) HTML(w http.ResponseWriter, code int, layout, page string, td *TemplateData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(code) - err := a.Templates.Template(w, name, td) + err := a.Templates.Template(w, layout, page, td) if err != nil { slog.Error("error rendering template", "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -45,12 +45,11 @@ func (a *App) HTML(w http.ResponseWriter, code int, name string, td *TemplateDat } } -func (a *App) RenderHTML(name string, td *TemplateData) (string, error) { - sw := newStringWriter() - err := a.Templates.Template(sw, name, td) +func (a *App) RenderComponent(name string, td *TemplateData) (string, error) { + result, err := a.Templates.RenderComponent(name, td) if err != nil { - slog.Error("error rendering template", "error", err) + slog.Error("error rendering component", "component", name, "error", err) return "", err } - return sw.builder.String(), nil + return result, nil } diff --git a/router.go b/router.go index 943d86f..c48e118 100644 --- a/router.go +++ b/router.go @@ -2,7 +2,10 @@ package goblocks import ( "net/http" + "os" + "path/filepath" "slices" + "strings" ) type Middleware func(http.Handler) http.Handler @@ -35,6 +38,31 @@ func (r *Router) Group(fn func(r *Router)) { fn(sub) } +func (r *Router) Static(urlPrefix, dir string) { + urlPrefix = strings.TrimSuffix(urlPrefix, "/") + + fileServer := http.FileServer(http.Dir(dir)) + fs := http.StripPrefix(urlPrefix, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + fullPath := filepath.Join(dir, req.URL.Path) + + info, err := os.Stat(fullPath) + if err != nil { + http.NotFound(w, req) + return + } + if info.IsDir() { + http.NotFound(w, req) + return + } + + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + + fileServer.ServeHTTP(w, req) + })) + + r.Handle(urlPrefix+"/", fs) +} + func (r *Router) HandleFunc(pattern string, h http.HandlerFunc) { r.Handle(pattern, h) } diff --git a/templates.go b/templates.go index a49254d..d70c9f9 100644 --- a/templates.go +++ b/templates.go @@ -4,8 +4,8 @@ import ( "bytes" "errors" "html/template" - "io/fs" "log/slog" + "maps" "net/http" "path/filepath" "reflect" @@ -37,7 +37,10 @@ func defaultHTMLRender() *Render { EnableCache: false, TemplatesPath: "templates", TemplateData: TemplateData{}, - Functions: template.FuncMap{}, + Functions: template.FuncMap{ + "default": defaultIfEmpty, + "dict": Dict, + }, templateCache: templateCache{}, } } @@ -53,117 +56,99 @@ func (re *Render) apply(opts ...RenderOptions) *Render { opt(re) } } - return re } -func defaultIfEmpty(fallback, value string) string { - if strings.TrimSpace(value) == "" { +func defaultIfEmpty(fallback string, value any) string { + str, ok := value.(string) + if !ok || strings.TrimSpace(str) == "" { return fallback } - return value + + return str } -func (re *Render) Template(w http.ResponseWriter, tmpl string, td *TemplateData) error { - var tc templateCache - var err error - - re.Functions["default"] = defaultIfEmpty +func cloneFuncMap(src template.FuncMap) template.FuncMap { + c := make(template.FuncMap) + maps.Copy(c, src) + return c +} +func (re *Render) Template(w http.ResponseWriter, layoutName, pageName string, td *TemplateData) error { if td == nil { td = &TemplateData{} } - tc, err = re.getTemplateCache() + tmpl, err := re.loadTemplateWithLayout(layoutName, pageName) if err != nil { return err } - t, ok := tc[tmpl] - if !ok { - return errors.New("can't get template from cache") - } - buf := new(bytes.Buffer) - if err = t.Execute(buf, td); err != nil { + if err = tmpl.ExecuteTemplate(buf, strings.TrimSuffix(layoutName, ".gohtml"), td); err != nil { return err } - if _, err = buf.WriteTo(w); err != nil { - return err - } - - return nil + _, err = buf.WriteTo(w) + return err } -func (re *Render) getTemplateCache() (templateCache, error) { - if len(re.templateCache) == 0 { - cachedTemplates, err := re.createTemplateCache() - if err != nil { - return nil, err - } - re.templateCache = cachedTemplates +func (re *Render) RenderComponent(name string, td *TemplateData) (string, error) { + if td == nil { + td = &TemplateData{} } + + path := filepath.Join(re.TemplatesPath, "components", name) + + files := []string{path} + matches, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml")) + if err != nil { + return "", err + } + files = append(files, matches...) + + funcs := cloneFuncMap(re.Functions) + tmpl, err := template.New(name).Funcs(funcs).ParseFiles(files...) + if err != nil { + return "", err + } + + var buf bytes.Buffer + err = tmpl.ExecuteTemplate(&buf, strings.TrimSuffix(name, ".gohtml"), td) + return buf.String(), err +} + +func (re *Render) loadTemplateWithLayout(layoutName, pageName string) (*template.Template, error) { + cacheKey := layoutName + "::" + pageName + if re.EnableCache { - return re.templateCache, nil + if tmpl, ok := re.templateCache[cacheKey]; ok { + return tmpl, nil + } } - return re.createTemplateCache() -} -func (re *Render) findHTMLFiles() ([]string, error) { - var files []string + layoutPath := filepath.Join(re.TemplatesPath, "layouts", layoutName) + pagePath := filepath.Join(re.TemplatesPath, "pages", pageName) - err := filepath.WalkDir(re.TemplatesPath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if !d.IsDir() && filepath.Ext(path) == ".gohtml" { - files = append(files, path) - } - - return nil - }) + files := []string{layoutPath, pagePath} + componentFiles, err := filepath.Glob(filepath.Join(re.TemplatesPath, "components", "*.gohtml")) + if err != nil { + return nil, err + } + files = append(files, componentFiles...) + funcs := cloneFuncMap(re.Functions) + tmpl, err := template.New(layoutName).Funcs(funcs).ParseFiles(files...) if err != nil { return nil, err } - return files, nil -} - -func (re *Render) createTemplateCache() (templateCache, error) { - cache := templateCache{} - var baseTemplates []string - var renderTemplates []string - - templates, err := re.findHTMLFiles() - if err != nil { - return cache, err + if re.EnableCache { + re.templateCache[cacheKey] = tmpl + slog.Debug("template cached", "key", cacheKey) } - slog.Debug("templates", "templates", templates) - - for _, file := range templates { - filePathBase := filepath.Base(file) - if strings.Contains(filePathBase, "layout") || strings.Contains(filePathBase, "fragment") { - baseTemplates = append(baseTemplates, file) - } - } - - for _, file := range templates { - filePathBase := filepath.Base(file) - if strings.Contains(filePathBase, "page") || strings.Contains(filePathBase, "component") { - renderTemplates = append(baseTemplates, file) - ts, err := template.New(filePathBase).Funcs(re.Functions).ParseFiles(append(baseTemplates, renderTemplates...)...) - if err != nil { - return cache, err - } - cache[filePathBase] = ts - } - } - - return cache, nil + return tmpl, nil } // Pages contains pagination info.