Breaking! Render everything using go functions, remove custom pages support and now unused template rendering

pull/25/head
Jan-Lukas Else 8 months ago
parent 6dc36e697f
commit 1c7195a135
  1. 3
      app.go
  2. 2
      authentication.go
  3. 2
      blogroll.go
  4. 4
      blogstats.go
  5. 2
      captcha.go
  6. 2
      comments.go
  7. 14
      commentsAdmin.go
  8. 9
      config.go
  9. 4
      contact.go
  10. 13
      customPages.go
  11. 18
      editor.go
  12. 4
      editorFiles.go
  13. 2
      errors.go
  14. 9
      example-config.yml
  15. 4
      geoMap.go
  16. 8
      go.mod
  17. 16
      go.sum
  18. 26
      httpRouters.go
  19. 2
      indieAuthServer.go
  20. 4
      main.go
  21. 2
      notifications.go
  22. 10
      original-assets/styles/styles.scss
  23. 4
      posts.go
  24. 90
      render.go
  25. 2
      search.go
  26. 6
      sitemap.go
  27. 2
      taxonomies.go
  28. 14
      templates/assets/css/styles.css
  29. 46
      templates/base.gohtml
  30. 36
      templates/commentsadmin.gohtml
  31. 66
      templates/editor.gohtml
  32. 43
      templates/webmentionadmin.gohtml
  33. 335
      ui.go
  34. 25
      uiComponents.go
  35. 5
      ui_test.go
  36. 5
      utils.go
  37. 16
      webmentionAdmin.go

