fixes and improvements in broker and render

This commit is contained in:
Pedro Pérez 2025-05-23 14:31:23 +02:00
parent ba604bb8b4
commit 2fea2e6ac5
5 changed files with 116 additions and 94 deletions

4
app.go
View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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.