package doors
import (
"context"
"github.com/doors-dev/doors/internal/common"
"github.com/doors-dev/doors/internal/front"
"github.com/doors-dev/doors/internal/instance"
"github.com/doors-dev/doors/internal/resources"
"github.com/doors-dev/doors/internal/router"
"github.com/doors-dev/doors/internal/shredder"
"net/http"
"reflect"
)
// HeadData represents page metadata including title and meta tags
type HeadData struct {
Title string
Meta map[string]string
}
// Head renders both
and elements that update dynamically based on a Beam value.
//
// It outputs HTML and tags, and includes the necessary script bindings
// to ensure all metadata updates reactively when the Beam changes on the server.
//
// Example:
//
// @doors.Head(beam, func(p Path) HeadData {
// return HeadData{
// Title: "Product: " + p.Name,
// Meta: map[string]string{
// "description": "Buy " + p.Name + " at the best price",
// "keywords": p.Name + ", product, buy",
// "og:title": p.Name,
// "og:description": "Check out this amazing product",
// },
// }
// })
//
// Parameters:
// - b: a Beam providing the input value (usually page path Beam)
// - cast: a function that maps the Beam value to a HeadData struct.
//
// Returns:
// - A templ.Component that renders title and meta elements with remote call scripts.
type headUsed struct{}
templ Head[M any](b Beam[M], cast func(M) HeadData) {
{{
_, ok := InstanceLoad(ctx, headUsed{}).(headUsed)
if ok {
return nil
}
InstanceSave(ctx, headUsed{}, headUsed{})
inst := ctx.Value(common.InstanceCtxKey).(instance.Core)
thread := inst.Thread()
var cancel = func() {}
var currentMeta HeadData
m, ok := b.ReadAndSub(ctx, func(ctx context.Context, m M) bool {
thread.Write(func(t *shredder.Thread) {
if t == nil {
return
}
newMeta := cast(m)
if reflect.DeepEqual(newMeta, currentMeta) {
return
}
currentMeta = newMeta
cancel()
cancel, _ = Call(ctx, CallConf{
Name: "update_metadata",
Arg: map[string]interface{}{
"title": newMeta.Title,
"meta": func() map[string]string {
escapedTags := make(map[string]string, len(newMeta.Meta))
for k, v := range newMeta.Meta {
escapedTags[k] = templ.EscapeString(v)
}
return escapedTags
}(),
},
})
})
return false
})
if !ok {
return nil
}
currentMeta = cast(m)
tags := make([]string, len(currentMeta.Meta))
i := 0
for k := range currentMeta.Meta {
tags[i] = k
i++
}
}}
{ currentMeta.Title }
for name, content := range currentMeta.Meta {
}
@Script() {
@AData{
Name: "tags",
Value: tags,
}
}
}
func inlineName(attr templ.Attributes, ext string) string {
name := "inline"
dataName, ok := attr["data-name"]
if ok {
dataNameStr, ok := dataName.(string)
if ok {
name = dataNameStr
}
}
return name + "." + ext
}
templ scriptRender(i *resources.InlineResource, inline bool, mode resources.InlineMode, attrs *front.Attrs) {
{{
name := inlineName(i.Attrs, "js")
}}
if inline && mode != resources.InlineModeHost {
@renderRaw("script", attrs, i.Content())
} else if mode == resources.InlineModeHost {
{{ attrs.Set("src", router.ResourcePath(i.Resource(), name)) }}
} else {
{{
attrs.Join(A(ctx, ARawSrc{
Once: true,
Name: name,
Handler: func(w http.ResponseWriter, r *http.Request) {
i.Serve(w, r)
},
}))
}}
}
}
templ styleRender(i *resources.InlineResource, inline bool, mode resources.InlineMode, attrs *front.Attrs) {
{{
name := inlineName(i.Attrs, "css")
}}
if inline && mode != resources.InlineModeHost {
@renderRaw("style", attrs, i.Content())
} else if mode == resources.InlineModeHost {
{{
attrs.Set("href", router.ResourcePath(i.Resource(), name))
}}
} else {
{{
attrs.Join(front.A(ctx, ARawFileHref{
Name: name,
Once: false,
Handler: func(w http.ResponseWriter, r *http.Request) {
i.Serve(w, r)
},
}))
}}
}
}