Taxonomies!

This commit is contained in:
Jan-Lukas Else 2020-08-31 21:12:43 +02:00
parent c98f3eaca2
commit 484da515aa
9 changed files with 145 additions and 44 deletions

View File

@ -40,6 +40,8 @@ type configBlog struct {
Pagination int `mapstructure:"pagination"`
// Sections
Sections []string `mapstructure:"sections"`
// Taxonomies
Taxonomies []string `mapstructure:"taxonomies"`
}
type configUser struct {
@ -71,6 +73,7 @@ func initConfig() error {
viper.SetDefault("blog.title", "My blog")
viper.SetDefault("blog.pagination", 10)
viper.SetDefault("blog.sections", []string{"posts"})
viper.SetDefault("blog.taxonomies", []string{"tags"})
viper.SetDefault("user.nick", "admin")
viper.SetDefault("user.name", "Admin")
viper.SetDefault("user.password", "secret")

View File

@ -38,6 +38,13 @@ func migrateDb() error {
return err
},
},
&migrator.Migration{
Name: "00005",
Func: func(tx *sql.Tx) error {
_, err := tx.Exec("create table pp_tmp(id integer primary key autoincrement, path text not null, parameter text not null, value text); insert into pp_tmp(path, parameter, value) select path, parameter, value from post_parameters; drop table post_parameters; alter table pp_tmp rename to post_parameters;")
return err
},
},
),
)
if err != nil {

View File

@ -15,6 +15,8 @@ blog:
title: My blog
sections:
- posts
taxonomies:
- tags
user:
nick: admin
name: Admin

8
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/lib/pq v1.8.0 // indirect
github.com/lopezator/migrator v0.3.0
github.com/magiconair/properties v1.8.2 // indirect
github.com/mattn/go-sqlite3 v1.14.1
github.com/mattn/go-sqlite3 v1.14.2
github.com/miekg/dns v1.1.31 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
@ -27,16 +27,16 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1 // indirect
github.com/tdewolff/minify/v2 v2.8.0
github.com/tdewolff/minify/v2 v2.9.1
github.com/vcraescu/go-paginator v0.0.0-20200304054438-86d84f27c0b3
github.com/yuin/goldmark v1.2.1
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 // indirect
golang.org/x/sys v0.0.0-20200828194041-157a740278f4 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.60.1 // indirect
gopkg.in/ini.v1 v1.60.2 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
)

20
go.sum
View File

@ -277,8 +277,8 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.1 h1:AHx9Ra40wIzl+GelgX2X6AWxmT5tfxhI1PL0523HcSw=
github.com/mattn/go-sqlite3 v1.14.1/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.2 h1:A2EQLwjYf/hfYaM20FVjs1UewCTTFR7RmjEHkLjldIA=
github.com/mattn/go-sqlite3 v1.14.2/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@ -405,10 +405,10 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tdewolff/minify/v2 v2.8.0 h1:t3tOPWkTpKhsgxm3IM9Sy8hE2eIt30Oaa+2havJGGIE=
github.com/tdewolff/minify/v2 v2.8.0/go.mod h1:6zN8VLhMfFxNrwHROcboYNo2+huPNu4SV8DPh3PUQ8E=
github.com/tdewolff/parse/v2 v2.4.4 h1:uMdbQRtYbKR/msP9CbI7li9wK6pionYiH6s7ipltyGY=
github.com/tdewolff/parse/v2 v2.4.4/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/minify/v2 v2.9.1 h1:k6QEyGlg/Oh+6dZASJDM8dzSbKoCS5S4lp3tHk2AnAM=
github.com/tdewolff/minify/v2 v2.9.1/go.mod h1:njYNbXhVTAhI1hARVHCbHAgRd44j+AEt0LdW+menKsY=
github.com/tdewolff/parse/v2 v2.5.1 h1:1PxbcgMxb8RWH1nmeQjBC+lZIOTUEjiYQ3u8RpzndN0=
github.com/tdewolff/parse/v2 v2.5.1/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
@ -565,8 +565,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 h1:AvbQYmiaaaza3cW3QXRyPo5kYgpFIzOAfeAAN7m3qQ4=
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -676,8 +676,8 @@ gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.60.1 h1:P5y5shSkb0CFe44qEeMBgn8JLow09MP17jlJHanke5g=
gopkg.in/ini.v1 v1.60.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.60.2 h1:7i8mqModL63zqi8nQn8Q3+0zvSCZy1AxhBgthKfi4WU=
gopkg.in/ini.v1 v1.60.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

