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 <meta> elements that update dynamically based on a Beam value. // // It outputs HTML <title> and <meta> 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++ } }} <title>{ 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) }, })) }} } }