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)
|
app := core.New(DocumentRoot, userRoot)
|
||||||
if fDebug {
|
if fDebug {
|
||||||
app.Debug = true
|
app.Debug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set $JSONDB_DEBUG=1 to debug JsonDB; it's very noisy!
|
||||||
|
if os.Getenv("JSONDB_DEBUG") != "" {
|
||||||
jsondb.SetDebug(true)
|
jsondb.SetDebug(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.ListenAndServe(fAddress)
|
app.ListenAndServe(fAddress)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
r.HandleFunc("/login", blog.LoginHandler)
|
r.HandleFunc("/login", blog.LoginHandler)
|
||||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||||
blog.AdminRoutes(r)
|
blog.AdminRoutes(r)
|
||||||
|
blog.ContactRoutes(r)
|
||||||
blog.BlogRoutes(r)
|
blog.BlogRoutes(r)
|
||||||
blog.CommentRoutes(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
|
// Public routes
|
||||||
r.HandleFunc("/blog", b.IndexHandler)
|
r.HandleFunc("/blog", b.IndexHandler)
|
||||||
r.HandleFunc("/archive", b.BlogArchive)
|
r.HandleFunc("/archive", b.BlogArchive)
|
||||||
|
r.HandleFunc("/tagged", b.Tagged)
|
||||||
r.HandleFunc("/tagged/{tag}", b.Tagged)
|
r.HandleFunc("/tagged/{tag}", b.Tagged)
|
||||||
|
|
||||||
// Login-required routers.
|
// Login-required routers.
|
||||||
|
@ -75,7 +76,9 @@ func (b *Blog) Tagged(w http.ResponseWriter, r *http.Request) {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
tag, ok := params["tag"]
|
tag, ok := params["tag"]
|
||||||
if !ok {
|
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, "")
|
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)
|
log.Error("couldn't load full post data for ID %d (found in index.json)", pool[i].ID)
|
||||||
continue
|
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.
|
// Look up the author's information.
|
||||||
author, err := users.LoadReadonly(post.AuthorID)
|
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{
|
view = append(view, PostMeta{
|
||||||
Post: post,
|
Post: post,
|
||||||
Rendered: rendered,
|
|
||||||
Author: author,
|
Author: author,
|
||||||
NumComments: numComments,
|
NumComments: numComments,
|
||||||
})
|
})
|
||||||
|
@ -247,6 +235,31 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
||||||
return template.HTML(output.String())
|
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.
|
// BlogArchive summarizes all blog entries in an archive view.
|
||||||
func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
|
||||||
idx, err := posts.GetIndex()
|
idx, err := posts.GetIndex()
|
||||||
|
@ -266,7 +279,7 @@ func (b *Blog) BlogArchive(w http.ResponseWriter, r *http.Request) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
label := post.Created.Format("2006-02")
|
label := post.Created.Format("2006-01")
|
||||||
if _, ok := byMonth[label]; !ok {
|
if _, ok := byMonth[label]; !ok {
|
||||||
months = append(months, label)
|
months = append(months, label)
|
||||||
byMonth[label] = &Archive{
|
byMonth[label] = &Archive{
|
||||||
|
@ -335,13 +348,14 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa
|
||||||
var snipped bool
|
var snipped bool
|
||||||
if indexView {
|
if indexView {
|
||||||
if strings.Contains(p.Body, "<snip>") {
|
if strings.Contains(p.Body, "<snip>") {
|
||||||
log.Warn("HAS SNIP TAG!")
|
|
||||||
parts := strings.SplitN(p.Body, "<snip>", 2)
|
parts := strings.SplitN(p.Body, "<snip>", 2)
|
||||||
p.Body = parts[0]
|
p.Body = parts[0]
|
||||||
snipped = true
|
snipped = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.Body = strings.Replace(p.Body, "<snip>", "<div id=\"snip\"></div>", 1)
|
||||||
|
|
||||||
// Render the post to HTML.
|
// Render the post to HTML.
|
||||||
var rendered template.HTML
|
var rendered template.HTML
|
||||||
if p.ContentType == string(MARKDOWN) {
|
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.
|
// Email configuration.
|
||||||
type Email struct {
|
type Email struct {
|
||||||
To string
|
To string
|
||||||
|
ReplyTo string
|
||||||
Admin bool /* admin view of the email */
|
Admin bool /* admin view of the email */
|
||||||
Subject string
|
Subject string
|
||||||
UnsubscribeURL string
|
UnsubscribeURL string
|
||||||
|
@ -71,6 +72,9 @@ func (b *Blog) SendEmail(email Email) {
|
||||||
m := gomail.NewMessage()
|
m := gomail.NewMessage()
|
||||||
m.SetHeader("From", fmt.Sprintf("%s <%s>", s.Site.Title, s.Mail.Sender))
|
m.SetHeader("From", fmt.Sprintf("%s <%s>", s.Site.Title, s.Mail.Sender))
|
||||||
m.SetHeader("To", email.To)
|
m.SetHeader("To", email.To)
|
||||||
|
if email.ReplyTo != "" {
|
||||||
|
m.SetHeader("Reply-To", email.ReplyTo)
|
||||||
|
}
|
||||||
m.SetHeader("Subject", email.Subject)
|
m.SetHeader("Subject", email.Subject)
|
||||||
m.SetBody("text/plain", plaintext)
|
m.SetBody("text/plain", plaintext)
|
||||||
m.AddAlternative("text/html", html.String())
|
m.AddAlternative("text/html", html.String())
|
||||||
|
@ -81,6 +85,8 @@ func (b *Blog) SendEmail(email Email) {
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info("SendEmail: %s (%s) to %s", email.Subject, email.Template, email.To)
|
||||||
if err := d.DialAndSend(m); err != nil {
|
if err := d.DialAndSend(m); err != nil {
|
||||||
log.Error("SendEmail: %s", err.Error())
|
log.Error("SendEmail: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ var (
|
||||||
reMarkdownTitle = regexp.MustCompile(`(?m:^#([^#\r\n]+)$)`)
|
reMarkdownTitle = regexp.MustCompile(`(?m:^#([^#\r\n]+)$)`)
|
||||||
|
|
||||||
// Match fenced code blocks with languages defined.
|
// 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.
|
// Regexp to match fenced code blocks in rendered Markdown HTML.
|
||||||
// Tweak this if you change Markdown engines later.
|
// 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 {
|
func (b *Blog) RenderTrustedMarkdown(input string) string {
|
||||||
// Find and hang on to fenced code blocks.
|
// Find and hang on to fenced code blocks.
|
||||||
codeBlocks := []codeBlock{}
|
codeBlocks := []codeBlock{}
|
||||||
log.Info("RE: %s", reFencedCode.String())
|
|
||||||
matches := reFencedCode.FindAllStringSubmatch(input, -1)
|
matches := reFencedCode.FindAllStringSubmatch(input, -1)
|
||||||
for i, m := range matches {
|
for i, m := range matches {
|
||||||
language, source := m[1], m[2]
|
language, source := m[1], m[2]
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package posts
|
package posts
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// UpdateIndex updates a post's metadata in the blog index.
|
// UpdateIndex updates a post's metadata in the blog index.
|
||||||
func UpdateIndex(p *Post) error {
|
func UpdateIndex(p *Post) error {
|
||||||
|
@ -68,6 +71,59 @@ func (idx *Index) Delete(p *Post) error {
|
||||||
return DB.Commit("blog/index", idx)
|
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.
|
// CleanupFragments to clean up old URL fragments.
|
||||||
func CleanupFragments() error {
|
func CleanupFragments() error {
|
||||||
idx, err := GetIndex()
|
idx, err := GetIndex()
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
// PageHandler is the catch-all route handler, for serving static web pages.
|
// PageHandler is the catch-all route handler, for serving static web pages.
|
||||||
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
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.
|
// Remove trailing slashes by redirecting them away.
|
||||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
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?
|
// 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)
|
b.RenderTemplate(w, r, filepath.URI, nil)
|
||||||
return
|
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."}
|
message = []string{"The page you were looking for was not found."}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error("HERE 2")
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
|
|
|
@ -121,6 +121,7 @@ func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, wi
|
||||||
"Now": time.Now,
|
"Now": time.Now,
|
||||||
"RenderIndex": b.RenderIndex,
|
"RenderIndex": b.RenderIndex,
|
||||||
"RenderPost": b.RenderPost,
|
"RenderPost": b.RenderPost,
|
||||||
|
"RenderTags": b.RenderTags,
|
||||||
}
|
}
|
||||||
if functions != nil {
|
if functions != nil {
|
||||||
for name, fn := range functions {
|
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...)
|
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
log.Debug("Parsed template")
|
|
||||||
|
|
||||||
return nil
|
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">
|
<li class="nav-item">
|
||||||
<a href="/archive" class="nav-link">Archive</a>
|
<a href="/archive" class="nav-link">Archive</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/contact" class="nav-link">Contact Me</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form class="form-inline mt-2 mt-md-0">
|
<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 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2">
|
<div class="markdown col-12 col-lg-1 mb-1">
|
||||||
<img src="{{ .Avatar }}"
|
<img src="{{ .Avatar }}"
|
||||||
width="96"
|
width="96"
|
||||||
height="96"
|
height="96"
|
||||||
alt="Avatar image">
|
alt="Avatar image">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-10">
|
<div class="markdown col-12 col-lg-11">
|
||||||
<div class="comment-meta">
|
<div class="comment-meta">
|
||||||
{{ if and .UserID .Username }}
|
{{ if and .UserID .Username }}
|
||||||
<a href="/u/{{ .Username }}"><strong>{{ or .Name "Anonymous" }}</strong></a>
|
<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;
|
font-size: smaller;
|
||||||
margin-bottom: 1rem;
|
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