diff --git a/blog/blog.go b/blog/blog.go new file mode 100644 index 0000000..7d7d1ae --- /dev/null +++ b/blog/blog.go @@ -0,0 +1,189 @@ +package blog + +import ( + "github.com/Kugelschieber/marvinblum.de/tpl" + emvi "github.com/emvi/api-go" + "github.com/emvi/logbuch" + "github.com/gorilla/mux" + "html/template" + "net/http" + "os" + "strings" + "time" +) + +const ( + blogCacheTime = time.Hour + maxLatestArticles = 3 +) + +var ( + blog Blog +) + +type Blog struct { + client *emvi.Client + articles map[string]emvi.Article // id -> article + articlesYear map[int][]emvi.Article // year -> articles + nextUpdate time.Time +} + +func InitBlog() { + logbuch.Info("Initializing blog") + blog.client = emvi.NewClient(os.Getenv("MB_EMVI_CLIENT_ID"), + os.Getenv("MB_EMVI_CLIENT_SECRET"), + os.Getenv("MB_EMVI_ORGA"), + nil) + blog.nextUpdate = time.Now().Add(blogCacheTime) + blog.loadArticles() +} + +func (blog *Blog) loadArticles() { + logbuch.Info("Refreshing blog articles...") + articles, offset, count := make(map[string]emvi.Article), 0, 1 + var err error + + for count > 0 { + var results []emvi.Article + results, _, err = blog.client.FindArticles("", &emvi.ArticleFilter{ + BaseSearch: emvi.BaseSearch{Offset: offset}, + Tags: "blog", + SortPublished: emvi.SortDescending, + }) + + if err != nil { + logbuch.Error("Error loading blog articles", logbuch.Fields{"err": err}) + break + } + + offset += len(results) + count = len(results) + + for _, article := range results { + articles[article.Id] = article + } + } + + if err == nil { + for k, v := range articles { + v.LatestArticleContent = blog.loadArticle(v) + articles[k] = v + } + + blog.setArticles(articles) + } + + blog.nextUpdate = time.Now().Add(blogCacheTime) + logbuch.Info("Done", logbuch.Fields{"count": len(articles)}) +} + +func (blog *Blog) loadArticle(article emvi.Article) *emvi.ArticleContent { + _, content, _, err := blog.client.GetArticle(article.Id, article.LatestArticleContent.LanguageId, 0) + + if err != nil { + logbuch.Error("Error loading article", logbuch.Fields{"err": err, "id": article.Id}) + return nil + } + + logbuch.Debug("Article loaded", logbuch.Fields{"id": article.Id}) + return content +} + +func (blog *Blog) setArticles(articles map[string]emvi.Article) { + blog.articles = articles + blog.articlesYear = make(map[int][]emvi.Article) + + for _, article := range articles { + if blog.articlesYear[article.Published.Year()] == nil { + blog.articlesYear[article.Published.Year()] = make([]emvi.Article, 0) + } + + blog.articlesYear[article.Published.Year()] = append(blog.articlesYear[article.Published.Year()], article) + } +} + +func (blog *Blog) getArticle(id string) emvi.Article { + blog.refreshIfRequired() + return blog.articles[id] +} + +func (blog *Blog) getArticles() map[int][]emvi.Article { + blog.refreshIfRequired() + return blog.articlesYear +} + +func (blog *Blog) getLatestArticles() []emvi.Article { + blog.refreshIfRequired() + articles := make([]emvi.Article, 0, 3) + i := 0 + + for _, v := range blog.articles { + articles = append(articles, v) + i++ + + if i > maxLatestArticles { + break + } + } + + return articles +} + +func (blog *Blog) refreshIfRequired() { + if blog.nextUpdate.Before(time.Now()) { + blog.loadArticles() + } +} + +func GetLatestArticles() []emvi.Article { + return blog.getLatestArticles() +} + +func ServeBlogPage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data := struct { + Articles map[int][]emvi.Article + }{ + blog.getArticles(), + } + + if err := tpl.Get().ExecuteTemplate(w, "blog.html", data); err != nil { + logbuch.Error("Error executing blog template", logbuch.Fields{"err": err}) + w.WriteHeader(http.StatusInternalServerError) + } + } +} + +func ServeBlogArticle() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + slug := strings.Split(vars["slug"], "-") + + if len(slug) == 0 { + http.Redirect(w, r, "/notfound", http.StatusFound) + return + } + + article := blog.getArticle(slug[len(slug)-1]) + + if len(article.Id) == 0 { + http.Redirect(w, r, "/notfound", http.StatusFound) + return + } + + data := struct { + Title string + Content template.HTML + Published string + }{ + article.LatestArticleContent.Title, + template.HTML(article.LatestArticleContent.Content), + article.Published.Format("2. January 2006"), + } + + if err := tpl.Get().ExecuteTemplate(w, "article.html", data); err != nil { + logbuch.Error("Error executing blog article template", logbuch.Fields{"err": err}) + w.WriteHeader(http.StatusInternalServerError) + } + } +} diff --git a/dev.sh b/dev.sh index 9fe65cb..e86e4e6 100755 --- a/dev.sh +++ b/dev.sh @@ -7,5 +7,8 @@ export MB_LOGLEVEL=debug export MB_ALLOWED_ORIGINS=* export MB_HOST=localhost:8080 export MB_HOT_RELOAD=true +export MB_EMVI_CLIENT_ID=3fBBn144yvSF9R3dPC8l +export MB_EMVI_CLIENT_SECRET=dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg +export MB_EMVI_ORGA=marvin go run main.go diff --git a/docker-compose.yml b/docker-compose.yml index 5b3e089..968bd00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,10 @@ services: restart: always depends_on: - traefik + environment: + - MB_EMVI_CLIENT_ID: 3fBBn144yvSF9R3dPC8l + - MB_EMVI_CLIENT_SECRET: dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg + - MB_EMVI_ORGA: marvin labels: - "traefik.enable=true" - "traefik.http.routers.marvinblum.entrypoints=web" diff --git a/go.mod b/go.mod index a350cb5..5fdb28f 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/Kugelschieber/marvinblum.de go 1.14 require ( - github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/caddyserver/certmagic v0.10.13 // indirect - github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 // indirect - github.com/gorilla/mux v1.7.4 // indirect - github.com/rs/cors v1.7.0 // indirect + github.com/NYTimes/gziphandler v1.1.1 + github.com/caddyserver/certmagic v0.10.13 + github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8 + github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 + github.com/gorilla/mux v1.7.4 + github.com/gosimple/slug v1.9.0 + github.com/rs/cors v1.7.0 ) diff --git a/go.sum b/go.sum index 50418f0..6e5c114 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/dnsimple/dnsimple-go v0.60.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8 h1:DZJfRdvwlybLvHkyeN8HRU4QnBFqVZg21ki4to1AVPo= +github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8/go.mod h1:g9RdDC3s5ebCknAHQQ5PjoM2vRFSyyGoOUX3QkDKU+o= github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 h1:+G10WRp72llJuaW89QezNK8QU9SsOmd5Ja7ocrrUaI0= github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -137,6 +139,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= +github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -216,6 +220,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/main.go b/main.go index e114c35..b477bbc 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,14 @@ package main import ( - "bytes" + "github.com/Kugelschieber/marvinblum.de/blog" + "github.com/Kugelschieber/marvinblum.de/tpl" "github.com/NYTimes/gziphandler" "github.com/caddyserver/certmagic" + emvi "github.com/emvi/api-go" "github.com/emvi/logbuch" "github.com/gorilla/mux" "github.com/rs/cors" - "html/template" "net/http" "os" "strings" @@ -16,17 +17,10 @@ import ( const ( staticDir = "static" staticDirPrefix = "/static/" - templateDir = "template/*" logTimeFormat = "2006-01-02_15:04:05" envPrefix = "MB_" ) -var ( - tpl *template.Template - tplCache = make(map[string][]byte) - hotReload bool -) - func configureLog() { logbuch.SetFormatter(logbuch.NewFieldFormatter(logTimeFormat, "\t\t")) logbuch.Info("Configure logging...") @@ -50,42 +44,17 @@ func logEnvConfig() { } } -func loadTemplate() { - logbuch.Debug("Loading templates") - var err error - tpl, err = template.ParseGlob(templateDir) - - if err != nil { - logbuch.Fatal("Error loading template", logbuch.Fields{"err": err}) - } - - hotReload = os.Getenv("MB_HOT_RELOAD") == "true" - logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": hotReload}) -} - -func renderTemplate(name string) { - logbuch.Debug("Rendering template", logbuch.Fields{"name": name}) - var buffer bytes.Buffer - - if err := tpl.ExecuteTemplate(&buffer, name, nil); err != nil { - logbuch.Fatal("Error executing template", logbuch.Fields{"err": err, "name": name}) - } - - tplCache[name] = buffer.Bytes() -} - -func serveTemplate(name string) http.HandlerFunc { - // render once so we have it in cache - renderTemplate(name) - +func serveAbout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if hotReload { - loadTemplate() - renderTemplate(name) + data := struct { + Articles []emvi.Article + }{ + blog.GetLatestArticles(), } - if _, err := w.Write(tplCache[name]); err != nil { - logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name}) + if err := tpl.Get().ExecuteTemplate(w, "about.html", data); err != nil { + logbuch.Error("Error executing blog template", logbuch.Fields{"err": err}) + w.WriteHeader(http.StatusInternalServerError) } } } @@ -93,10 +62,11 @@ func serveTemplate(name string) http.HandlerFunc { func setupRouter() *mux.Router { router := mux.NewRouter() router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir))))) - router.Handle("/legal", serveTemplate("legal.html")) - router.Handle("/blog", serveTemplate("blog.html")) - router.Handle("/", serveTemplate("about.html")) - router.NotFoundHandler = serveTemplate("notfound.html") + router.Handle("/blog/{slug}", blog.ServeBlogArticle()) + router.Handle("/blog", blog.ServeBlogPage()) + router.Handle("/legal", tpl.ServeTemplate("legal.html")) + router.Handle("/", serveAbout()) + router.NotFoundHandler = tpl.ServeTemplate("notfound.html") return router } @@ -136,7 +106,8 @@ func start(handler http.Handler) { func main() { configureLog() logEnvConfig() - loadTemplate() + tpl.LoadTemplate() + blog.InitBlog() router := setupRouter() corsConfig := configureCors(router) start(corsConfig) diff --git a/template/about.html b/template/about.html index 15d0c6b..9f9b943 100644 --- a/template/about.html +++ b/template/about.html @@ -19,9 +19,13 @@

latest blog posts

-

- TODO -

+ {{range $article := .Articles}} +

+ {{$article.LatestArticleContent.Title}} +

+ {{else}} +

There are no blog posts yet...

+ {{end}}

View all

diff --git a/template/article.html b/template/article.html new file mode 100644 index 0000000..4c49f22 --- /dev/null +++ b/template/article.html @@ -0,0 +1,10 @@ +{{template "head.html"}} +{{template "menu.html"}} + +
+

{{.Title}}

+ Published on {{.Published}} + {{.Content}} +
+ +{{template "end.html"}} diff --git a/template/blog.html b/template/blog.html index d16ce71..1da03c3 100644 --- a/template/blog.html +++ b/template/blog.html @@ -2,7 +2,18 @@ {{template "menu.html"}}
-

TODO Blog

+

Blog

+ {{range $year, $articles := .Articles}} +

{{$year}}

+ + {{range $article := $articles}} +

+ {{$article.LatestArticleContent.Title}} +

+ {{end}} + {{else}} +

There are no blog posts yet...

+ {{end}}
{{template "end.html"}} diff --git a/template/notfound.html b/template/notfound.html index 6c036ab..7885e4c 100644 --- a/template/notfound.html +++ b/template/notfound.html @@ -5,7 +5,7 @@

Page not found

Nothing to see here...
- Go back Home + Return home

diff --git a/tpl/template.go b/tpl/template.go new file mode 100644 index 0000000..d37f20c --- /dev/null +++ b/tpl/template.go @@ -0,0 +1,68 @@ +package tpl + +import ( + "bytes" + "github.com/emvi/logbuch" + "github.com/gosimple/slug" + "html/template" + "net/http" + "os" +) + +const ( + templateDir = "template/*" +) + +var ( + tpl *template.Template + tplCache = make(map[string][]byte) + hotReload bool +) + +var funcMap = template.FuncMap{ + "slug": slug.Make, +} + +func LoadTemplate() { + logbuch.Debug("Loading templates") + var err error + tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir) + + if err != nil { + logbuch.Fatal("Error loading template", logbuch.Fields{"err": err}) + } + + hotReload = os.Getenv("MB_HOT_RELOAD") == "true" + logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": hotReload}) +} + +func renderTemplate(name string) { + logbuch.Debug("Rendering template", logbuch.Fields{"name": name}) + var buffer bytes.Buffer + + if err := tpl.ExecuteTemplate(&buffer, name, nil); err != nil { + logbuch.Fatal("Error executing template", logbuch.Fields{"err": err, "name": name}) + } + + tplCache[name] = buffer.Bytes() +} + +func Get() *template.Template { + return tpl +} + +func ServeTemplate(name string) http.HandlerFunc { + // render once so we have it in cache + renderTemplate(name) + + return func(w http.ResponseWriter, r *http.Request) { + if hotReload { + LoadTemplate() + renderTemplate(name) + } + + if _, err := w.Write(tplCache[name]); err != nil { + logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name}) + } + } +}