From 484da515aa182ab720cf4fc1eeed0d89772a98ff Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Mon, 31 Aug 2020 21:12:43 +0200 Subject: [PATCH] Taxonomies! --- config.go | 3 ++ database_migrations.go | 7 ++++ example-config.yaml | 2 + go.mod | 8 ++-- go.sum | 20 ++++----- http.go | 33 +++++++++++---- posts.go | 88 +++++++++++++++++++++++++++++---------- render.go | 12 +++++- templates/taxonomy.gohtml | 16 +++++++ 9 files changed, 145 insertions(+), 44 deletions(-) create mode 100644 templates/taxonomy.gohtml diff --git a/config.go b/config.go index 8c810a5..8f95ee3 100644 --- a/config.go +++ b/config.go @@ -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") diff --git a/database_migrations.go b/database_migrations.go index cabb8f3..ffb5753 100644 --- a/database_migrations.go +++ b/database_migrations.go @@ -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 { diff --git a/example-config.yaml b/example-config.yaml index 3a7950c..c0f64fe 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -15,6 +15,8 @@ blog: title: My blog sections: - posts + taxonomies: + - tags user: nick: admin name: Admin diff --git a/go.mod b/go.mod index bdc4f48..59a1b9b 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f4dc5c4..676332d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/http.go b/http.go index 203fd1c..06ad45e 100644 --- a/http.go +++ b/http.go @@ -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) diff --git a/posts.go b/posts.go index 062bbcb..d66ef95 100644 --- a/posts.go +++ b/posts.go @@ -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") diff --git a/render.go b/render.go index df7ce15..ae356ac 100644 --- a/render.go +++ b/render.go @@ -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) } } diff --git a/templates/taxonomy.gohtml b/templates/taxonomy.gohtml new file mode 100644 index 0000000..294322f --- /dev/null +++ b/templates/taxonomy.gohtml @@ -0,0 +1,16 @@ +{{ define "title" }} + {{ blog.Title }} +{{ end }} + +{{ define "main" }} +
+ +
+{{ end }} + +{{ define "taxonomy" }} + {{ template "base" . }} +{{ end }} \ No newline at end of file