Blog tag pages and contact form
This commit is contained in:
parent
d730b4d43c
commit
94cdc916ac
|
@ -44,7 +44,12 @@ func main() {
|
|||
app := core.New(DocumentRoot, userRoot)
|
||||
if fDebug {
|
||||
app.Debug = true
|
||||
}
|
||||
|
||||
// Set $JSONDB_DEBUG=1 to debug JsonDB; it's very noisy!
|
||||
if os.Getenv("JSONDB_DEBUG") != "" {
|
||||
jsondb.SetDebug(true)
|
||||
}
|
||||
|
||||
app.ListenAndServe(fAddress)
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
r.HandleFunc("/login", blog.LoginHandler)
|
||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||
blog.AdminRoutes(r)
|
||||
blog.ContactRoutes(r)
|
||||
blog.BlogRoutes(r)
|
||||
blog.CommentRoutes(r)
|
||||
|
||||
|
|
50
core/blog.go
50
core/blog.go
|
@ -40,6 +40,7 @@ func (b *Blog) BlogRoutes(r *mux.Router) {
|
|||
// Public routes
|
||||
r.HandleFunc("/blog", b.IndexHandler)
|
||||
r.HandleFunc("/archive", b.BlogArchive)
|
||||
r.HandleFunc("/tagged", b.Tagged)
|
||||
r.HandleFunc("/tagged/{tag}", b.Tagged)
|
||||
|
||||
// Login-required routers.
|
||||
|
@ -75,7 +76,9 @@ func (b *Blog) Tagged(w http.ResponseWriter, r *http.Request) {
|
|||
params := mux.Vars(r)
|
||||
tag, ok := params["tag"]
|
||||
if !ok {
|
||||
b.BadRequest(w, r, "Missing category in URL")
|
||||
// They're listing all the tags.
|
||||
b.RenderTemplate(w, r, "blog/tags.gohtml", NewVars())
|
||||
return
|
||||
}
|
||||
|
||||
b.CommonIndexHandler(w, r, tag, "")
|
||||
|
@ -199,20 +202,6 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
|||
log.Error("couldn't load full post data for ID %d (found in index.json)", pool[i].ID)
|
||||
continue
|
||||
}
|
||||
var rendered template.HTML
|
||||
|
||||
// Body has a snipped section?
|
||||
if strings.Contains(post.Body, "<snip>") {
|
||||
parts := strings.SplitN(post.Body, "<snip>", 1)
|
||||
post.Body = parts[0]
|
||||
}
|
||||
|
||||
// Render the post.
|
||||
if post.ContentType == string(MARKDOWN) {
|
||||
rendered = template.HTML(b.RenderTrustedMarkdown(post.Body))
|
||||
} else {
|
||||
rendered = template.HTML(post.Body)
|
||||
}
|
||||
|
||||
// Look up the author's information.
|
||||
author, err := users.LoadReadonly(post.AuthorID)
|
||||
|
@ -229,7 +218,6 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
|||
|
||||
view = append(view, PostMeta{
|
||||
Post: post,
|
||||
Rendered: rendered,
|
||||
Author: author,
|
||||
NumComments: numComments,
|
||||
})
|
||||
|
@ -247,6 +235,31 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
|||
return template.HTML(output.String())
|
||||
}
|
||||
|
||||
// RenderTags renders the tags partial.
|
||||
func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML {
|
||||
idx, err := posts.GetIndex()
|
||||
if err != nil {
|
||||
return template.HTML("[RenderTags: error getting blog index]")
|
||||
}
|
||||
|
||||
tags, err := idx.Tags()
|
||||
if err != nil {
|
||||
return template.HTML("[RenderTags: error getting tags]")
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
v := struct {
|
||||
IndexView bool
|
||||
Tags []posts.Tag
|
||||
}{
|
||||
IndexView: indexView,
|
||||
Tags: tags,
|
||||
}
|
||||
b.RenderPartialTemplate(&output, "blog/tags.partial", v, false, nil)
|
||||
|
||||
return template.HTML(output.String())
|
||||
}
|
||||
|
||||
// BlogArchive summarizes all blog entries in an archive view.
|
||||
func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
|
||||
idx, err := posts.GetIndex()
|
||||
|
@ -266,7 +279,7 @@ func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
|
|||
continue
|
||||
}
|
||||
|
||||
label := post.Created.Format("2006-02")
|
||||
label := post.Created.Format("2006-01")
|
||||
if _, ok := byMonth[label]; !ok {
|
||||
months = append(months, label)
|
||||
byMonth[label] = &Archive{
|
||||
|
@ -335,13 +348,14 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa
|
|||
var snipped bool
|
||||
if indexView {
|
||||
if strings.Contains(p.Body, "<snip>") {
|
||||
log.Warn("HAS SNIP TAG!")
|
||||
parts := strings.SplitN(p.Body, "<snip>", 2)
|
||||
p.Body = parts[0]
|
||||
snipped = true
|
||||
}
|
||||
}
|
||||
|
||||
p.Body = strings.Replace(p.Body, "<snip>", "<div id=\"snip\"></div>", 1)
|
||||
|
||||
// Render the post to HTML.
|
||||
var rendered template.HTML
|
||||
if p.ContentType == string(MARKDOWN) {
|
||||
|
|
57
core/contact.go
Normal file
57
core/contact.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kirsle/blog/core/forms"
|
||||
"github.com/kirsle/blog/core/models/settings"
|
||||
)
|
||||
|
||||
// ContactRoutes attaches the contact URL to the app.
|
||||
func (b *Blog) ContactRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := NewVars()
|
||||
form := forms.Contact{}
|
||||
v.Form = &form
|
||||
|
||||
// If there is no site admin, show an error.
|
||||
cfg, err := settings.Load()
|
||||
if err != nil {
|
||||
b.Error(w, r, "Error loading site configuration!")
|
||||
return
|
||||
} else if cfg.Site.AdminEmail == "" {
|
||||
b.Error(w, r, "There is no admin email configured for this website!")
|
||||
return
|
||||
} else if !cfg.Mail.Enabled {
|
||||
b.Error(w, r, "This website doesn't have an e-mail gateway configured.")
|
||||
return
|
||||
}
|
||||
|
||||
// Posting?
|
||||
if r.Method == http.MethodPost {
|
||||
form.ParseForm(r)
|
||||
if err = form.Validate(); err != nil {
|
||||
b.Flash(w, r, err.Error())
|
||||
} else {
|
||||
go b.SendEmail(Email{
|
||||
To: cfg.Site.AdminEmail,
|
||||
Admin: true,
|
||||
ReplyTo: form.Email,
|
||||
Subject: fmt.Sprintf("Contact Form on %s: %s", cfg.Site.Title, form.Subject),
|
||||
Template: ".email/contact.gohtml",
|
||||
Data: map[string]interface{}{
|
||||
"Name": form.Name,
|
||||
"Message": template.HTML(b.RenderMarkdown(form.Message)),
|
||||
"Email": form.Email,
|
||||
},
|
||||
})
|
||||
b.FlashAndRedirect(w, r, "/contact", "Your message has been sent.")
|
||||
}
|
||||
}
|
||||
|
||||
b.RenderTemplate(w, r, "contact", v)
|
||||
})
|
||||
}
|
42
core/forms/contact.go
Normal file
42
core/forms/contact.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Contact form for the site admin.
|
||||
type Contact struct {
|
||||
Name string
|
||||
Email string
|
||||
Subject string
|
||||
Message string
|
||||
Trap1 string // 'contact'
|
||||
Trap2 string // 'website'
|
||||
}
|
||||
|
||||
// ParseForm parses the form.
|
||||
func (c *Contact) ParseForm(r *http.Request) {
|
||||
c.Name = r.FormValue("name")
|
||||
c.Email = r.FormValue("email")
|
||||
c.Subject = r.FormValue("subject")
|
||||
c.Message = r.FormValue("message")
|
||||
c.Trap1 = r.FormValue("contact")
|
||||
c.Trap2 = r.FormValue("website")
|
||||
|
||||
// Default values.
|
||||
if c.Name == "" {
|
||||
c.Name = "Anonymous"
|
||||
}
|
||||
if c.Subject == "" {
|
||||
c.Subject = "No Subject"
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the form.
|
||||
func (c Contact) Validate() error {
|
||||
if len(c.Message) == 0 {
|
||||
return errors.New("message is required")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
// Email configuration.
|
||||
type Email struct {
|
||||
To string
|
||||
ReplyTo string
|
||||
Admin bool /* admin view of the email */
|
||||
Subject string
|
||||
UnsubscribeURL string
|
||||
|
@ -71,6 +72,9 @@ func (b *Blog) SendEmail(email Email) {
|
|||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", fmt.Sprintf("%s <%s>", s.Site.Title, s.Mail.Sender))
|
||||
m.SetHeader("To", email.To)
|
||||
if email.ReplyTo != "" {
|
||||
m.SetHeader("Reply-To", email.ReplyTo)
|
||||
}
|
||||
m.SetHeader("Subject", email.Subject)
|
||||
m.SetBody("text/plain", plaintext)
|
||||
m.AddAlternative("text/html", html.String())
|
||||
|
@ -81,6 +85,8 @@ func (b *Blog) SendEmail(email Email) {
|
|||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("SendEmail: %s (%s) to %s", email.Subject, email.Template, email.To)
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
log.Error("SendEmail: %s", err.Error())
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ var (
|
|||
reMarkdownTitle = regexp.MustCompile(`(?m:^#([^#\r\n]+)$)`)
|
||||
|
||||
// Match fenced code blocks with languages defined.
|
||||
reFencedCode = regexp.MustCompile("```" + `([a-z]*)\n([\s\S]*?)\n\s*` + "```")
|
||||
reFencedCode = regexp.MustCompile("```" + `([a-z]*)[\r\n]([\s\S]*?)[\r\n]\s*` + "```")
|
||||
|
||||
// Regexp to match fenced code blocks in rendered Markdown HTML.
|
||||
// Tweak this if you change Markdown engines later.
|
||||
|
@ -66,7 +66,6 @@ func (b *Blog) RenderMarkdown(input string) string {
|
|||
func (b *Blog) RenderTrustedMarkdown(input string) string {
|
||||
// Find and hang on to fenced code blocks.
|
||||
codeBlocks := []codeBlock{}
|
||||
log.Info("RE: %s", reFencedCode.String())
|
||||
matches := reFencedCode.FindAllStringSubmatch(input, -1)
|
||||
for i, m := range matches {
|
||||
language, source := m[1], m[2]
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package posts
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UpdateIndex updates a post's metadata in the blog index.
|
||||
func UpdateIndex(p *Post) error {
|
||||
|
@ -68,6 +71,59 @@ func (idx *Index) Delete(p *Post) error {
|
|||
return DB.Commit("blog/index", idx)
|
||||
}
|
||||
|
||||
// Tag is a response from Tags including metadata about it.
|
||||
type Tag struct {
|
||||
Name string
|
||||
Count int
|
||||
}
|
||||
|
||||
type ByPopularity []Tag
|
||||
|
||||
func (s ByPopularity) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
func (s ByPopularity) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
func (s ByPopularity) Less(i, j int) bool {
|
||||
if s[i].Count < s[j].Count {
|
||||
return true
|
||||
} else if s[i].Count > s[j].Count {
|
||||
return false
|
||||
}
|
||||
return s[i].Name < s[j].Name
|
||||
}
|
||||
|
||||
// Tags returns the tags sorted by most frequent.
|
||||
func (idx *Index) Tags() ([]Tag, error) {
|
||||
idx, err := GetIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unique := map[string]*Tag{}
|
||||
|
||||
for _, post := range idx.Posts {
|
||||
for _, name := range post.Tags {
|
||||
tag, ok := unique[name]
|
||||
if !ok {
|
||||
tag = &Tag{name, 0}
|
||||
unique[name] = tag
|
||||
}
|
||||
tag.Count++
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the tags.
|
||||
tags := []Tag{}
|
||||
for _, tag := range unique {
|
||||
tags = append(tags, *tag)
|
||||
}
|
||||
sort.Sort(sort.Reverse(ByPopularity(tags)))
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// CleanupFragments to clean up old URL fragments.
|
||||
func CleanupFragments() error {
|
||||
idx, err := GetIndex()
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
// PageHandler is the catch-all route handler, for serving static web pages.
|
||||
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
log.Debug("Catch-all page handler invoked for request URI: %s", path)
|
||||
// log.Debug("Catch-all page handler invoked for request URI: %s", path)
|
||||
|
||||
// Remove trailing slashes by redirecting them away.
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
|
@ -39,7 +39,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Is it a template file?
|
||||
if strings.HasSuffix(filepath.URI, ".gohtml") || strings.HasSuffix(filepath.URI, ".html") {
|
||||
if strings.HasSuffix(filepath.URI, ".gohtml") {
|
||||
b.RenderTemplate(w, r, filepath.URI, nil)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin
|
|||
message = []string{"The page you were looking for was not found."}
|
||||
}
|
||||
|
||||
log.Error("HERE 2")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
||||
Message: message[0],
|
||||
|
|
|
@ -121,6 +121,7 @@ func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, wi
|
|||
"Now": time.Now,
|
||||
"RenderIndex": b.RenderIndex,
|
||||
"RenderPost": b.RenderPost,
|
||||
"RenderTags": b.RenderTags,
|
||||
}
|
||||
if functions != nil {
|
||||
for name, fn := range functions {
|
||||
|
@ -185,7 +186,6 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
|||
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
|
||||
},
|
||||
})
|
||||
log.Debug("Parsed template")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
48
root/.email/contact.gohtml
Normal file
48
root/.email/contact.gohtml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="x-apple-disable-message-reformatting"><!-- Disable auto-scale in iOS 10 Mail -->
|
||||
<title>{{ .Subject }}</title>
|
||||
</head>
|
||||
<body width="100%" bgcolor="#FFFFFF" color="#000000" style="margin: 0; mso-line-height-rule: exactly;">
|
||||
|
||||
<center>
|
||||
<table width="90%" cellspacing="0" cellpadding="8" style="border: 1px solid #000000">
|
||||
<tr>
|
||||
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||
<font face="Helvetica,Arial,Verdana-sans-serif" size="6" color="#000000">
|
||||
<b>{{ .Subject }}</b>
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="top" bgcolor="#FEFEFE">
|
||||
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||
{{ or .Data.Name "Anonymous" }} has left you a message:
|
||||
<br><br>
|
||||
|
||||
{{ .Data.Message }}
|
||||
|
||||
<hr>
|
||||
|
||||
{{ if .Data.Email }}
|
||||
You can e-mail them back at <a href="mailto:{{ .Data.Email }}">{{ .Data.Email }}</a>
|
||||
{{ end }}
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" valign="top" bgcolor="#C0C0C0">
|
||||
<font face="Helvetica,Arial,Verdana-sans-serif" size="3" color="#000000">
|
||||
This e-mail was automatically generated; do not reply to it.
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -35,6 +35,9 @@
|
|||
<li class="nav-item">
|
||||
<a href="/archive" class="nav-link">Archive</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/contact" class="nav-link">Contact Me</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form class="form-inline mt-2 mt-md-0">
|
||||
|
|
7
root/blog/tags.gohtml
Normal file
7
root/blog/tags.gohtml
Normal file
|
@ -0,0 +1,7 @@
|
|||
{{ define "title" }}Tags{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Tags</h1>
|
||||
|
||||
{{ RenderTags .Request true }}
|
||||
|
||||
{{ end }}
|
17
root/blog/tags.partial.gohtml
Normal file
17
root/blog/tags.partial.gohtml
Normal file
|
@ -0,0 +1,17 @@
|
|||
{{ if .IndexView }}
|
||||
Sorted by most frequently used:
|
||||
|
||||
<ul>
|
||||
{{ range .Tags }}
|
||||
<li><a href="/tagged/{{ .Name }}">{{ .Name }}</a> ({{ .Count }})</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<ul>
|
||||
{{ range $i, $t := .Tags }}
|
||||
{{ if le $i 20 }}
|
||||
<li><a href="/tagged/{{ .Name }}">{{ .Name }}</a> ({{ .Count }})</li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
|
@ -2,13 +2,13 @@
|
|||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<div class="markdown col-12 col-lg-1 mb-1">
|
||||
<img src="{{ .Avatar }}"
|
||||
width="96"
|
||||
height="96"
|
||||
alt="Avatar image">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="markdown col-12 col-lg-11">
|
||||
<div class="comment-meta">
|
||||
{{ if and .UserID .Username }}
|
||||
<a href="/u/{{ .Username }}"><strong>{{ or .Name "Anonymous" }}</strong></a>
|
||||
|
|
64
root/contact.gohtml
Normal file
64
root/contact.gohtml
Normal file
|
@ -0,0 +1,64 @@
|
|||
{{ define "title" }}Contact Me{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Contact Me</h1>
|
||||
|
||||
<p>
|
||||
You can use the form below to send an e-mail to this website's
|
||||
administrator.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/contact">
|
||||
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||
<div class="form-group">
|
||||
<label for="name">
|
||||
Your name:
|
||||
<small class="form-text text-muted">(so I know who you are)</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="form-control"
|
||||
id="name"
|
||||
placeholder="Anonymous"
|
||||
value="{{ .Form.Name }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Your email:</label>
|
||||
<input type="email"
|
||||
name="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
placeholder="(if you want a response)"
|
||||
value="{{ .Form.Email }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subject">
|
||||
Message subject:
|
||||
<small class="form-text text-muted">(optional)</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
name="subject"
|
||||
class="form-control"
|
||||
id="subject"
|
||||
placeholder="No Subject"
|
||||
value="{{ .Form.Subject }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message:</label>
|
||||
<textarea class="form-control"
|
||||
cols="40"
|
||||
rows="12"
|
||||
name="message"
|
||||
id="message"
|
||||
placeholder="Message"
|
||||
required>{{ .Form.Message }}</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Send Message</button>
|
||||
|
||||
<div style="display: none">
|
||||
If you can see these boxes, don't touch them.<br>
|
||||
<input type="text" size="40" name="contact" value=""><br>
|
||||
<input type="text" size="40" name="website" value="http://">
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
|
@ -29,9 +29,3 @@ a.blog-title {
|
|||
font-size: smaller;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Code blocks treated as <pre> tags */
|
||||
.markdown code {
|
||||
display: block;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user