Make the blog index includeable from site index

This commit is contained in:
Noah 2017-12-01 08:07:21 -08:00
parent cd575ffb1e
commit 88a9908c19
6 changed files with 198 additions and 135 deletions

View File

@ -38,7 +38,7 @@ type Archive struct {
// BlogRoutes attaches the blog routes to the app. // BlogRoutes attaches the blog routes to the app.
func (b *Blog) BlogRoutes(r *mux.Router) { func (b *Blog) BlogRoutes(r *mux.Router) {
// Public routes // Public routes
r.HandleFunc("/blog", b.BlogIndex) r.HandleFunc("/blog", b.IndexHandler)
r.HandleFunc("/archive", b.BlogArchive) r.HandleFunc("/archive", b.BlogArchive)
r.HandleFunc("/tagged/{tag}", b.Tagged) 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. // IndexHandler renders the main index page of the blog.
func (b *Blog) BlogIndex(w http.ResponseWriter, r *http.Request) { func (b *Blog) IndexHandler(w http.ResponseWriter, r *http.Request) {
b.PartialIndex(w, r, "", "") b.CommonIndexHandler(w, r, "", "")
} }
// Tagged lets you browse blog posts by category. // 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.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. // Drafts renders an index view of only draft posts. Login required.
func (b *Blog) Drafts(w http.ResponseWriter, r *http.Request) { 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. // PrivatePosts renders an index view of only private posts. Login required.
func (b *Blog) PrivatePosts(w http.ResponseWriter, r *http.Request) { 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. // CommonIndexHandler handles common logic for blog index views.
func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request, func (b *Blog) CommonIndexHandler(w http.ResponseWriter, r *http.Request, tag, privacy string) {
tag, privacy string) { // Page title.
v := NewVars() 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. // Get the blog index.
idx, _ := posts.GetIndex() idx, _ := posts.GetIndex()
@ -144,8 +162,7 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request,
} }
if len(pool) == 0 { if len(pool) == 0 {
b.NotFound(w, r, "No blog posts were found.") return template.HTML("No blog posts were found.")
return
} }
sort.Sort(sort.Reverse(posts.ByUpdated(pool))) sort.Sort(sort.Reverse(posts.ByUpdated(pool)))
@ -160,16 +177,16 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request,
stop := offset + perPage stop := offset + perPage
// Handle pagination. // Handle pagination.
v.Data["Page"] = page var previousPage, nextPage int
if page > 1 { if page > 1 {
v.Data["PreviousPage"] = page - 1 previousPage = page - 1
} else { } else {
v.Data["PreviousPage"] = 0 previousPage = 0
} }
if offset+perPage < len(pool) { if offset+perPage < len(pool) {
v.Data["NextPage"] = page + 1 nextPage = page + 1
} else { } else {
v.Data["NextPage"] = 0 nextPage = 0
} }
var view []PostMeta var view []PostMeta
@ -218,8 +235,16 @@ func (b *Blog) PartialIndex(w http.ResponseWriter, r *http.Request,
}) })
} }
v.Data["View"] = view // Render the blog index partial.
b.RenderTemplate(w, r, "blog/index", v) 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. // 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 // viewPost is the underlying implementation of the handler to view a blog
// post, so that it can be called from non-http.HandlerFunc contexts. // 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 { func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string) error {
post, err := posts.LoadFragment(fragment) post, err := posts.LoadFragment(fragment)
if err != nil { if err != nil {
@ -323,19 +350,6 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa
rendered = template.HTML(p.Body) 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{ meta := PostMeta{
Post: p, Post: p,
Rendered: rendered, Rendered: rendered,
@ -345,10 +359,9 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa
NumComments: numComments, NumComments: numComments,
} }
output := bytes.Buffer{} output := bytes.Buffer{}
err = t.Execute(&output, meta) err = b.RenderPartialTemplate(&output, "blog/entry.partial", meta, false, nil)
if err != nil { if err != nil {
log.Error(err.Error()) return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
return template.HTML("[error executing template in blog/entry.partial]")
} }
return template.HTML(output.String()) return template.HTML(output.String())

View File

@ -19,12 +19,6 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// Handle the root URI with the blog index.
if path == "/" {
b.BlogIndex(w, r)
return
}
// Restrict special paths. // Restrict special paths.
if strings.HasPrefix(strings.ToLower(path), "/.") { if strings.HasPrefix(strings.ToLower(path), "/.") {
b.Forbidden(w, r) b.Forbidden(w, r)

View File

@ -2,6 +2,7 @@ package core
import ( import (
"html/template" "html/template"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -15,7 +16,7 @@ import (
// variables in. It auto-loads global template variables (site name, etc.) // variables in. It auto-loads global template variables (site name, etc.)
// when the template is rendered. // when the template is rendered.
type Vars struct { type Vars struct {
// Global template variables. // Global, "constant" template variables.
SetupNeeded bool SetupNeeded bool
Title string Title string
Path string Path string
@ -24,6 +25,9 @@ type Vars struct {
CSRF string CSRF string
Request *http.Request Request *http.Request
// Configuration variables
NoLayout bool // don't wrap in .layout.html, just render the template
// Common template variables. // Common template variables.
Message string Message string
Flashes []string Flashes []string
@ -47,7 +51,7 @@ func NewVars(data ...map[interface{}]interface{}) *Vars {
} }
// LoadDefaults combines template variables with default, globally available 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. // Get the site settings.
s, err := settings.Load() s, err := settings.Load()
if err != nil { if err != nil {
@ -64,41 +68,46 @@ func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) {
user, err := b.CurrentUser(r) user, err := b.CurrentUser(r)
v.CurrentUser = user v.CurrentUser = user
v.LoggedIn = err == nil 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.Request)
// }
// TemplateVars is an interface that describes the template variable struct. // RenderPartialTemplate handles rendering a Go template to a writer, without
type TemplateVars interface { // doing anything extra to the vars or dealing with net/http. This is ideal for
LoadDefaults(*Blog, http.ResponseWriter, *http.Request) // 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
)
// RenderTemplate responds with an HTML template. // Find the file path to the 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
}
// And the template in question.
filepath, err := b.ResolvePath(path) filepath, err := b.ResolvePath(path)
if err != nil { if err != nil {
log.Error("RenderTemplate(%s): file not found", path) log.Error("RenderTemplate(%s): file not found", path)
return err 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. // The comment entry partial.
commentEntry, err := b.ResolvePath("comments/entry.partial") commentEntry, err := b.ResolvePath("comments/entry.partial")
if err != nil { if err != nil {
@ -106,39 +115,76 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
return err return err
} }
// Useful template functions. // Template functions.
t := template.New(filepath.Absolute).Funcs(template.FuncMap{ funcmap := template.FuncMap{
"StringsJoin": strings.Join, "StringsJoin": strings.Join,
"Now": time.Now, "Now": time.Now,
"RenderIndex": b.RenderIndex,
"RenderPost": b.RenderPost, "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 { "RenderComments": func(subject string, ids ...string) template.HTML {
session := b.Session(r) session := b.Session(r)
csrf := b.GenerateCSRFToken(w, r, session) csrf := b.GenerateCSRFToken(w, r, session)
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...) 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") log.Debug("Parsed template")
return nil return nil

View File

@ -1,46 +1,8 @@
{{ define "title" }}Welcome{{ end }} {{ define "title" }}{{ .Data.Title }}{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="row"> <h1>{{ .Data.Title }}</h1>
<div class="col text-right">
<ul class="list-inline">
{{ if .Data.PreviousPage }}
<li class="list-inline-item"><a href="?page={{ .Data.PreviousPage }}">Earlier</a></li>
{{ end }}
{{ if .Data.NextPage }}
<li class="list-inline-item"><a href="?page={{ .Data.NextPage }}">Older</a></li>
{{ end }}
</div>
</div>
{{ range .Data.View }} {{ RenderIndex .Request .Data.Tag .Data.Privacy }}
{{ $p := .Post }}
{{ RenderPost $p true .NumComments }}
{{ if and $.LoggedIn $.CurrentUser.Admin }}
<div class="mb-4">
<small>
<strong>Admin Actions:</strong>
[
<a href="/blog/edit?id={{ $p.ID }}">Edit</a> |
<a href="/blog/delete?id={{ $p.ID }}">Delete</a>
]
</small>
</div>
{{ end }}
<hr>
{{ end }}
<div class="row">
<div class="col text-right">
<ul class="list-inline">
{{ if .Data.PreviousPage }}
<li class="list-inline-item"><a href="?page={{ .Data.PreviousPage }}">Earlier</a></li>
{{ end }}
{{ if .Data.NextPage }}
<li class="list-inline-item"><a href="?page={{ .Data.NextPage }}">Older</a></li>
{{ end }}
</div>
</div>
{{ end }} {{ end }}

View File

@ -0,0 +1,41 @@
<div class="row">
<div class="col text-right">
<ul class="list-inline">
{{ if .PreviousPage }}
<li class="list-inline-item"><a href="?page={{ .PreviousPage }}">Earlier</a></li>
{{ end }}
{{ if .NextPage }}
<li class="list-inline-item"><a href="?page={{ .NextPage }}">Older</a></li>
{{ end }}
</div>
</div>
{{ range .View }}
{{ $p := .Post }}
{{ RenderPost $p true .NumComments }}
{{ if and $.LoggedIn $.CurrentUser.Admin }}
<div class="mb-4">
<small>
<strong>Admin Actions:</strong>
[
<a href="/blog/edit?id={{ $p.ID }}">Edit</a> |
<a href="/blog/delete?id={{ $p.ID }}">Delete</a>
]
</small>
</div>
{{ end }}
<hr>
{{ end }}
<div class="row">
<div class="col text-right">
<ul class="list-inline">
{{ if .PreviousPage }}
<li class="list-inline-item"><a href="?page={{ .PreviousPage }}">Earlier</a></li>
{{ end }}
{{ if .NextPage }}
<li class="list-inline-item"><a href="?page={{ .NextPage }}">Older</a></li>
{{ end }}
</div>
</div>

View File

@ -1,4 +1,11 @@
{{ define "title" }}Welcome{{ end }} {{ define "title" }}Welcome{{ end }}
{{ define "content" }} {{ define "content" }}
<h1>Index</h1> <h1>Welcome to "Blog!"</h1>
<p>
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.
</p>
{{ RenderIndex .Request "" "" }}
{{ end }} {{ end }}