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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import "net/http"
const customPageContextKey = "custompage" const customPageContextKey = "custompage"
func (a *goBlog) serveCustomPage(w http.ResponseWriter, r *http.Request) { 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 a.cfg.Cache != nil && a.cfg.Cache.Enable && page.Cache {
if page.CacheExpiration != 0 { if page.CacheExpiration != 0 {
setInternalCacheExpirationHeader(w, r, page.CacheExpiration) setInternalCacheExpirationHeader(w, r, page.CacheExpiration)

View File

@ -5,8 +5,11 @@ import (
"net/http" "net/http"
) )
const defaultGeoMapPath = "/map"
func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) { func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string) blog := r.Context().Value(blogContextKey).(string)
bc := a.cfg.Blogs[blog]
allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{ allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{
blog: blog, blog: blog,
@ -57,6 +60,7 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
a.render(w, r, templateGeoMap, &renderData{ a.render(w, r, templateGeoMap, &renderData{
BlogString: blog, BlogString: blog,
Canonical: a.getFullAddress(bc.getRelativePath(defaultIfEmpty(bc.Map.Path, defaultGeoMapPath))),
Data: map[string]interface{}{ Data: map[string]interface{}{
"locations": string(jb), "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/go-shutdowner v0.0.0-20210707065515-773db8099c30
git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4 git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4
git.jlel.se/jlelse/template-strings v0.0.0-20210617205924-cfa3bd35ae40 git.jlel.se/jlelse/template-strings v0.0.0-20210617205924-cfa3bd35ae40
github.com/PuerkitoBio/goquery v1.7.0 github.com/PuerkitoBio/goquery v1.7.1
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
@ -54,7 +53,7 @@ require (
github.com/yuin/goldmark v1.4.0 github.com/yuin/goldmark v1.4.0
// master // master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38 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/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 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/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/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.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/PuerkitoBio/goquery v1.7.0 h1:O5SP3b9JWqMSVMG69zMfj577zwkSNpxrFf7ybS74eiw= github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4=
github.com/PuerkitoBio/goquery v1.7.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 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.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 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= 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= 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-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-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-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-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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{ a.photosMiddlewares[blog] = middleware.WithValue(indexConfigKey, &indexConfig{
path: blogConfig.getRelativePath(blogConfig.Photos.Path), path: blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath)),
parameter: blogConfig.Photos.Parameter, parameter: pc.Parameter,
title: blogConfig.Photos.Title, title: pc.Title,
description: blogConfig.Photos.Description, description: pc.Description,
summaryTemplate: templatePhotosSummary, summaryTemplate: templatePhotosSummary,
}) })
} }
if blogConfig.Search != nil && blogConfig.Search.Enabled { if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
a.searchMiddlewares[blog] = middleware.WithValue(pathContextKey, blogConfig.getRelativePath(blogConfig.Search.Path)) a.searchMiddlewares[blog] = middleware.WithValue(
pathContextKey,
blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)),
)
} }
for _, cp := range blogConfig.CustomPages { for _, cp := range blogConfig.CustomPages {
@ -437,11 +440,11 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
} }
// Photos // Photos
if blogConfig.Photos != nil && blogConfig.Photos.Enabled { if pc := blogConfig.Photos; pc != nil && pc.Enabled {
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...) r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm, a.photosMiddlewares[blog]) 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, a.serveIndex)
r.Get(photoPath+feedPath, a.serveIndex) r.Get(photoPath+feedPath, a.serveIndex)
r.Get(photoPath+paginationPath, a.serveIndex) r.Get(photoPath+paginationPath, a.serveIndex)
@ -449,14 +452,13 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
} }
// Search // Search
if blogConfig.Search != nil && blogConfig.Search.Enabled { if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
searchPath := blogConfig.getRelativePath(blogConfig.Search.Path) r.With(sbm, a.searchMiddlewares[blog]).Mount(blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)), a.searchRouter)
r.With(sbm, a.searchMiddlewares[blog]).Mount(searchPath, a.searchRouter)
} }
// Stats // Stats
if blogConfig.BlogStats != nil && blogConfig.BlogStats.Enabled { if bsc := blogConfig.BlogStats; bsc != nil && bsc.Enabled {
statsPath := blogConfig.getRelativePath(blogConfig.BlogStats.Path) statsPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...) r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm) r.Use(a.cache.cacheMiddleware, sbm)
@ -513,11 +515,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Random post // Random post
if rp := blogConfig.RandomPost; rp != nil && rp.Enabled { if rp := blogConfig.RandomPost; rp != nil && rp.Enabled {
randomPath := rp.Path r.With(a.privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
if randomPath == "" {
randomPath = "/random"
}
r.With(a.privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(randomPath), a.redirectToRandomPost)
} }
// Editor // Editor
@ -530,7 +528,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Blogroll // Blogroll
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled { 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.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...) r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm) r.Use(a.cache.cacheMiddleware, sbm)
@ -541,11 +539,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Geo map // Geo map
if mc := blogConfig.Map; mc != nil && mc.Enabled { if mc := blogConfig.Map; mc != nil && mc.Enabled {
mapPath := mc.Path r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath)), a.serveGeoMap)
if mc.Path == "" {
mapPath = "/map"
}
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(mapPath), a.serveGeoMap)
} }
} }

