From 88a9908c19f707258ec06480470c4836af91bdca Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 1 Dec 2017 08:07:21 -0800 Subject: [PATCH] Make the blog index includeable from site index --- core/blog.go | 85 +++++++++++-------- core/pages.go | 6 -- core/templates.go | 148 +++++++++++++++++++++------------ root/blog/index.gohtml | 44 +--------- root/blog/index.partial.gohtml | 41 +++++++++ root/index.gohtml | 9 +- 6 files changed, 198 insertions(+), 135 deletions(-) create mode 100644 root/blog/index.partial.gohtml diff --git a/core/blog.go b/core/blog.go index 486793f..a747705 100644 --- a/core/blog.go +++ b/core/blog.go @@ -38,7 +38,7 @@ type Archive struct { // BlogRoutes attaches the blog routes to the app. func (b *Blog) BlogRoutes(r *mux.Router) { // Public routes - r.HandleFunc("/blog", b.BlogIndex) + r.HandleFunc("/blog", b.IndexHandler) r.HandleFunc("/archive", b.BlogArchive) r.HandleFunc("/tagged/{tag}", b.Tagged) @@ -65,9 +65,9 @@ func (b *Blog) BlogRoutes(r *mux.Router) { )) } -// BlogIndex renders the main index page of the blog. -func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) { - b.PartialIndex(w, r, "", "") +// IndexHandler renders the main index page of the blog. +func (b *Blog) IndexHandler(w http.ResponseWriter, r *http.Request) { + b.CommonIndexHandler(w, r, "", "") } // Tagged lets you browse blog posts by category. @@ -78,24 +78,42 @@ func (b *Blog) Tagged(w http.ResponseWriter, r *http.Request) { b.BadRequest(w, r, "Missing category in URL") } - b.PartialIndex(w, r, tag, "") + b.CommonIndexHandler(w, r, tag, "") } // Drafts renders an index view of only draft posts. Login required. func (b *Blog) Drafts(w http.ResponseWriter, r *http.Request) { - b.PartialIndex(w, r, "", DRAFT) + b.CommonIndexHandler(w, r, "", DRAFT) } // PrivatePosts renders an index view of only private posts. Login required. func (b *Blog) PrivatePosts(w http.ResponseWriter, r *http.Request) { - b.PartialIndex(w, r, "", PRIVATE) + b.CommonIndexHandler(w, r, "", PRIVATE) } -// PartialIndex handles common logic for blog index views. -func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request, - tag, privacy string) { - v := NewVars() +// CommonIndexHandler handles common logic for blog index views. +func (b *Blog) CommonIndexHandler(w http.ResponseWriter, r *http.Request, tag, privacy string) { + // Page title. + var title string + if privacy == DRAFT { + title = "Draft Posts" + } else if privacy == PRIVATE { + title = "Private Posts" + } else if tag != "" { + title = "Tagged as: " + tag + } else { + title = "Blog" + } + b.RenderTemplate(w, r, "blog/index", NewVars(map[interface{}]interface{}{ + "Title": title, + "Tag": tag, + "Privacy": privacy, + })) +} + +// RenderIndex renders and returns the blog index partial. +func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML { // Get the blog index. idx, _ := posts.GetIndex() @@ -144,8 +162,7 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request, } if len(pool) == 0 { - b.NotFound(w, r, "No blog posts were found.") - return + return template.HTML("No blog posts were found.") } sort.Sort(sort.Reverse(posts.ByUpdated(pool))) @@ -160,16 +177,16 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request, stop := offset + perPage // Handle pagination. - v.Data["Page"] = page + var previousPage, nextPage int if page > 1 { - v.Data["PreviousPage"] = page - 1 + previousPage = page - 1 } else { - v.Data["PreviousPage"] = 0 + previousPage = 0 } if offset+perPage < len(pool) { - v.Data["NextPage"] = page + 1 + nextPage = page + 1 } else { - v.Data["NextPage"] = 0 + nextPage = 0 } var view []PostMeta @@ -218,8 +235,16 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request, }) } - v.Data["View"] = view - b.RenderTemplate(w, r, "blog/index", v) + // Render the blog index partial. + var output bytes.Buffer + v := map[string]interface{}{ + "PreviousPage": previousPage, + "NextPage": nextPage, + "View": view, + } + b.RenderPartialTemplate(&output, "blog/index.partial", v, false, nil) + + return template.HTML(output.String()) } // BlogArchive summarizes all blog entries in an archive view. @@ -271,6 +296,8 @@ func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) { // viewPost is the underlying implementation of the handler to view a blog // post, so that it can be called from non-http.HandlerFunc contexts. +// Specifically, from the catch-all page handler to allow blog URL fragments +// to map to their post. func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string) error { post, err := posts.LoadFragment(fragment) if err != nil { @@ -323,19 +350,6 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa rendered = template.HTML(p.Body) } - // Get the template snippet. - filepath, err := b.ResolvePath("blog/entry.partial") - if err != nil { - log.Error(err.Error()) - return template.HTML("[error: missing blog/entry.partial]") - } - t := template.New("entry.partial.gohtml") - t, err = t.ParseFiles(filepath.Absolute) - if err != nil { - log.Error("Failed to parse entry.partial: %s", err.Error()) - return template.HTML("[error parsing template in blog/entry.partial]") - } - meta := PostMeta{ Post: p, Rendered: rendered, @@ -345,10 +359,9 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa NumComments: numComments, } output := bytes.Buffer{} - err = t.Execute(&output, meta) + err = b.RenderPartialTemplate(&output, "blog/entry.partial", meta, false, nil) if err != nil { - log.Error(err.Error()) - return template.HTML("[error executing template in blog/entry.partial]") + return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error())) } return template.HTML(output.String()) diff --git a/core/pages.go b/core/pages.go index fb77a32..dc37291 100644 --- a/core/pages.go +++ b/core/pages.go @@ -19,12 +19,6 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) { return } - // Handle the root URI with the blog index. - if path == "/" { - b.BlogIndex(w, r) - return - } - // Restrict special paths. if strings.HasPrefix(strings.ToLower(path), "/.") { b.Forbidden(w, r) diff --git a/core/templates.go b/core/templates.go index 0b9576e..e3a51ad 100644 --- a/core/templates.go +++ b/core/templates.go @@ -2,6 +2,7 @@ package core import ( "html/template" + "io" "net/http" "strings" "time" @@ -15,7 +16,7 @@ import ( // variables in. It auto-loads global template variables (site name, etc.) // when the template is rendered. type Vars struct { - // Global template variables. + // Global, "constant" template variables. SetupNeeded bool Title string Path string @@ -24,6 +25,9 @@ type Vars struct { CSRF string Request *http.Request + // Configuration variables + NoLayout bool // don't wrap in .layout.html, just render the template + // Common template variables. Message string Flashes []string @@ -47,7 +51,7 @@ func NewVars(data ...map[interface{}]interface{}) *Vars { } // LoadDefaults combines template variables with default, globally available vars. -func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) { +func (v *Vars) LoadDefaults(b *Blog, r *http.Request) { // Get the site settings. s, err := settings.Load() if err != nil { @@ -64,41 +68,46 @@ func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) { user, err := b.CurrentUser(r) v.CurrentUser = user v.LoggedIn = err == nil - - // Add any flashed messages from the endpoint controllers. - session := b.Session(r) - if flashes := session.Flashes(); len(flashes) > 0 { - for _, flash := range flashes { - _ = flash - v.Flashes = append(v.Flashes, flash.(string)) - } - session.Save(r, w) - } - - v.CSRF = b.GenerateCSRFToken(w, r, session) } -// TemplateVars is an interface that describes the template variable struct. -type TemplateVars interface { - LoadDefaults(*Blog, http.ResponseWriter, *http.Request) -} +// // TemplateVars is an interface that describes the template variable struct. +// type TemplateVars interface { +// LoadDefaults(*Blog, *http.Request) +// } -// RenderTemplate responds with an HTML template. -func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars TemplateVars) error { - // Get the layout template. - layout, err := b.ResolvePath(".layout") - if err != nil { - log.Error("RenderTemplate(%s): layout template not found", path) - return err - } +// RenderPartialTemplate handles rendering a Go template to a writer, without +// doing anything extra to the vars or dealing with net/http. This is ideal for +// rendering partials, such as comment partials. +// +// This will wrap the template in `.layout.gohtml` by default. To render just +// a bare template on its own, i.e. for partial templates, create a Vars struct +// with `Vars{NoIndex: true}` +func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, withLayout bool, functions map[string]interface{}) error { + var ( + layout Filepath + templateName string + err error + ) - // And the template in question. + // Find the file path to the template. filepath, err := b.ResolvePath(path) if err != nil { log.Error("RenderTemplate(%s): file not found", path) return err } + // Get the layout template. + if withLayout { + templateName = "layout" + layout, err = b.ResolvePath(".layout") + if err != nil { + log.Error("RenderTemplate(%s): layout template not found", path) + return err + } + } else { + templateName = filepath.Basename + } + // The comment entry partial. commentEntry, err := b.ResolvePath("comments/entry.partial") if err != nil { @@ -106,39 +115,76 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin return err } - // Useful template functions. - t := template.New(filepath.Absolute).Funcs(template.FuncMap{ + // Template functions. + funcmap := template.FuncMap{ "StringsJoin": strings.Join, "Now": time.Now, + "RenderIndex": b.RenderIndex, "RenderPost": b.RenderPost, + } + if functions != nil { + for name, fn := range functions { + funcmap[name] = fn + } + } + + // Useful template functions. + t := template.New(filepath.Absolute).Funcs(funcmap) + + // Parse the template files. The layout comes first because it's the wrapper + // and allows the filepath template to set the page title. + var templates []string + if withLayout { + templates = append(templates, layout.Absolute) + } + t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...) + if err != nil { + log.Error(err.Error()) + return err + } + + err = t.ExecuteTemplate(w, templateName, v) + if err != nil { + log.Error("Template parsing error: %s", err) + return err + } + + return nil +} + +// RenderTemplate responds with an HTML template. +// +// The vars will be massaged a bit to load the global defaults (such as the +// website title and user login status), the user's session may be updated with +// new CSRF token, and other such things. If you just want to render a template +// without all that nonsense, use RenderPartialTemplate. +func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars *Vars) error { + // Inject globally available variables. + if vars == nil { + vars = &Vars{} + } + vars.LoadDefaults(b, r) + + // Add any flashed messages from the endpoint controllers. + session := b.Session(r) + if flashes := session.Flashes(); len(flashes) > 0 { + for _, flash := range flashes { + _ = flash + vars.Flashes = append(vars.Flashes, flash.(string)) + } + session.Save(r, w) + } + + vars.CSRF = b.GenerateCSRFToken(w, r, session) + + w.Header().Set("Content-Type", "text/html; encoding=UTF-8") + b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{ "RenderComments": func(subject string, ids ...string) template.HTML { session := b.Session(r) csrf := b.GenerateCSRFToken(w, r, session) return b.RenderComments(session, csrf, r.URL.Path, subject, ids...) }, }) - - // Parse the template files. The layout comes first because it's the wrapper - // and allows the filepath template to set the page title. - t, err = t.ParseFiles(layout.Absolute, commentEntry.Absolute, filepath.Absolute) - if err != nil { - log.Error(err.Error()) - return err - } - - // Inject globally available variables. - if vars == nil { - vars = &Vars{} - } - vars.LoadDefaults(b, w, r) - - w.Header().Set("Content-Type", "text/html; encoding=UTF-8") - err = t.ExecuteTemplate(w, "layout", vars) - if err != nil { - log.Error("Template parsing error: %s", err) - return err - } - log.Debug("Parsed template") return nil diff --git a/root/blog/index.gohtml b/root/blog/index.gohtml index 956cb91..1f31265 100644 --- a/root/blog/index.gohtml +++ b/root/blog/index.gohtml @@ -1,46 +1,8 @@ -{{ define "title" }}Welcome{{ end }} +{{ define "title" }}{{ .Data.Title }}{{ end }} {{ define "content" }} -
-
-
    - {{ if .Data.PreviousPage }} -
  • Earlier
  • - {{ end }} - {{ if .Data.NextPage }} -
  • Older
  • - {{ end }} -
-
+

{{ .Data.Title }}

-{{ range .Data.View }} - {{ $p := .Post }} - {{ RenderPost $p true .NumComments }} - - {{ if and $.LoggedIn $.CurrentUser.Admin }} -
- - Admin Actions: - [ - Edit | - Delete - ] - -
- {{ end }} -
-{{ end }} - -
-
-
    - {{ if .Data.PreviousPage }} -
  • Earlier
  • - {{ end }} - {{ if .Data.NextPage }} -
  • Older
  • - {{ end }} -
-
+{{ RenderIndex .Request .Data.Tag .Data.Privacy }} {{ end }} diff --git a/root/blog/index.partial.gohtml b/root/blog/index.partial.gohtml new file mode 100644 index 0000000..ecf161b --- /dev/null +++ b/root/blog/index.partial.gohtml @@ -0,0 +1,41 @@ +
+
+
    + {{ if .PreviousPage }} +
  • Earlier
  • + {{ end }} + {{ if .NextPage }} +
  • Older
  • + {{ end }} +
+
+ +{{ range .View }} + {{ $p := .Post }} + {{ RenderPost $p true .NumComments }} + + {{ if and $.LoggedIn $.CurrentUser.Admin }} +
+ + Admin Actions: + [ + Edit | + Delete + ] + +
+ {{ end }} +
+{{ end }} + +
+
+
    + {{ if .PreviousPage }} +
  • Earlier
  • + {{ end }} + {{ if .NextPage }} +
  • Older
  • + {{ end }} +
+
diff --git a/root/index.gohtml b/root/index.gohtml index d0448c0..fa80aa6 100644 --- a/root/index.gohtml +++ b/root/index.gohtml @@ -1,4 +1,11 @@ {{ define "title" }}Welcome{{ end }} {{ define "content" }} -

Index

+

Welcome to "Blog!"

+ +

+ This is your index page. You can edit it and put whatever you want here. + By default, the blog index is also embedded on the website's index page. +

+ +{{ RenderIndex .Request "" "" }} {{ end }}