Another approach that the talented developer I worked with is to process the Template instance to find any template that is not defined, and look at the file system for the corresponding file and analyze it for each one found; and then visualize after.
This gives you the following setup:
view / index.html
{{template "/includes/page-wrapper.html" .}} {{define "body"}} <div>Page guts go here</div> {{end}} {{define "head_section"}} <title>Title Tag</title> {{end}}
includes / Page-wrapper.html:
<html> <head> {{block "head_section" .}}{{end}} <head> <body> {{template "body" .}} </body> </html>
And your ServeHTTP() method ServeHTTP() for files in the "views" directory, loads and parses it, and then calls TmplIncludeAll() (below).
I ended up adapting the same basic concept as several functions, which are as follows. t is a template after analysis, but before rendering. And fs is the directory in which βviewsβ and βincludesβ live (mentioned above).
func TmplIncludeAll(fs http.FileSystem, t *template.Template) error { tlist := t.Templates() for _, et := range tlist { if et != nil && et.Tree != nil && et.Tree.Root != nil { err := TmplIncludeNode(fs, et, et.Tree.Root) if err != nil { return err } } } return nil } func TmplIncludeNode(fs http.FileSystem, t *template.Template, node parse.Node) error { if node == nil { return nil } switch node := node.(type) { case *parse.TemplateNode: if node == nil { return nil } // if template is already defined, do nothing tlist := t.Templates() for _, et := range tlist { if node.Name == et.Name() { return nil } } t2 := t.New(node.Name) f, err := fs.Open(node.Name) if err != nil { return err } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { return err } _, err = t2.Parse(string(b)) if err != nil { return err } // start over again, will stop recursing when there are no more templates to include return TmplIncludeAll(fs, t) case *parse.ListNode: if node == nil { return nil } for _, node := range node.Nodes { err := TmplIncludeNode(fs, t, node) if err != nil { return err } } case *parse.IfNode: if err := TmplIncludeNode(fs, t, node.BranchNode.List); err != nil { return err } if err := TmplIncludeNode(fs, t, node.BranchNode.ElseList); err != nil { return err } case *parse.RangeNode: if err := TmplIncludeNode(fs, t, node.BranchNode.List); err != nil { return err } if err := TmplIncludeNode(fs, t, node.BranchNode.ElseList); err != nil { return err } case *parse.WithNode: if err := TmplIncludeNode(fs, t, node.BranchNode.List); err != nil { return err } if err := TmplIncludeNode(fs, t, node.BranchNode.ElseList); err != nil { return err } } return nil }
This is my favorite approach, and I have been using it for a while. The advantage is that there is only one render of the template, the error messages are nice and clean, and the layout of the Go template is very readable and obvious. It would be great if the gut html / template.Template made it easier to implement, but overall this is a great IMO solution.