33
http.go
View File

@ -85,20 +85,39 @@ func buildHandler() (http.Handler, error) {
}
}
paginationPath := "/page/{page}"
for _, section := range appConfig.Blog.Sections {
if section != "" {
r.With(cacheMiddleware, minifier.Middleware).Get("/"+section, serveSection("/"+section, section))
r.With(cacheMiddleware, minifier.Middleware).Get("/"+section+"/page/{page}", serveSection("/"+section, section))
r.With(cacheMiddleware, minifier.Middleware).Get("/"+section+paginationPath, serveSection("/"+section, section))
}
}
for _, taxonomy := range appConfig.Blog.Taxonomies {
if taxonomy != "" {
r.With(cacheMiddleware, minifier.Middleware).Get("/"+taxonomy, serveTaxonomy(taxonomy))
values, err := allTaxonomyValues(taxonomy)
if err != nil {
return nil, err
}
for _, tv := range values {
path := "/" + taxonomy + "/" + tv
r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomyValue(path, taxonomy, tv))
r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveTaxonomyValue(path, taxonomy, tv))
}
}
}
routePatterns := routesToStringSlice(r.Routes())
if !routePatterns.has("/") {
r.With(cacheMiddleware, minifier.Middleware).Get("/", serveHome("/"))
r.With(cacheMiddleware, minifier.Middleware).Get("/page/{page}", serveHome("/"))
} else if !routePatterns.has("/blog") {
r.With(cacheMiddleware, minifier.Middleware).Get("/blog", serveHome("/blog"))
r.With(cacheMiddleware, minifier.Middleware).Get("/blog/page/{page}", serveHome("/blog"))
rootPath := "/"
blogPath := "/blog"
if !routePatterns.has(rootPath) {
r.With(cacheMiddleware, minifier.Middleware).Get(rootPath, serveHome(rootPath))
r.With(cacheMiddleware, minifier.Middleware).Get(paginationPath, serveHome(rootPath))
} else if !routePatterns.has(blogPath) {
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath, serveHome(blogPath))
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+paginationPath, serveHome(blogPath))
}
r.With(minifier.Middleware).NotFound(serve404)

View File

