Priorities for posts (pinned posts), comments on blogroll, stats and map & some refactorings and fixes

This commit is contained in:
Jan-Lukas Else 2021-07-12 16:19:28 +02:00
parent fabd30cc20
commit 4205fb173f
26 changed files with 241 additions and 126 deletions

View File

@ -16,6 +16,8 @@ import (
"go.goblog.app/app/pkgs/contenttype"
)
const defaultBlogrollPath = "/blogroll"
func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
t := servertiming.FromContext(r.Context()).NewMetric("bg").Start()
@ -32,13 +34,15 @@ func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
setInternalCacheExpirationHeader(w, r, int(a.cfg.Cache.Expiration))
}
c := a.cfg.Blogs[blog].Blogroll
can := a.getRelativePath(blog, defaultIfEmpty(c.Path, defaultBlogrollPath))
a.render(w, r, templateBlogroll, &renderData{
BlogString: blog,
Canonical: a.getFullAddress(can),
Data: map[string]interface{}{
"Title": c.Title,
"Description": c.Description,
"Outlines": outlines,
"Download": c.Path + ".opml",
"Download": can + ".opml",
},
})
}

View File

@ -9,6 +9,8 @@ import (
servertiming "github.com/mitchellh/go-server-timing"
)
const defaultBlogStatsPath = "/statistics"
func (a *goBlog) initBlogStats() {
f := func(p *post) {
a.db.resetBlogStats(p.Blog)
@ -21,7 +23,7 @@ func (a *goBlog) initBlogStats() {
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
bc := a.cfg.Blogs[blog]
canonical := bc.getRelativePath(bc.BlogStats.Path)
canonical := bc.getRelativePath(defaultIfEmpty(bc.BlogStats.Path, defaultBlogStatsPath))
a.render(w, r, templateBlogStats, &renderData{
BlogString: blog,
Canonical: a.getFullAddress(canonical),

View File

@ -52,50 +52,50 @@ type configCache struct {
}
type configBlog struct {
Path string `mapstructure:"path"`
Lang string `mapstructure:"lang"`
Title string `mapstructure:"title"`
Description string `mapstructure:"description"`
Pagination int `mapstructure:"pagination"`
DefaultSection string `mapstructure:"defaultsection"`
Sections map[string]*section `mapstructure:"sections"`
Taxonomies []*taxonomy `mapstructure:"taxonomies"`
Menus map[string]*menu `mapstructure:"menus"`
Photos *photos `mapstructure:"photos"`
Search *search `mapstructure:"search"`
BlogStats *blogStats `mapstructure:"blogStats"`
Blogroll *configBlogroll `mapstructure:"blogroll"`
CustomPages []*customPage `mapstructure:"custompages"`
Telegram *configTelegram `mapstructure:"telegram"`
PostAsHome bool `mapstructure:"postAsHome"`
RandomPost *randomPost `mapstructure:"randomPost"`
Comments *comments `mapstructure:"comments"`
Map *configMap `mapstructure:"map"`
Path string `mapstructure:"path"`
Lang string `mapstructure:"lang"`
Title string `mapstructure:"title"`
Description string `mapstructure:"description"`
Pagination int `mapstructure:"pagination"`
DefaultSection string `mapstructure:"defaultsection"`
Sections map[string]*configSection `mapstructure:"sections"`
Taxonomies []*configTaxonomy `mapstructure:"taxonomies"`
Menus map[string]*configMenu `mapstructure:"menus"`
Photos *configPhotos `mapstructure:"photos"`
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"`
Comments *configComments `mapstructure:"comments"`
Map *configGeoMap `mapstructure:"map"`
}
type section struct {
type configSection struct {
Name string `mapstructure:"name"`
Title string `mapstructure:"title"`
Description string `mapstructure:"description"`
PathTemplate string `mapstructure:"pathtemplate"`
}
type taxonomy struct {
type configTaxonomy struct {
Name string `mapstructure:"name"`
Title string `mapstructure:"title"`
Description string `mapstructure:"description"`
}
type menu struct {
Items []*menuItem `mapstructure:"items"`
type configMenu struct {
Items []*configMenuItem `mapstructure:"items"`
}
type menuItem struct {
type configMenuItem struct {
Title string `mapstructure:"title"`
Link string `mapstructure:"link"`
}
type photos struct {
type configPhotos struct {
Enabled bool `mapstructure:"enabled"`
Parameter string `mapstructure:"parameter"`
Path string `mapstructure:"path"`
@ -103,7 +103,7 @@ type photos struct {
Description string `mapstructure:"description"`
}
type search struct {
type configSearch struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`
Title string `mapstructure:"title"`
@ -111,7 +111,7 @@ type search struct {
Placeholder string `mapstructure:"placeholder"`
}
type blogStats struct {
type configBlogStats struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`
Title string `mapstructure:"title"`
@ -129,7 +129,7 @@ type configBlogroll struct {
Description string `mapstructure:"description"`
}
type customPage struct {
type configCustomPage struct {
Path string `mapstructure:"path"`
Template string `mapstructure:"template"`
Cache bool `mapstructure:"cache"`
@ -137,33 +137,33 @@ type customPage struct {
Data *interface{} `mapstructure:"data"`
}
type randomPost struct {
type configRandomPost struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`
}
type comments struct {
type configComments struct {
Enabled bool `mapstructure:"enabled"`
}
type configMap struct {
type configGeoMap struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`
}
type configUser struct {
Nick string `mapstructure:"nick"`
Name string `mapstructure:"name"`
Password string `mapstructure:"password"`
TOTP string `mapstructure:"totp"`
AppPasswords []*appPassword `mapstructure:"appPasswords"`
Picture string `mapstructure:"picture"`
Email string `mapstructure:"email"`
Link string `mapstructure:"link"`
Identities []string `mapstructure:"identities"`
Nick string `mapstructure:"nick"`
Name string `mapstructure:"name"`
Password string `mapstructure:"password"`
TOTP string `mapstructure:"totp"`
AppPasswords []*configAppPassword `mapstructure:"appPasswords"`
Picture string `mapstructure:"picture"`
Email string `mapstructure:"email"`
Link string `mapstructure:"link"`
Identities []string `mapstructure:"identities"`
}
type appPassword struct {
type configAppPassword struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}
@ -315,7 +315,7 @@ func (a *goBlog) initConfig() error {
if wm := a.cfg.Webmention; wm != nil && wm.DisableReceiving {
// Disable comments for all blogs
for _, b := range a.cfg.Blogs {
b.Comments = &comments{Enabled: false}
b.Comments = &configComments{Enabled: false}
}
}
// Check config for each blog

View File

@ -5,7 +5,7 @@ import "net/http"
const customPageContextKey = "custompage"
func (a *goBlog) serveCustomPage(w http.ResponseWriter, r *http.Request) {
page := r.Context().Value(customPageContextKey).(*customPage)
page := r.Context().Value(customPageContextKey).(*configCustomPage)
if a.cfg.Cache != nil && a.cfg.Cache.Enable && page.Cache {
if page.CacheExpiration != 0 {
setInternalCacheExpirationHeader(w, r, page.CacheExpiration)

View File

@ -5,8 +5,11 @@ import (
"net/http"
)
const defaultGeoMapPath = "/map"
func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
bc := a.cfg.Blogs[blog]
allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{
blog: blog,
@ -57,6 +60,7 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
a.render(w, r, templateGeoMap, &renderData{
BlogString: blog,
Canonical: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(bc.Map.Path, defaultGeoMapPath))),
Data: map[string]interface{}{
"locations": string(jb),
},

5
go.mod
View File

@ -7,8 +7,7 @@ require (
git.jlel.se/jlelse/go-shutdowner v0.0.0-20210707065515-773db8099c30
git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4
git.jlel.se/jlelse/template-strings v0.0.0-20210617205924-cfa3bd35ae40
github.com/PuerkitoBio/goquery v1.7.0
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/PuerkitoBio/goquery v1.7.1
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/boombuler/barcode v1.0.1 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
@ -54,7 +53,7 @@ require (
github.com/yuin/goldmark v1.4.0
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect

9
go.sum
View File

@ -49,10 +49,9 @@ git.jlel.se/jlelse/template-strings v0.0.0-20210617205924-cfa3bd35ae40/go.mod h1
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/PuerkitoBio/goquery v1.7.0 h1:O5SP3b9JWqMSVMG69zMfj577zwkSNpxrFf7ybS74eiw=
github.com/PuerkitoBio/goquery v1.7.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4=
github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -415,8 +414,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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=

44
http.go
View File

@ -254,18 +254,21 @@ func (a *goBlog) buildStaticHandlersRouters() error {
}
}
if blogConfig.Photos != nil && blogConfig.Photos.Enabled {
if pc := blogConfig.Photos; pc != nil && pc.Enabled {
a.photosMiddlewares[blog] = middleware.WithValue(indexConfigKey, &indexConfig{
path: blogConfig.getRelativePath(blogConfig.Photos.Path),
parameter: blogConfig.Photos.Parameter,
title: blogConfig.Photos.Title,
description: blogConfig.Photos.Description,
path: blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath)),
parameter: pc.Parameter,
title: pc.Title,
description: pc.Description,
summaryTemplate: templatePhotosSummary,
})
}
if blogConfig.Search != nil && blogConfig.Search.Enabled {
a.searchMiddlewares[blog] = middleware.WithValue(pathContextKey, blogConfig.getRelativePath(blogConfig.Search.Path))
if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
a.searchMiddlewares[blog] = middleware.WithValue(
pathContextKey,
blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)),
)
}
for _, cp := range blogConfig.CustomPages {
@ -437,11 +440,11 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
}
// Photos
if blogConfig.Photos != nil && blogConfig.Photos.Enabled {
if pc := blogConfig.Photos; pc != nil && pc.Enabled {
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm, a.photosMiddlewares[blog])
photoPath := blogConfig.getRelativePath(blogConfig.Photos.Path)
photoPath := blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath))
r.Get(photoPath, a.serveIndex)
r.Get(photoPath+feedPath, a.serveIndex)
r.Get(photoPath+paginationPath, a.serveIndex)
@ -449,14 +452,13 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
}
// Search
if blogConfig.Search != nil && blogConfig.Search.Enabled {
searchPath := blogConfig.getRelativePath(blogConfig.Search.Path)
r.With(sbm, a.searchMiddlewares[blog]).Mount(searchPath, a.searchRouter)
if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
r.With(sbm, a.searchMiddlewares[blog]).Mount(blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)), a.searchRouter)
}
// Stats
if blogConfig.BlogStats != nil && blogConfig.BlogStats.Enabled {
statsPath := blogConfig.getRelativePath(blogConfig.BlogStats.Path)
if bsc := blogConfig.BlogStats; bsc != nil && bsc.Enabled {
statsPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
@ -513,11 +515,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Random post
if rp := blogConfig.RandomPost; rp != nil && rp.Enabled {
randomPath := rp.Path
if randomPath == "" {
randomPath = "/random"
}
r.With(a.privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(randomPath), a.redirectToRandomPost)
r.With(a.privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
}
// Editor
@ -530,7 +528,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Blogroll
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
brPath := blogConfig.getRelativePath(brConfig.Path)
brPath := blogConfig.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath))
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
@ -541,11 +539,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Geo map
if mc := blogConfig.Map; mc != nil && mc.Enabled {
mapPath := mc.Path
if mc.Path == "" {
mapPath = "/map"
}
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(mapPath), a.serveGeoMap)
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath)), a.serveGeoMap)
}
}

View File

@ -3,10 +3,8 @@ package main
import (
"bytes"
"html/template"
"strings"
marktag "git.jlel.se/jlelse/goldmark-mark"
"github.com/PuerkitoBio/goquery"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
"github.com/yuin/goldmark/ast"
@ -73,11 +71,7 @@ func (a *goBlog) renderText(s string) string {
if err != nil {
return ""
}
d, err := goquery.NewDocumentFromReader(bytes.NewReader(h))
if err != nil {
return ""
}
return strings.TrimSpace(d.Text())
return htmlText(h)
}
// Extensions etc...

View File

@ -360,6 +360,10 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
p.Status = postStatus(status[0])
delete(p.Parameters, "status")
}
if priority := p.Parameters["priority"]; len(priority) == 1 {
p.Priority = cast.ToInt(priority[0])
delete(p.Parameters, "priority")
}
if p.Path == "" && p.Section == "" {
// Has no path or section -> default section
p.Section = a.cfg.Blogs[p.Blog].DefaultSection

View File

@ -11,7 +11,7 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
b := a.cfg.Blogs[blog]
title := b.Title
sURL := a.getFullAddress(b.getRelativePath(b.Search.Path))
sURL := a.getFullAddress(b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath)))
xml := fmt.Sprintf("<?xml version=\"1.0\"?><OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:moz=\"http://www.mozilla.org/2006/browser/search/\">"+
"<ShortName>%s</ShortName><Description>%s</Description>"+
"<Url type=\"text/html\" method=\"post\" template=\"%s\"><Param name=\"q\" value=\"{searchTerms}\" /></Url>"+
@ -24,7 +24,7 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
func openSearchUrl(b *configBlog) string {
if b.Search != nil && b.Search.Enabled {
return b.getRelativePath(b.Search.Path + "/opensearch.xml")
return b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath) + "/opensearch.xml")
}
return ""
}

View File

@ -19,16 +19,17 @@ import (
var errPostNotFound = errors.New("post not found")
type post struct {
Path string `json:"path"`
Content string `json:"content"`
Published string `json:"published"`
Updated string `json:"updated"`
Parameters map[string][]string `json:"parameters"`
Blog string `json:"blog"`
Section string `json:"section"`
Status postStatus `json:"status"`
Path string
Content string
Published string
Updated string
Parameters map[string][]string
Blog string
Section string
Status postStatus
Priority int
// Not persisted
Slug string `json:"slug"`
Slug string
rendered template.HTML
absoluteRendered template.HTML
}
@ -167,8 +168,8 @@ func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
type indexConfig struct {
blog string
path string
section *section
tax *taxonomy
section *configSection
tax *configTaxonomy
taxValue string
parameter string
year, month, day int
@ -177,6 +178,8 @@ type indexConfig struct {
summaryTemplate string
}
const defaultPhotosPath = "/photos"
const indexConfigKey contextKey = "indexConfig"
func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
@ -187,7 +190,8 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
}
search := chi.URLParam(r, "search")
if search != "" {
search = searchDecode(search)
// Decode and sanitize search
search = htmlText([]byte(bluemonday.StrictPolicy().Sanitize(searchDecode(search))))
}
pageNoString := chi.URLParam(r, "page")
pageNo, _ := strconv.Atoi(pageNoString)
@ -210,6 +214,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
publishedMonth: ic.month,
publishedDay: ic.day,
status: statusPublished,
priorityOrder: true,
}, db: a.db}, a.cfg.Blogs[blog].Pagination)
p.SetPage(pageNo)
var posts []*post
@ -231,8 +236,6 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
} else if search != "" {
title = fmt.Sprintf("%s: %s", a.cfg.Blogs[blog].Search.Title, search)
}
// Clean title
title = bluemonday.StrictPolicy().Sanitize(title)
// Check if feed
if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed {
a.generateFeed(blog, ft, w, r, posts, title, description)

View File

@ -162,8 +162,8 @@ func (db *database) savePost(p *post, o *postCreationOptions) error {
sqlArgs = append(sqlArgs, o.oldPath, o.oldPath)
}
// Insert new post
sqlBuilder.WriteString("insert into posts (path, content, published, updated, blog, section, status) values (?, ?, ?, ?, ?, ?, ?);")
sqlArgs = append(sqlArgs, p.Path, p.Content, p.Published, p.Updated, p.Blog, p.Section, p.Status)
sqlBuilder.WriteString("insert into posts (path, content, published, updated, blog, section, status, priority) values (?, ?, ?, ?, ?, ?, ?, ?);")
sqlArgs = append(sqlArgs, p.Path, p.Content, p.Published, p.Updated, p.Blog, p.Section, p.Status, p.Priority)
// Insert post parameters
for param, value := range p.Parameters {
for _, value := range value {
@ -220,12 +220,13 @@ type postsRequestConfig struct {
offset int
sections []string
status postStatus
taxonomy *taxonomy
taxonomy *configTaxonomy
taxonomyValue string
parameter string
parameterValue string
publishedYear, publishedMonth, publishedDay int
randomOrder bool
priorityOrder bool
withoutParameters bool
withOnlyParameters []string
}
@ -294,6 +295,8 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
sorting := " order by published desc"
if c.randomOrder {
sorting = " order by random()"
} else if c.priorityOrder {
sorting = " order by priority desc, published desc"
}
table += sorting
if c.limit != 0 || c.offset != 0 {
@ -339,15 +342,16 @@ func (d *database) getPostParameters(path string, parameters ...string) (params
func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err error) {
// Query posts
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status")
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, priority")
rows, err := d.query(query, queryParams...)
if err != nil {
return nil, err
}
// Prepare row scanning
var path, content, published, updated, blog, section, status string
var priority int
for rows.Next() {
if err = rows.Scan(&path, &content, &published, &updated, &blog, &section, &status); err != nil {
if err = rows.Scan(&path, &content, &published, &updated, &blog, &section, &status, &priority); err != nil {
return nil, err
}
// Create new post, fill and add to list
@ -359,6 +363,7 @@ func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err erro
Blog: blog,
Section: section,
Status: postStatus(status),
Priority: priority,
}
if !config.withoutParameters {
if p.Parameters, err = d.getPostParameters(path, config.withOnlyParameters...); err != nil {

View File

@ -20,8 +20,9 @@ func Test_postsDb(t *testing.T) {
},
Blogs: map[string]*configBlog{
"en": {
Sections: map[string]*section{
"test": {},
Sections: map[string]*configSection{
"test": {},
"micro": {},
},
},
},
@ -241,6 +242,57 @@ func Test_ftsWithoutTitle(t *testing.T) {
assert.Len(t, ps, 1)
}
func Test_postsPriority(t *testing.T) {
// Added because there was a bug where there were no search results without title
app := &goBlog{
cfg: &config{
Db: &configDb{
File: filepath.Join(t.TempDir(), "test.db"),
},
},
}
app.initDatabase(false)
err := app.db.savePost(&post{
Path: "/test/abc",
Content: "ABC",
Published: toLocalSafe(time.Now().String()),
Blog: "en",
Section: "test",
Status: statusPublished,
}, &postCreationOptions{new: true})
require.NoError(t, err)
err = app.db.savePost(&post{
Path: "/test/def",
Content: "DEF",
Published: toLocalSafe(time.Now().String()),
Blog: "en",
Section: "test",
Status: statusPublished,
Priority: 1,
}, &postCreationOptions{new: true})
require.NoError(t, err)
ps, err := app.db.getPosts(&postsRequestConfig{
priorityOrder: true,
})
require.NoError(t, err)
if assert.Len(t, ps, 2) {
post1 := ps[0]
assert.Equal(t, "/test/def", post1.Path)
assert.Equal(t, 1, post1.Priority)
post2 := ps[1]
assert.Equal(t, "/test/abc", post2.Path)
assert.Equal(t, 0, post2.Priority)
}
}
func Test_usesOfMediaFile(t *testing.T) {
app := &goBlog{
cfg: &config{

View File

@ -120,14 +120,22 @@ func (p *post) isPublishedSectionPost() bool {
}
func (a *goBlog) postToMfItem(p *post) *microformatItem {
params := p.Parameters
params["path"] = []string{p.Path}
params["section"] = []string{p.Section}
params["blog"] = []string{p.Blog}
params["published"] = []string{p.Published}
params["updated"] = []string{p.Updated}
params["status"] = []string{string(p.Status)}
pb, _ := yaml.Marshal(p.Parameters)
params := map[string]interface{}{}
for k, v := range p.Parameters {
if l := len(v); l == 1 {
params[k] = v[0]
} else if l > 1 {
params[k] = v
}
}
params["path"] = p.Path
params["section"] = p.Section
params["blog"] = p.Blog
params["published"] = p.Published
params["updated"] = p.Updated
params["status"] = string(p.Status)
params["priority"] = p.Priority
pb, _ := yaml.Marshal(params)
content := fmt.Sprintf("---\n%s---\n%s", string(pb), p.Content)
return &microformatItem{
Type: []string{"h-entry"},

View File

@ -7,8 +7,11 @@ import (
"net/url"
"path"
"strings"
"github.com/microcosm-cc/bluemonday"
)
const defaultSearchPath = "/search"
const searchPlaceholder = "{search}"
func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
@ -20,6 +23,9 @@ func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
return
}
if q := r.Form.Get("q"); q != "" {
// Clean query
q = htmlText([]byte(bluemonday.StrictPolicy().Sanitize(q)))
// Redirect to results
http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
return
}

View File

@ -101,27 +101,33 @@ func (a *goBlog) serveSitemap(w http.ResponseWriter, r *http.Request) {
}
}
// Photos
if bc.Photos != nil && bc.Photos.Enabled {
if pc := bc.Photos; pc != nil && pc.Enabled {
sm.Add(&sitemap.URL{
Loc: a.getFullAddress(bc.getRelativePath(bc.Photos.Path)),
Loc: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath))),
})
}
// Search
if bc.Search != nil && bc.Search.Enabled {
if bsc := bc.Search; bsc != nil && bsc.Enabled {
sm.Add(&sitemap.URL{
Loc: a.getFullAddress(bc.getRelativePath(bc.Search.Path)),
Loc: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath))),
})
}
// Stats
if bc.BlogStats != nil && bc.BlogStats.Enabled {
if bsc := bc.BlogStats; bsc != nil && bsc.Enabled {
sm.Add(&sitemap.URL{
Loc: a.getFullAddress(bc.getRelativePath(bc.BlogStats.Path)),
Loc: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))),
})
}
// Blogroll
if bc.Blogroll != nil && bc.Blogroll.Enabled {
if brc := bc.Blogroll; brc != nil && brc.Enabled {
sm.Add(&sitemap.URL{
Loc: a.getFullAddress(bc.getRelativePath(bc.Blogroll.Path)),
Loc: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(brc.Path, defaultBlogrollPath))),
})
}
// Geo map
if mc := bc.Map; mc != nil && mc.Enabled {
sm.Add(&sitemap.URL{
Loc: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath))),
})
}
// Custom pages

View File

@ -6,7 +6,7 @@ const taxonomyContextKey = "taxonomy"
func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
tax := r.Context().Value(taxonomyContextKey).(*taxonomy)
tax := r.Context().Value(taxonomyContextKey).(*configTaxonomy)
allValues, err := a.db.allTaxonomyValues(blog, tax.Name)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)

View File

@ -21,6 +21,9 @@
</ul>
{{ end }}
</main>
{{ if .CommentsEnabled }}
{{ include "interactions" . }}
{{ end }}
{{ end }}
{{ define "blogroll" }}

View File

@ -9,6 +9,9 @@
<p id="loading" data-table="{{.Data.TableUrl}}">{{ string .Blog.Lang "loading" }}</p>
<script defer src="{{ asset "js/blogstats.js" }}"></script>
</main>
{{ if .CommentsEnabled }}
{{ include "interactions" . }}
{{ end }}
{{ end }}
{{ define "blogstats" }}

View File

@ -24,6 +24,9 @@
<script defer src="{{ asset "js/geomap.js" }}"></script>
{{ end }}
</main>
{{ if .CommentsEnabled }}
{{ include "interactions" . }}
{{ end }}
{{ end }}
{{ define "geomap" }}

View File

@ -1,6 +1,13 @@
{{ define "photosummary" }}
<article class="h-entry border-bottom">
{{ with p .Data "title" }}<h2>{{ . }}</h2>{{ end }}
{{ if gt .Data.Priority 0 }}<p>📌 {{ string .Blog.Lang "pinned" }}</p>{{ end }}
{{ if p .Data "title" }}
<h2 class="p-name">
<a class="u-url" href="{{ .Data.Path }}">
{{ p .Data "title" }}
</a>
</h2>
{{ end }}
{{ include "summarymeta" . }}
{{ range $i, $photo := ( ps .Data .Blog.Photos.Parameter ) }}
{{ md ( printf "![](%s)" $photo ) }}

View File

@ -26,6 +26,7 @@ nofiles: "Keine Dateien"
nolocations: "Keine Posts mit Standorten"
noposts: "Hier sind keine Posts."
oldcontent: "⚠️ Dieser Eintrag ist bereits über ein Jahr alt. Er ist möglicherweise nicht mehr aktuell. Meinungen können sich geändert haben."
pinned: "Angepinnt"
posts: "Posts"
prev: "Zurück"
publishedon: "Veröffentlicht am"

View File

@ -38,6 +38,7 @@ noposts: "There are no posts here."
notifications: "Notifications"
oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."
password: "Password"
pinned: "Pinned"
posts: "Posts"
prev: "Previous"
publishedon: "Published on"

View File

@ -1,5 +1,6 @@
{{ define "summary" }}
<article class="h-entry border-bottom">
{{ if gt .Data.Priority 0 }}<p>📌 {{ string .Blog.Lang "pinned" }}</p>{{ end }}
{{ if p .Data "title" }}
<h2 class="p-name">
<a class="u-url" href="{{ .Data.Path }}">

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"html/template"
@ -211,3 +212,18 @@ func getSHA256(file io.ReadSeeker) (filename string, err error) {
func mBytesString(size int64) string {
return fmt.Sprintf("%.2f MB", datasize.ByteSize(size).MBytes())
}
func htmlText(b []byte) string {
d, err := goquery.NewDocumentFromReader(bytes.NewReader(b))
if err != nil {
return ""
}
return strings.TrimSpace(d.Text())
}
func defaultIfEmpty(s, d string) string {
if s != "" {
return s
}
return d
}