View File

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

View File

@ -360,6 +360,10 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
p.Status = postStatus(status[0]) p.Status = postStatus(status[0])
delete(p.Parameters, "status") 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 == "" { if p.Path == "" && p.Section == "" {
// Has no path or section -> default section // Has no path or section -> default section
p.Section = a.cfg.Blogs[p.Blog].DefaultSection 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) blog := r.Context().Value(blogContextKey).(string)
b := a.cfg.Blogs[blog] b := a.cfg.Blogs[blog]
title := b.Title 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/\">"+ 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>"+ "<ShortName>%s</ShortName><Description>%s</Description>"+
"<Url type=\"text/html\" method=\"post\" template=\"%s\"><Param name=\"q\" value=\"{searchTerms}\" /></Url>"+ "<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 { func openSearchUrl(b *configBlog) string {
if b.Search != nil && b.Search.Enabled { 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 "" return ""
} }

View File

@ -19,16 +19,17 @@ import (
var errPostNotFound = errors.New("post not found") var errPostNotFound = errors.New("post not found")
type post struct { type post struct {
Path string `json:"path"` Path string
Content string `json:"content"` Content string
Published string `json:"published"` Published string
Updated string `json:"updated"` Updated string
Parameters map[string][]string `json:"parameters"` Parameters map[string][]string
Blog string `json:"blog"` Blog string
Section string `json:"section"` Section string
Status postStatus `json:"status"` Status postStatus
Priority int
// Not persisted // Not persisted
Slug string `json:"slug"` Slug string
rendered template.HTML rendered template.HTML
absoluteRendered template.HTML absoluteRendered template.HTML
} }
@ -167,8 +168,8 @@ func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
type indexConfig struct { type indexConfig struct {
blog string blog string
path string path string
section *section section *configSection
tax *taxonomy tax *configTaxonomy
taxValue string taxValue string
parameter string parameter string
year, month, day int year, month, day int
@ -177,6 +178,8 @@ type indexConfig struct {
summaryTemplate string summaryTemplate string
} }
const defaultPhotosPath = "/photos"
const indexConfigKey contextKey = "indexConfig" const indexConfigKey contextKey = "indexConfig"
func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { 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") search := chi.URLParam(r, "search")
if search != "" { if search != "" {
search = searchDecode(search) // Decode and sanitize search
search = htmlText([]byte(bluemonday.StrictPolicy().Sanitize(searchDecode(search))))
} }
pageNoString := chi.URLParam(r, "page") pageNoString := chi.URLParam(r, "page")
pageNo, _ := strconv.Atoi(pageNoString) pageNo, _ := strconv.Atoi(pageNoString)
@ -210,6 +214,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
publishedMonth: ic.month, publishedMonth: ic.month,
publishedDay: ic.day, publishedDay: ic.day,
status: statusPublished, status: statusPublished,
priorityOrder: true,
}, db: a.db}, a.cfg.Blogs[blog].Pagination) }, db: a.db}, a.cfg.Blogs[blog].Pagination)
p.SetPage(pageNo) p.SetPage(pageNo)
var posts []*post var posts []*post
@ -231,8 +236,6 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
} else if search != "" { } else if search != "" {
title = fmt.Sprintf("%s: %s", a.cfg.Blogs[blog].Search.Title, search) title = fmt.Sprintf("%s: %s", a.cfg.Blogs[blog].Search.Title, search)
} }
// Clean title
title = bluemonday.StrictPolicy().Sanitize(title)
// Check if feed // Check if feed
if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed { if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed {
a.generateFeed(blog, ft, w, r, posts, title, description) 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) sqlArgs = append(sqlArgs, o.oldPath, o.oldPath)
} }
// Insert new post // Insert new post
sqlBuilder.WriteString("insert into posts (path, content, published, updated, blog, section, status) values (?, ?, ?, ?, ?, ?, ?);") 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) sqlArgs = append(sqlArgs, p.Path, p.Content, p.Published, p.Updated, p.Blog, p.Section, p.Status, p.Priority)
// Insert post parameters // Insert post parameters
for param, value := range p.Parameters { for param, value := range p.Parameters {
for _, value := range value { for _, value := range value {
@ -220,12 +220,13 @@ type postsRequestConfig struct {
offset int offset int
sections []string sections []string
status postStatus status postStatus
taxonomy *taxonomy taxonomy *configTaxonomy
taxonomyValue string taxonomyValue string
parameter string parameter string
parameterValue string parameterValue string
publishedYear, publishedMonth, publishedDay int publishedYear, publishedMonth, publishedDay int
randomOrder bool randomOrder bool
priorityOrder bool
withoutParameters bool withoutParameters bool
withOnlyParameters []string withOnlyParameters []string
} }
@ -294,6 +295,8 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
sorting := " order by published desc" sorting := " order by published desc"
if c.randomOrder { if c.randomOrder {
sorting = " order by random()" sorting = " order by random()"
} else if c.priorityOrder {
sorting = " order by priority desc, published desc"
} }
table += sorting table += sorting
if c.limit != 0 || c.offset != 0 { 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) { func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err error) {
// Query posts // 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...) rows, err := d.query(query, queryParams...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Prepare row scanning // Prepare row scanning
var path, content, published, updated, blog, section, status string var path, content, published, updated, blog, section, status string
var priority int
for rows.Next() { 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 return nil, err
} }
// Create new post, fill and add to list // Create new post, fill and add to list
@ -359,6 +363,7 @@ func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err erro
Blog: blog, Blog: blog,
Section: section, Section: section,
Status: postStatus(status), Status: postStatus(status),
Priority: priority,
} }
if !config.withoutParameters { if !config.withoutParameters {
if p.Parameters, err = d.getPostParameters(path, config.withOnlyParameters...); err != nil { 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{ Blogs: map[string]*configBlog{
"en": { "en": {
Sections: map[string]*section{ Sections: map[string]*configSection{
"test": {}, "test": {},
"micro": {},
}, },
}, },
}, },
@ -241,6 +242,57 @@ func Test_ftsWithoutTitle(t *testing.T) {
assert.Len(t, ps, 1) 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) { func Test_usesOfMediaFile(t *testing.T) {
app := &goBlog{ app := &goBlog{
cfg: &config{ cfg: &config{

View File

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

View File

@ -7,8 +7,11 @@ import (
"net/url" "net/url"
"path" "path"
"strings" "strings"
"github.com/microcosm-cc/bluemonday"
) )
const defaultSearchPath = "/search"
const searchPlaceholder = "{search}" const searchPlaceholder = "{search}"
func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) { 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 return
} }
if q := r.Form.Get("q"); q != "" { 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) http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
return return
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,13 @@
{{ define "photosummary" }} {{ define "photosummary" }}
<article class="h-entry border-bottom"> <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" . }} {{ include "summarymeta" . }}
{{ range $i, $photo := ( ps .Data .Blog.Photos.Parameter ) }} {{ range $i, $photo := ( ps .Data .Blog.Photos.Parameter ) }}
{{ md ( printf "![](%s)" $photo ) }} {{ md ( printf "![](%s)" $photo ) }}

View File

@ -26,6 +26,7 @@ nofiles: "Keine Dateien"
nolocations: "Keine Posts mit Standorten" nolocations: "Keine Posts mit Standorten"
noposts: "Hier sind keine Posts." 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." 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" posts: "Posts"
prev: "Zurück" prev: "Zurück"
publishedon: "Veröffentlicht am" publishedon: "Veröffentlicht am"

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"html/template" "html/template"
@ -211,3 +212,18 @@ func getSHA256(file io.ReadSeeker) (filename string, err error) {
func mBytesString(size int64) string { func mBytesString(size int64) string {
return fmt.Sprintf("%.2f MB", datasize.ByteSize(size).MBytes()) 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
}