@ -16,11 +16,11 @@ 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"`
Path string `json:"path"`
Content string `json:"content"`
Published string `json:"published"`
Updated string `json:"updated"`
Parameters map[string][]string `json:"parameters"`
}
func servePost(w http.ResponseWriter, r *http.Request) {
@ -62,24 +62,45 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro
panic("data has to be a pointer")
}
posts, err := getPosts(p.context, &postsRequestConfig{
sections: p.config.sections,
offset: offset,
limit: length,
})
modifiedConfig := *p.config
modifiedConfig.offset = offset
modifiedConfig.limit = length
posts, err := getPosts(p.context, &modifiedConfig)
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(&posts).Elem())
return err
}
func serveHome(path string) func(w http.ResponseWriter, r *http.Request) {
return serveIndex(path, "")
return serveIndex(path, "", "", "")
}
func serveSection(path, section string) func(w http.ResponseWriter, r *http.Request) {
return serveIndex(path, section)
return serveIndex(path, section, "", "")
}
func serveIndex(path string, section string) func(w http.ResponseWriter, r *http.Request) {
func serveTaxonomy(taxonomy string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
allValues, err := allTaxonomyValues(taxonomy)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
render(w, templateTaxonomy, struct {
Taxonomy string
TaxonomyValues []string
}{
Taxonomy: taxonomy,
TaxonomyValues: allValues,
})
}
}
func serveTaxonomyValue(path, taxonomy, value string) func(w http.ResponseWriter, r *http.Request) {
return serveIndex(path, "", taxonomy, value)
}
func serveIndex(path, section, taxonomy, taxonomyValue string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageNoString := chi.URLParam(r, "page")
pageNo, _ := strconv.Atoi(pageNoString)
@ -87,7 +108,11 @@ func serveIndex(path string, section string) func(w http.ResponseWriter, r *http
if len(section) > 0 {
sections = []string{section}
}
p := paginator.New(&postPaginationAdapter{context: r.Context(), config: &postsRequestConfig{sections: sections}}, appConfig.Blog.Pagination)
p := paginator.New(&postPaginationAdapter{context: r.Context(), config: &postsRequestConfig{
sections: sections,
taxonomy: taxonomy,
taxonomyValue: taxonomyValue,
}}, appConfig.Blog.Pagination)
p.SetPage(pageNo)
var posts []*Post
err := p.Results(&posts)
@ -124,10 +149,12 @@ func getPost(context context.Context, path string) (*Post, error) {
}
type postsRequestConfig struct {
path string
limit int
offset int
sections []string
path string
limit int
offset int
sections []string
taxonomy string
taxonomyValue string
}
func getPosts(context context.Context, config *postsRequestConfig) (posts []*Post, err error) {
@ -135,8 +162,11 @@ func getPosts(context context.Context, config *postsRequestConfig) (posts []*Pos
var rows *sql.Rows
defaultSelection := "select p.path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(parameter, ''), coalesce(value, '') "
postsTable := "posts"
if len(config.sections) != 0 {
postsTable = "(select * from posts where"
if len(config.taxonomy) > 0 && len(config.taxonomyValue) > 0 {
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = '" + config.taxonomy + "' and pp.value = '" + config.taxonomyValue + "')"
}
if len(config.sections) > 0 {
postsTable = "(select * from " + postsTable + " where"
for i, section := range config.sections {
if i > 0 {
postsTable += " or"
@ -173,11 +203,11 @@ func getPosts(context context.Context, config *postsRequestConfig) (posts []*Pos
if paths[post.Path] == 0 {
index := len(posts)
paths[post.Path] = index + 1
post.Parameters = make(map[string]string)
post.Parameters = make(map[string][]string)
posts = append(posts, post)
}
if parameterName != "" && posts != nil {
posts[paths[post.Path]-1].Parameters[parameterName] = parameterValue
posts[paths[post.Path]-1].Parameters[parameterName] = append(posts[paths[post.Path]-1].Parameters[parameterName], parameterValue)
}
}
return posts, nil
@ -202,6 +232,20 @@ func allPostPaths() ([]string, error) {
return postPaths, nil
}
func allTaxonomyValues(taxonomy string) ([]string, error) {
var values []string
rows, err := appDb.Query("select distinct value from post_parameters where parameter = ? and value not null and value != ''", taxonomy)
if err != nil {
return nil, err
}
for rows.Next() {
var value string
_ = rows.Scan(&value)
values = append(values, value)
}
return values, nil
}
func checkPost(post *Post) error {
if post == nil {
return errors.New("no post")

View File

@ -13,6 +13,7 @@ const templateError = "error"
const templateRedirect = "redirect"
const templateIndex = "index"
const templateSummary = "summary"
const templateTaxonomy = "taxonomy"
var templates map[string]*template.Template
var templateFunctions template.FuncMap
@ -30,7 +31,16 @@ func initRendering() {
}
return template.HTML(htmlContent)
},
// First parameter value
"p": func(post Post, parameter string) string {
if len(post.Parameters[parameter]) > 0 {
return post.Parameters[parameter][0]
} else {
return ""
}
},
// All parameter values
"ps": func(post Post, parameter string) []string {
return post.Parameters[parameter]
},
"include": func(templateName string, data interface{}) (template.HTML, error) {
@ -41,7 +51,7 @@ func initRendering() {
}
templates = make(map[string]*template.Template)
for _, name := range []string{templatePost, templateError, templateRedirect, templateIndex, templateSummary} {
for _, name := range []string{templatePost, templateError, templateRedirect, templateIndex, templateSummary, templateTaxonomy} {
templates[name] = loadTemplate(name)
}
}

16
templates/taxonomy.gohtml Normal file
View File

@ -0,0 +1,16 @@
{{ define "title" }}
<title>{{ blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<ul>
{{ $taxonomy := .Taxonomy }}
{{ range $i, $value := .TaxonomyValues }}<li><a href="/{{ $taxonomy }}/{{ . }}">{{ . }}</a></li>{{ end }}
</ul>
</main>
{{ end }}
{{ define "taxonomy" }}
{{ template "base" . }}
{{ end }}