@ -2,7 +2,6 @@ package main
import (
"crypto/rsa"
"html/template"
"net/http"
"sync"
@ -70,8 +69,6 @@ type goBlog struct {
min minify.Minifier
// Regex Redirects
regexRedirects []*regexRedirect
// Rendering
templates map[string]*template.Template
// Sessions
loginSessions, captchaSessions *dbSessionStore
// Shutdown

@ -62,7 +62,7 @@ func (a *goBlog) authMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm()
b = []byte(r.PostForm.Encode())
}
a.renderNew(w, r, a.renderLogin, &renderData{
a.render(w, r, a.renderLogin, &renderData{
Data: &loginRenderData{
loginMethod: r.Method,
loginHeaders: base64.StdEncoding.EncodeToString(h),

@ -29,7 +29,7 @@ func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
}
c := bc.Blogroll
can := bc.getRelativePath(defaultIfEmpty(c.Path, defaultBlogrollPath))
a.renderNew(w, r, a.renderBlogroll, &renderData{
a.render(w, r, a.renderBlogroll, &renderData{
Canonical: a.getFullAddress(can),
Data: &blogrollRenderData{
title: c.Title,

@ -25,7 +25,7 @@ func (a *goBlog) initBlogStats() {
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
canonical := bc.getRelativePath(defaultIfEmpty(bc.BlogStats.Path, defaultBlogStatsPath))
a.renderNew(w, r, a.renderBlogStats, &renderData{
a.render(w, r, a.renderBlogStats, &renderData{
Canonical: a.getFullAddress(canonical),
Data: &blogStatsRenderData{
tableUrl: canonical + blogStatsTablePath,
@ -43,7 +43,7 @@ func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
return
}
// Render
a.renderNew(w, r, a.renderBlogStatsTable, &renderData{
a.render(w, r, a.renderBlogStatsTable, &renderData{
Data: data,
})
}

@ -65,7 +65,7 @@ func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler {
// Render captcha
_ = ses.Save(r, w)
w.Header().Set("Cache-Control", "no-store,max-age=0")
a.renderNewWithStatusCode(w, r, http.StatusUnauthorized, a.renderCaptcha, &renderData{
a.renderWithStatusCode(w, r, http.StatusUnauthorized, a.renderCaptcha, &renderData{
Data: &captchaRenderData{
captchaMethod: r.Method,
captchaHeaders: base64.StdEncoding.EncodeToString(h),

@ -41,7 +41,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
return
}
_, bc := a.getBlog(r)
a.renderNew(w, r, a.renderComment, &renderData{
a.render(w, r, a.renderComment, &renderData{
Canonical: a.getFullAddress(bc.getRelativePath(path.Join(commentPath, strconv.Itoa(id)))),
Data: comment,
})

@ -70,13 +70,13 @@ func (a *goBlog) commentsAdmin(w http.ResponseWriter, r *http.Request) {
}
nextPath = fmt.Sprintf("%s/page/%d", commentsPath, nextPage)
// Render
a.render(w, r, templateCommentsAdmin, &renderData{
Data: map[string]interface{}{
"Comments": comments,
"HasPrev": hasPrev,
"HasNext": hasNext,
"Prev": prevPath,
"Next": nextPath,
a.render(w, r, a.renderCommentsAdmin, &renderData{
Data: &commentsRenderData{
comments: comments,
hasPrev: hasPrev,
hasNext: hasNext,
prev: prevPath,
next: nextPath,
},
})
}

@ -81,7 +81,6 @@ type configBlog struct {
Search *configSearch `mapstructure:"search"`
BlogStats *configBlogStats `mapstructure:"blogStats"`
Blogroll *configBlogroll `mapstructure:"blogroll"`
CustomPages []*configCustomPage `mapstructure:"custompages"`
Telegram *configTelegram `mapstructure:"telegram"`
PostAsHome bool `mapstructure:"postAsHome"`
RandomPost *configRandomPost `mapstructure:"randomPost"`
@ -148,14 +147,6 @@ type configBlogroll struct {
Description string `mapstructure:"description"`
}
type configCustomPage struct {
Path string `mapstructure:"path"`
Template string `mapstructure:"template"`
Cache bool `mapstructure:"cache"`
CacheExpiration int `mapstructure:"cacheExpiration"`
Data *interface{} `mapstructure:"data"`
}
type configRandomPost struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`

@ -17,7 +17,7 @@ const defaultContactPath = "/contact"
func (a *goBlog) serveContactForm(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
cc := bc.Contact
a.renderNew(w, r, a.renderContact, &renderData{
a.render(w, r, a.renderContact, &renderData{
Data: &contactRenderData{
title: cc.Title,
description: cc.Description,
@ -63,7 +63,7 @@ func (a *goBlog) sendContactSubmission(w http.ResponseWriter, r *http.Request) {
// Send notification
a.sendNotification(message.String())
// Give feedback
a.renderNew(w, r, a.renderContact, &renderData{
a.render(w, r, a.renderContact, &renderData{
Data: &contactRenderData{
sent: true,
},

@ -1,13 +0,0 @@
package main
import "net/http"
const customPageContextKey = "custompage"
func (a *goBlog) serveCustomPage(w http.ResponseWriter, r *http.Request) {
page := r.Context().Value(customPageContextKey).(*configCustomPage)
a.render(w, r, page.Template, &renderData{
Canonical: a.getFullAddress(page.Path),
Data: page.Data,
})
}

@ -21,8 +21,8 @@ import (
const editorPath = "/editor"
func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) {
a.render(w, r, templateEditor, &renderData{
Data: map[string]interface{}{},
a.render(w, r, a.renderEditor, &renderData{
Data: &editorRenderData{},
})
}
@ -89,10 +89,10 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
a.render(w, r, templateEditor, &renderData{
Data: map[string]interface{}{
"UpdatePostURL": a.fullPostURL(post),
"UpdatePostContent": a.postToMfItem(post).Properties.Content[0],
a.render(w, r, a.renderEditor, &renderData{
Data: &editorRenderData{
updatePostUrl: a.fullPostURL(post),
updatePostContent: a.postToMfItem(post).Properties.Content[0],
},
})
case "updatepost":
@ -188,14 +188,13 @@ func (a *goBlog) editorMicropubPost(w http.ResponseWriter, r *http.Request, medi
_ = result.Body.Close()
}
func (a *goBlog) editorPostTemplate(blog string) string {
func (a *goBlog) editorPostTemplate(blog string, bc *configBlog) string {
var builder strings.Builder
marsh := func(param string, i interface{}) {
_ = yaml.NewEncoder(&builder).Encode(map[string]interface{}{
param: i,
})
}
bc := a.cfg.Blogs[blog]
builder.WriteString("---\n")
marsh("blog", blog)
marsh("section", bc.DefaultSection)
@ -210,8 +209,7 @@ func (a *goBlog) editorPostTemplate(blog string) string {
return builder.String()
}
func (a *goBlog) editorPostDesc(blog string) string {
bc := a.cfg.Blogs[blog]
func (a *goBlog) editorPostDesc(bc *configBlog) string {
t := a.ts.GetTemplateStringVariant(bc.Lang, "editorpostdesc")
var paramBuilder, statusBuilder strings.Builder
for i, param := range []string{

@ -16,7 +16,7 @@ func (a *goBlog) serveEditorFiles(w http.ResponseWriter, r *http.Request) {
}
// Check if files at all
if len(files) == 0 {
a.renderNew(w, r, a.renderEditorFiles, &renderData{
a.render(w, r, a.renderEditorFiles, &renderData{
Data: &editorFilesRenderData{},
})
return
@ -39,7 +39,7 @@ func (a *goBlog) serveEditorFiles(w http.ResponseWriter, r *http.Request) {
return
}
// Serve HTML
a.renderNew(w, r, a.renderEditorFiles, &renderData{
a.render(w, r, a.renderEditorFiles, &renderData{
Data: &editorFilesRenderData{
files: files,
uses: uses,

@ -35,7 +35,7 @@ func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message stri
http.Error(w, message, status)
return
}
a.renderNewWithStatusCode(w, r, status, a.renderError, &renderData{
a.renderWithStatusCode(w, r, status, a.renderError, &renderData{
Data: &errorRenderData{
Title: fmt.Sprintf("%d %s", status, http.StatusText(status)),
Message: message,

@ -222,15 +222,6 @@ blogs:
authValue: abc # Authentication value for OPML
categories: # Optional, allow only these categories
- Blogs
# Custom pages
custompages:
- path: /blogroll # Path
template: blogroll # Template
cache: true # Enable caching
cacheExpiration: 600 # Cache expiration (default uses blog cache TTL)
data: # Data to provide to template
Title: Blogroll
Description: "This are alphabetically sorted lists of blogs and sites I subscribe to."
# Redirect to random post
randomPost:
enabled: true # Enable

@ -25,7 +25,7 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
}
if len(allPostsWithLocation) == 0 {
a.renderNew(w, r, a.renderGeoMap, &renderData{
a.render(w, r, a.renderGeoMap, &renderData{
Canonical: canonical,
Data: &geoMapRenderData{
noLocations: true,
@ -85,7 +85,7 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
tracksJson = string(tracksJsonBytes)
}
a.renderNew(w, r, a.renderGeoMap, &renderData{
a.render(w, r, a.renderGeoMap, &renderData{
Canonical: canonical,
Data: &geoMapRenderData{
locations: locationsJson,

@ -35,7 +35,7 @@ require (
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lopezator/migrator v0.3.0
github.com/mattn/go-sqlite3 v1.14.10
github.com/mattn/go-sqlite3 v1.14.11
github.com/microcosm-cc/bluemonday v1.0.17
github.com/mmcdole/gofeed v1.1.3
github.com/paulmach/go.geojson v1.4.0
@ -46,7 +46,7 @@ require (
github.com/spf13/cast v1.4.1
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tdewolff/minify/v2 v2.9.29
github.com/tdewolff/minify/v2 v2.10.0
github.com/thoas/go-funk v0.9.1
github.com/tkrajina/gpxgo v1.2.0
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
@ -55,8 +55,8 @@ require (
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8
golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b

@ -314,8 +314,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsFrzQ=
github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60 h1:tHdB+hQRHU10CfcK0furo6rSNgZ38JT8uPh70c/pFD8=
@ -415,8 +415,8 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tdewolff/minify/v2 v2.9.29 h1:QMVJaCJzWL0mXS33cX792YD074xz4lOhkyBS8hAzYAY=
github.com/tdewolff/minify/v2 v2.9.29/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM=
github.com/tdewolff/minify/v2 v2.10.0 h1:ovVAHUcjfGrBDf1EIvsodRUVJiZK/28mMose08B7k14=
github.com/tdewolff/minify/v2 v2.10.0/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM=
github.com/tdewolff/parse/v2 v2.5.27 h1:PL3LzzXaOpmdrknnOlIeO2muIBHAwiKp6TxN1RbU5gI=
github.com/tdewolff/parse/v2 v2.5.27/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
@ -472,8 +472,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8 h1:kACShD3qhmr/3rLmg1yXyt+N4HcwutKyPRB93s54TIU=
golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA=
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -553,8 +553,8 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba h1:6u6sik+bn/y7vILcYkK3iwTBWN7WtBvB0+SZswQnbf8=
golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

@ -128,9 +128,6 @@ func (a *goBlog) blogRouter(blog string, conf *configBlog) func(r chi.Router) {
// Search
r.Group(a.blogSearchRouter(conf))
// Custom pages
r.Group(a.blogCustomPagesRouter(conf))
// Random post
r.Group(a.blogRandomRouter(conf))
@ -295,29 +292,6 @@ func (a *goBlog) blogSearchRouter(conf *configBlog) func(r chi.Router) {
}
}
// Blog - Custom pages
func (a *goBlog) blogCustomPagesRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {
r.Use(a.privateModeHandler)
for _, cp := range conf.CustomPages {
r.Group(func(r chi.Router) {
r.Use(middleware.WithValue(customPageContextKey, cp))
if cp.Cache {
ce := cp.CacheExpiration
if ce == 0 {
ce = a.defaultCacheExpiration()
}
r.Use(
a.cacheMiddleware,
middleware.WithValue(cacheExpirationKey, ce),
)
}
r.Get(cp.Path, a.serveCustomPage)
})
}
}
}
// Blog - Random
func (a *goBlog) blogRandomRouter(conf *configBlog) func(r chi.Router) {
return func(r chi.Router) {

@ -31,7 +31,7 @@ func (a *goBlog) indieAuthRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Render page that let's the user authorize the app
a.renderNew(w, r, a.renderIndieAuth, &renderData{
a.render(w, r, a.renderIndieAuth, &renderData{
Data: iareq,
})
}

@ -154,10 +154,6 @@ func (app *goBlog) initComponents(logging bool) {
app.logErrAndQuit("Failed to init template translations:", err.Error())
return
}
if err = app.initRendering(); err != nil { // Needs assets and minify
app.logErrAndQuit("Failed to init HTML rendering:", err.Error())
return
}
if err = app.initCache(); err != nil {
app.logErrAndQuit("Failed to init HTTP cache:", err.Error())
return

@ -154,7 +154,7 @@ func (a *goBlog) notificationsAdmin(w http.ResponseWriter, r *http.Request) {
}
nextPath = fmt.Sprintf("%s/page/%d", notificationsPath, nextPage)
// Render
a.renderNew(w, r, a.renderNotificationsAdmin, &renderData{
a.render(w, r, a.renderNotificationsAdmin, &renderData{
Data: &notificationsRenderData{
notifications: notifications,
hasPrev: hasPrev,

@ -250,11 +250,7 @@ details summary {
margin-bottom: 5px;
}
#map {
height: 400px;
}
#post-actions, #posteditactions {
.actions {
@extend .p;
display: flex;
flex-wrap: wrap;
@ -265,6 +261,10 @@ details summary {
}
}
#map {
height: 400px;
}
#announcement {
@extend .invert;
padding: 5px;

@ -83,7 +83,7 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
status = http.StatusGone
}
a.renderNewWithStatusCode(w, r, status, renderMethod, &renderData{
a.renderWithStatusCode(w, r, status, renderMethod, &renderData{
BlogString: p.Blog,
Canonical: canonical,
Data: p,
@ -356,7 +356,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
if summaryTemplate == "" {
summaryTemplate = defaultSummary
}
a.renderNew(w, r, a.renderIndex, &renderData{
a.render(w, r, a.renderIndex, &renderData{
Canonical: a.getFullAddress(path),
Data: &indexRenderData{
title: title,

@ -1,76 +1,12 @@
package main
import (
"html/template"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype"
)
const (
templatesDir = "templates"
templatesExt = ".gohtml"
templateBase = "base"
templateEditor = "editor"
templateCommentsAdmin = "commentsadmin"
templateWebmentionAdmin = "webmentionadmin"
)
func (a *goBlog) initRendering() error {
a.templates = map[string]*template.Template{}
templateFunctions := template.FuncMap{
"md": a.safeRenderMarkdownAsHTML,
"mdtitle": a.renderMdTitle,
"html": wrapStringAsHTML,
// Code based rendering
"tor": func(rd *renderData) template.HTML {
buf := bufferpool.Get()
hb := newHtmlBuilder(buf)
a.renderTorNotice(hb, rd)
res := template.HTML(buf.String())
bufferpool.Put(buf)
return res
},
// Others
"dateformat": dateFormat,
"unixtodate": unixToLocalDateString,
"now": localNowString,
"asset": a.assetFileName,
"string": a.ts.GetTemplateStringVariantFunc(),
"absolute": a.getFullAddress,
"opensearch": openSearchUrl,
"editortemplate": a.editorPostTemplate,
"editorpostdesc": a.editorPostDesc,
}
baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt))
if err != nil {
return err
}
err = filepath.Walk(templatesDir, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.Mode().IsRegular() && path.Ext(p) == templatesExt {
if name := strings.TrimSuffix(path.Base(p), templatesExt); name != templateBase {
if a.templates[name], err = template.Must(baseTemplate.Clone()).New(name).ParseFiles(p); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
type renderData struct {
BlogString string
Canonical string
@ -91,31 +27,11 @@ func (d *renderData) LoggedIn() bool {
return d.app.isLoggedIn(d.req)
}
func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, data *renderData) {
a.renderWithStatusCode(w, r, http.StatusOK, template, data)
}
func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, template string, data *renderData) {
// Check render data
a.checkRenderData(r, data)
// Set content type
w.Header().Set(contentType, contenttype.HTMLUTF8)
// Render template and write minified HTML
buf := bufferpool.Get()
defer bufferpool.Put(buf)
if err := a.templates[template].ExecuteTemplate(buf, template, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(statusCode)
_ = a.min.Minify(contenttype.HTML, w, buf)
}
func (a *goBlog) renderNew(w http.ResponseWriter, r *http.Request, f func(*htmlBuilder, *renderData), data *renderData) {
a.renderNewWithStatusCode(w, r, http.StatusOK, f, data)
func (a *goBlog) render(w http.ResponseWriter, r *http.Request, f func(*htmlBuilder, *renderData), data *renderData) {
a.renderWithStatusCode(w, r, http.StatusOK, f, data)
}
func (a *goBlog) renderNewWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, f func(*htmlBuilder, *renderData), data *renderData) {
func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, f func(*htmlBuilder, *renderData), data *renderData) {
// Check render data
a.checkRenderData(r, data)
// Set content type

@ -26,7 +26,7 @@ func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
return
}
a.renderNew(w, r, a.renderSearch, &renderData{
a.render(w, r, a.renderSearch, &renderData{
Canonical: a.getFullAddress(servePath),
})
}

@ -102,12 +102,6 @@ func (a *goBlog) serveSitemapBlogFeatures(w http.ResponseWriter, r *http.Request
Loc: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(cc.Path, defaultContactPath))),
})
}
// Custom pages
for _, cp := range bc.CustomPages {
sm.Add(&sitemap.URL{
Loc: a.getFullAddress(cp.Path),
})
}
// Write sitemap
a.writeSitemapXML(w, sm)
}

@ -20,7 +20,7 @@ func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
a.renderNew(w, r, a.renderTaxonomy, &renderData{
a.render(w, r, a.renderTaxonomy, &renderData{
Canonical: a.getFullAddress(r.URL.Path),
Data: &taxonomyRenderData{
taxonomy: tax,

@ -144,7 +144,7 @@ details summary > *:first-child {
border-bottom: 1px solid var(--primary, #000);
}
.p, #post-actions, #posteditactions, table {
.p, .actions, table {
display: block;
margin-top: 1em;
margin-bottom: 1em;
@ -210,19 +210,19 @@ details summary > *:first-child {
margin-bottom: 5px;
}
#map {
height: 400px;
}
#post-actions, #posteditactions {
.actions {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
#post-actions *, #posteditactions * {
.actions * {
text-align: center;
}
#map {
height: 400px;
}
#announcement {
padding: 5px;
text-align: center;

@ -1,46 +0,0 @@
{{ define "base" }}
<!doctype html>
<html lang="{{ .Blog.Lang }}">
<meta charset="utf-8">
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="{{ asset "css/styles.css" }}">
{{ with .Canonical }}<link rel="canonical" href="{{ . }}">{{ end }}
{{ block "title" . }}<title>{{ mdtitle .Blog.Title }}</title>{{ end }}
<link rel="alternate" type="application/rss+xml" title="RSS ({{ mdtitle .Blog.Title }})" href="{{ .Blog.Path }}.rss"/>
<link rel="alternate" type="application/atom+xml" title="Atom ({{ mdtitle .Blog.Title }})" href="{{ .Blog.Path }}.atom"/>
<link rel="alternate" type="application/feed+json" title="JSON Feed ({{ mdtitle .Blog.Title }})" href="{{ .Blog.Path }}.json"/>
<link rel="webmention" href="{{ absolute "/webmention" }}" />
<link rel="micropub" href="/micropub" />
<link rel="authorization_endpoint" href="/indieauth" />
<link rel="token_endpoint" href="/indieauth/token" />
{{ with .User }}{{ range .Identities }}<link rel="me" href="{{ . }}" />{{ end }}{{ end }}
{{ $os := opensearch .Blog }}
{{ if $os }}<link rel="search" type="application/opensearchdescription+xml" href="{{ $os }}" title="{{ mdtitle .Blog.Title }}" />{{ end }}
{{ with .Blog.Announcement }}{{ with .Text }}<div id="announcement" data-nosnippet>{{ md . }}</div>{{ end }}{{ end }}
<header>
<h1><a href="{{ .Blog.RelativePath "/" }}" rel="home" title="{{ mdtitle .Blog.Title }}" translate="no">{{ mdtitle .Blog.Title }}</a></h1>
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
{{ with index .Blog.Menus "main" }}
<nav>{{ range $i, $item := .Items }}{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}</nav>
{{ end }}
{{ if .LoggedIn }}
<nav>
<a href="{{ .Blog.RelativePath "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
&bull; <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
{{ if .WebmentionReceivingEnabled }}&bull; <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>{{ end }}
{{ if .CommentsEnabled }}&bull; <a href="{{ .Blog.RelativePath "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
&bull; <a href="/logout">{{ string .Blog.Lang "logout" }}</a>
</nav>
{{ end }}
</header>
{{ block "main" . }}{{ end }}
<footer>
{{ with index .Blog.Menus "footer" }}
<nav>{{ range $i, $item := .Items }}{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}</nav>
{{ end }}
<p translate="no">&copy; {{ dateformat now "2006" }} {{ with .User.Name }}{{ . }}{{ else }}{{ mdtitle .Blog.Title }}{{ end }}</p>
{{ tor . }}
</footer>
{{ if .EasterEgg }}<script defer src="{{ asset "js/easteregg.js" }}"></script>{{ end }}
</html>
{{ end }}

@ -1,36 +0,0 @@
{{ define "title" }}
<title>{{ string .Blog.Lang "comments" }} - {{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<h1>{{ string .Blog.Lang "comments" }}</h1>
{{ $blog := .Blog }}
{{ range $i, $comment := .Data.Comments }}
<div class="p">
<p>
ID: {{ $comment.ID }}<br/>
Target: <a href="{{ $comment.Target }}" target="_blank">{{ $comment.Target }}</a><br/>
Name: {{ if $comment.Website }}<a href="{{ $comment.Website }}" target="_blank" rel="nofollow noopener noreferrer ugc">{{ $comment.Name }}</a>{{ else }}{{ $comment.Name }}{{ end }}
</p>
<p>
{{ html $comment.Comment }}
</p>
<form method="post">
<input type="hidden" name="commentid" value="{{ $comment.ID }}">
<input type="submit" formaction="comment/delete" value="{{ string $blog.Lang "delete" }}">
</form>
</div>
{{ end }}
{{ if .Data.HasPrev }}
<p><a href="{{ .Data.Prev }}">{{ string .Blog.Lang "prev" }}</a></p>
{{ end }}
{{ if .Data.HasNext }}
<p><a href="{{ .Data.Next }}">{{ string .Blog.Lang "next" }}</a></p>
{{ end }}
</main>
{{ end }}
{{ define "commentsadmin" }}
{{ template "base" . }}
{{ end }}

@ -1,66 +0,0 @@
{{ define "title" }}
<link rel="stylesheet" href="{{ asset "css/chroma.css" }}">
<title>{{ string .Blog.Lang "editor" }} - {{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<h1>{{ string .Blog.Lang "editor" }}</h1>
<h2>{{ string .Blog.Lang "create" }}</h2>
{{ md (editorpostdesc .BlogString) }}
<form class="fw p" method="post">
<input type="hidden" name="h" value="entry">
<textarea name="content" class="monospace h400p formcache mdpreview" id="create-input" data-preview="post-preview" data-previewws="{{ .Blog.RelativePath "/editor/preview" }}">{{ editortemplate .BlogString }}</textarea>
<div id="post-preview" class="hide"></div>
<input type="submit" value="{{ string .Blog.Lang "create" }}">
</form>
{{ if .Data.UpdatePostURL }}
<h2 id="update">{{ string .Blog.Lang "update" }}</h2>
<form class="fw p" method="post" action="#update">
<input type="hidden" name="editoraction" value="updatepost">
<input type="hidden" name="url" value="{{ .Data.UpdatePostURL }}">
<textarea name="content" class="monospace h400p mdpreview" data-preview="update-preview" data-previewws="{{ .Blog.RelativePath "/editor/preview" }}">{{ .Data.UpdatePostContent }}</textarea>
<div id="update-preview" class="hide"></div>
<input type="submit" value="{{ string .Blog.Lang "update" }}">
</form>
{{ end }}
<h2>{{ string .Blog.Lang "posts" }}</h2>
<p><a href="{{ .Blog.RelativePath "/editor/drafts" }}">{{ string .Blog.Lang "drafts" }}</a></p>
<p><a href="{{ .Blog.RelativePath "/editor/private" }}">{{ string .Blog.Lang "privateposts" }}</a></p>
<p><a href="{{ .Blog.RelativePath "/editor/unlisted" }}">{{ string .Blog.Lang "unlistedposts" }}</a></p>
<p><a href="{{ .Blog.RelativePath "/editor/scheduled" }}">{{ string .Blog.Lang "scheduledposts" }}</a></p>
<p><a href="{{ .Blog.RelativePath "/editor/deleted" }}">{{ string .Blog.Lang "deletedposts" }}</a></p>
<h2>{{ string .Blog.Lang "upload" }}</h2>
<form class="fw p" method="post" enctype="multipart/form-data">
<input type="hidden" name="editoraction" value="upload">
<input type="file" name="file">
<input type="submit" value="{{ string .Blog.Lang "upload" }}">
</form>
<p><a href="{{ .Blog.RelativePath "/editor/files" }}">{{ string .Blog.Lang "mediafiles" }}</a></p>
<h2>{{ string .Blog.Lang "location" }}</h2>
<form class="fw p">
<input id="geobtn" type="button" value="{{ string .Blog.Lang "locationget" }}" data-failed="{{ string .Blog.Lang "locationfailed" }}" data-notsupported="{{ string .Blog.Lang "locationnotsupported" }}">
<input id="geostatus" type="text" class="hide" readonly>
</form>
<h2>{{ string .Blog.Lang "gpxhelper" }}</h2>
<p>{{ string .Blog.Lang "gpxhelperdesc" }}</p>
<form class="fw p" method="post" enctype="multipart/form-data">
<input type="hidden" name="editoraction" value="helpgpx">
<input type="file" name="file">
<input type="submit" value="{{ string .Blog.Lang "upload" }}">
</form>
<script defer src="{{ asset "js/mdpreview.js" }}"></script>
<script defer src="{{ asset "js/geohelper.js" }}"></script>
<script defer src="{{ asset "js/formcache.js" }}"></script>
</main>
{{ end }}
{{ define "editor" }}
{{ template "base" . }}
{{ end }}

@ -1,43 +0,0 @@
{{ define "title" }}
<title>{{ string .Blog.Lang "webmentions" }} - {{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<h1>{{ string .Blog.Lang "webmentions" }}</h1>
{{ $blog := .Blog }}
{{ $current := .Data.Current }}
{{ range $i, $mention := .Data.Mentions }}
<div id="mention-{{ $mention.ID }}" class="p">
<p>
From: <a href="{{ $mention.Source }}" target="_blank" rel="noopener noreferrer">{{ $mention.Source }}</a><br/>
{{ if not (eq $mention.Source $mention.Url ) }}u-url: <a href="{{ $mention.Url }}" target="_blank" rel="noopener noreferrer">{{ $mention.Url }}</a><br/>{{ end }}
To: <a href="{{ $mention.Target }}" target="_blank">{{ $mention.Target }}</a><br/>
Created: {{ unixtodate $mention.Created }}<br/><br/>
{{ if $mention.Author }}{{ $mention.Author }}<br/>{{ end }}
{{ with $mention.Title }}<b>{{.}}</b><br/>{{ end }}
{{ with $mention.Content }}<i>{{.}}</i>{{ end }}
</p>
<form method="post">
<input type="hidden" name="mentionid" value="{{ $mention.ID }}">
<input type="hidden" name="redir" value="{{ $current }}#mention-{{ $mention.ID }}">
{{ if eq $mention.Status "verified" }}
<input type="submit" formaction="/webmention/approve" value="{{ string $blog.Lang "approve" }}">
{{ end }}
<input type="submit" formaction="/webmention/delete" value="{{ string $blog.Lang "delete" }}">
<input type="submit" formaction="/webmention/reverify" value="{{ string $blog.Lang "reverify" }}">
</form>
</div>
{{ end }}
{{ if .Data.HasPrev }}
<p><a href="{{ .Data.Prev }}">{{ string .Blog.Lang "prev" }}</a></p>
{{ end }}
{{ if .Data.HasNext }}
<p><a href="{{ .Data.Next }}">{{ string .Blog.Lang "next" }}</a></p>
{{ end }}
</main>
{{ end }}
{{ define "webmentionadmin" }}
{{ template "base" . }}
{{ end }}

335
ui.go

@ -336,7 +336,7 @@ func (a *goBlog) renderComment(h *htmlBuilder, rd *renderData) {
hb.writeElementClose("main")
// Interactions
if rd.CommentsEnabled {
a.renderInteractions(hb, rd.Blog, rd.Canonical)
a.renderInteractions(hb, rd)
}
},
)
@ -448,7 +448,7 @@ func (a *goBlog) renderBlogStats(hb *htmlBuilder, rd *renderData) {
hb.writeElementClose("main")
// Interactions
if rd.CommentsEnabled {
a.renderInteractions(hb, rd.Blog, rd.Canonical)
a.renderInteractions(hb, rd)
}
},
)
@ -620,7 +620,7 @@ func (a *goBlog) renderGeoMap(hb *htmlBuilder, rd *renderData) {
}
hb.writeElementClose("main")
if rd.CommentsEnabled {
a.renderInteractions(hb, rd.Blog, rd.Canonical)
a.renderInteractions(hb, rd)
}
},
)
@ -695,7 +695,7 @@ func (a *goBlog) renderBlogroll(hb *htmlBuilder, rd *renderData) {
hb.writeElementClose("main")
// Interactions
if rd.CommentsEnabled {
a.renderInteractions(hb, rd.Blog, rd.Canonical)
a.renderInteractions(hb, rd)
}
},
)
@ -891,7 +891,7 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
// Post meta
a.renderPostMeta(hb, p, rd.Blog, "post")
// Post actions
hb.writeElementOpen("div", "id", "post-actions")
hb.writeElementOpen("div", "class", "actions")
// Share button
hb.writeElementOpen("a", "class", "button", "href", fmt.Sprintf("https://www.addtoany.com/share#url=%s%s", a.shortPostURL(p), funk.ShortIf(p.RenderedTitle != "", "&title="+p.RenderedTitle, "")), "target", "_blank", "rel", "nofollow noopener noreferrer")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "share"))
@ -964,7 +964,7 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
hb.writeElementClose("main")
// Post edit actions
if rd.LoggedIn() {
hb.writeElementOpen("div", "id", "posteditactions")
hb.writeElementOpen("div", "class", "actions")
// Update
hb.writeElementOpen("form", "method", "post", "action", rd.Blog.RelativePath("/editor")+"#update")
hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "loadupdate")
@ -999,7 +999,7 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
}
// Comments
if rd.CommentsEnabled {
a.renderInteractions(hb, rd.Blog, rd.Canonical)
a.renderInteractions(hb, rd)
}
},
)
@ -1201,9 +1201,9 @@ func (a *goBlog) renderNotificationsAdmin(hb *htmlBuilder, rd *renderData) {
hb.writeElementClose("i")
hb.writeElementClose("p")
// Message
a.renderMarkdownToWriter(hb, n.Text, false)
_ = a.renderMarkdownToWriter(hb, n.Text, false)
// Delete form
hb.writeElementOpen("form", "method", "post", "action", "/notifications/delete")
hb.writeElementOpen("form", "class", "actions", "method", "post", "action", "/notifications/delete")
hb.writeElementOpen("input", "type", "hidden", "name", "notificationid", "value", n.ID)
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"))
hb.writeElementClose("form")
@ -1215,3 +1215,320 @@ func (a *goBlog) renderNotificationsAdmin(hb *htmlBuilder, rd *renderData) {
},
)
}
type commentsRenderData struct {
comments []*comment
hasPrev, hasNext bool
prev, next string
}
func (a *goBlog) renderCommentsAdmin(hb *htmlBuilder, rd *renderData) {
crd, ok := rd.Data.(*commentsRenderData)
if !ok {
return
}
a.renderBase(
hb, rd,
func(hb *htmlBuilder) {
a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments"))
},
func(hb *htmlBuilder) {
hb.writeElementOpen("main")
// Title
hb.writeElementOpen("h1")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "comments"))
hb.writeElementClose("h1")
// Notifications
for _, c := range crd.comments {
hb.writeElementOpen("div", "class", "p")
// ID, Target, Name
hb.writeElementOpen("p")
hb.writeEscaped("ID: ")
hb.writeEscaped(fmt.Sprintf("%d", c.ID))
hb.writeElementOpen("br")
hb.writeEscaped("Target: ")
hb.writeElementOpen("a", "href", c.Target, "target", "_blank")
hb.writeEscaped(c.Target)
hb.writeElementClose("a")
hb.writeElementOpen("br")
hb.writeEscaped("Name: ")
if c.Website != "" {
hb.writeElementOpen("a", "href", c.Website, "target", "_blank", "rel", "nofollow noopener noreferrer ugc")
}
hb.writeEscaped(c.Name)
if c.Website != "" {
hb.writeElementClose("a")
}
hb.writeElementClose("p")
// Comment
hb.writeElementOpen("p")
hb.write(c.Comment)
hb.writeElementClose("p")
// Delete form
hb.writeElementOpen("form", "class", "actions", "method", "post", "action", rd.Blog.getRelativePath("/comment/delete"))
hb.writeElementOpen("input", "type", "hidden", "name", "commentid", "value", c.ID)
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"))
hb.writeElementClose("form")
hb.writeElementClose("div")
}
// Pagination
a.renderPagination(hb, rd.Blog, crd.hasPrev, crd.hasNext, crd.prev, crd.next)
hb.writeElementClose("main")
},
)
}
type webmentionRenderData struct {
mentions []*mention
hasPrev, hasNext bool
prev, current, next string
}
func (a *goBlog) renderWebmentionAdmin(hb *htmlBuilder, rd *renderData) {
wrd, ok := rd.Data.(*webmentionRenderData)
if !ok {
return
}
a.renderBase(
hb, rd,
func(hb *htmlBuilder) {
a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions"))
},
func(hb *htmlBuilder) {
hb.writeElementOpen("main")
// Title
hb.writeElementOpen("h1")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions"))
hb.writeElementClose("h1")
// Notifications
for _, m := range wrd.mentions {
hb.writeElementOpen("div", "id", fmt.Sprintf("mention-%d", m.ID), "class", "p")
hb.writeElementOpen("p")
// Source
hb.writeEscaped("From: ")
hb.writeElementOpen("a", "href", m.Source, "target", "_blank", "rel", "noopener noreferrer")
hb.writeEscaped(m.Source)
hb.writeElementClose("a")
hb.writeElementOpen("br")
// u-url
if m.Source != m.Url {
hb.writeEscaped("u-url: ")
hb.writeElementOpen("a", "href", m.Url, "target", "_blank", "rel", "noopener noreferrer")
hb.writeEscaped(m.Url)
hb.writeElementClose("a")
hb.writeElementOpen("br")
}
// Target
hb.writeEscaped("To: ")
hb.writeElementOpen("a", "href", m.Target, "target", "_blank")
hb.writeEscaped(m.Target)
hb.writeElementClose("a")
hb.writeElementOpen("br")
// Date
hb.writeEscaped("Created: ")
hb.writeEscaped(unixToLocalDateString(m.Created))
hb.writeElementOpen("br")
hb.writeElementOpen("br")
// Author
if m.Author != "" {
hb.writeEscaped(m.Author)
hb.writeElementOpen("br")
}
// Title
if m.Title != "" {
hb.writeElementOpen("strong")
hb.writeEscaped(m.Title)
hb.writeElementClose("strong")
hb.writeElementOpen("br")
}
// Content
if m.Content != "" {
hb.writeElementOpen("i")
hb.writeEscaped(m.Content)
hb.writeElementClose("i")
hb.writeElementOpen("br")
}
hb.writeElementClose("p")
// Actions
hb.writeElementOpen("form", "method", "post", "class", "actions")
hb.writeElementOpen("input", "type", "hidden", "name", "mentionid", "value", m.ID)
hb.writeElementOpen("input", "type", "hidden", "name", "redir", "value", fmt.Sprintf("%s#mention-%d", wrd.current, m.ID))
if m.Status == webmentionStatusVerified {
// Approve verified mention
hb.writeElementOpen("input", "type", "submit", "formaction", "/webmention/approve", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "approve"))
}
// Delete mention
hb.writeElementOpen("input", "type", "submit", "formaction", "/webmention/delete", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "delete"))
// Reverify mention
hb.writeElementOpen("input", "type", "submit", "formaction", "/webmention/reverify", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "reverify"))
hb.writeElementClose("form")
}
// Pagination
a.renderPagination(hb, rd.Blog, wrd.hasPrev, wrd.hasNext, wrd.prev, wrd.next)
hb.writeElementClose("main")
},
)
}
type editorRenderData struct {
updatePostUrl string
updatePostContent string
}
func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) {
edrd, ok := rd.Data.(*editorRenderData)
if !ok {
return
}
a.renderBase(
hb, rd,
func(hb *htmlBuilder) {
a.renderTitleTag(hb, rd.Blog, a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor"))
// Chroma CSS
hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css"))
},
func(hb *htmlBuilder) {
hb.writeElementOpen("main")
// Title
hb.writeElementOpen("h1")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "editor"))
hb.writeElementClose("h1")
// Create
hb.writeElementOpen("h2")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create"))
hb.writeElementClose("h2")
_ = a.renderMarkdownToWriter(hb, a.editorPostDesc(rd.Blog), false)
hb.writeElementOpen("form", "method", "post", "class", "fw p")
hb.writeElementOpen("input", "type", "hidden", "name", "h", "value", "entry")
hb.writeElementOpen(
"textarea",
"name", "content",
"class", "monospace h400p formcache mdpreview",
"id", "create-input",
"data-preview", "post-preview",
"data-previewws", rd.Blog.getRelativePath("/editor/preview"),
)
hb.writeEscaped(a.editorPostTemplate(rd.BlogString, rd.Blog))
hb.writeElementClose("textarea")
hb.writeElementOpen("div", "id", "post-preview", "class", "hide")
hb.writeElementClose("div")
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "create"))
hb.writeElementClose("form")
// Update
if edrd.updatePostUrl != "" {
hb.writeElementOpen("h2", "id", "#update")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"))
hb.writeElementClose("h2")
hb.writeElementOpen("form", "method", "post", "class", "fw p", "action", "#update")
hb.writeElementOpen("input", "type", "hidden", "name", "editoraction", "value", "updatepost")
hb.writeElementOpen("input", "type", "hidden", "name", "url", "value", edrd.updatePostUrl)
hb.writeElementOpen(
"textarea",
"name", "content",
"class", "monospace h400p mdpreview",
"data-preview", "update-preview",
"data-previewws", rd.Blog.getRelativePath("/editor/preview"),
)
hb.writeEscaped(edrd.updatePostContent)
hb.writeElementClose("textarea")
hb.writeElementOpen("div", "id", "update-preview", "class", "hide")
hb.writeElementClose("div")
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(rd.Blog.Lang, "update"))
hb.writeElementClose("form")
}
// Posts
hb.writeElementOpen("h2")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts"))
hb.writeElementClose("h2")
// Drafts
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/drafts"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "drafts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Private
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/private"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "privateposts"))
hb.writeElementClose("a")