Make the blog index includeable from site index

pull/4/head
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.
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())

View File

@ -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)

View File

@ -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

View File

@ -1,46 +1,8 @@
{{ define "title" }}Welcome{{ end }}
{{ define "title" }}{{ .Data.Title }}{{ end }}
{{ define "content" }}
<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>
<h1>{{ .Data.Title }}</h1>
{{ range .Data.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 .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>
{{ RenderIndex .Request .Data.Tag .Data.Privacy }}
{{ 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 "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 }}