Added blog page, article page and added latest articles to start page.

This commit is contained in:
2020-06-14 02:16:39 +02:00
parent 71ca09427f
commit da2bc2ef28
11 changed files with 325 additions and 57 deletions

189
blog/blog.go Normal file
View File

@@ -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)
}
}
}

3
dev.sh
View File

@@ -7,5 +7,8 @@ export MB_LOGLEVEL=debug
export MB_ALLOWED_ORIGINS=* export MB_ALLOWED_ORIGINS=*
export MB_HOST=localhost:8080 export MB_HOST=localhost:8080
export MB_HOT_RELOAD=true 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 go run main.go

View File

@@ -30,6 +30,10 @@ services:
restart: always restart: always
depends_on: depends_on:
- traefik - traefik
environment:
- MB_EMVI_CLIENT_ID: 3fBBn144yvSF9R3dPC8l
- MB_EMVI_CLIENT_SECRET: dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
- MB_EMVI_ORGA: marvin
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.marvinblum.entrypoints=web" - "traefik.http.routers.marvinblum.entrypoints=web"

12
go.mod
View File

@@ -3,9 +3,11 @@ module github.com/Kugelschieber/marvinblum.de
go 1.14 go 1.14
require ( require (
github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/NYTimes/gziphandler v1.1.1
github.com/caddyserver/certmagic v0.10.13 // indirect github.com/caddyserver/certmagic v0.10.13
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 // indirect github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8
github.com/gorilla/mux v1.7.4 // indirect github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934
github.com/rs/cors v1.7.0 // indirect github.com/gorilla/mux v1.7.4
github.com/gosimple/slug v1.9.0
github.com/rs/cors v1.7.0
) )

6
go.sum
View File

@@ -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-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/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/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 h1:+G10WRp72llJuaW89QezNK8QU9SsOmd5Ja7ocrrUaI0=
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw= 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= 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.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 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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/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/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= 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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 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/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/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/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

65
main.go
View File

@@ -1,13 +1,14 @@
package main package main
import ( import (
"bytes" "github.com/Kugelschieber/marvinblum.de/blog"
"github.com/Kugelschieber/marvinblum.de/tpl"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
emvi "github.com/emvi/api-go"
"github.com/emvi/logbuch" "github.com/emvi/logbuch"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/rs/cors" "github.com/rs/cors"
"html/template"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@@ -16,17 +17,10 @@ import (
const ( const (
staticDir = "static" staticDir = "static"
staticDirPrefix = "/static/" staticDirPrefix = "/static/"
templateDir = "template/*"
logTimeFormat = "2006-01-02_15:04:05" logTimeFormat = "2006-01-02_15:04:05"
envPrefix = "MB_" envPrefix = "MB_"
) )
var (
tpl *template.Template
tplCache = make(map[string][]byte)
hotReload bool
)
func configureLog() { func configureLog() {
logbuch.SetFormatter(logbuch.NewFieldFormatter(logTimeFormat, "\t\t")) logbuch.SetFormatter(logbuch.NewFieldFormatter(logTimeFormat, "\t\t"))
logbuch.Info("Configure logging...") logbuch.Info("Configure logging...")
@@ -50,42 +44,17 @@ func logEnvConfig() {
} }
} }
func loadTemplate() { func serveAbout() http.HandlerFunc {
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)
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if hotReload { data := struct {
loadTemplate() Articles []emvi.Article
renderTemplate(name) }{
blog.GetLatestArticles(),
} }
if _, err := w.Write(tplCache[name]); err != nil { if err := tpl.Get().ExecuteTemplate(w, "about.html", data); err != nil {
logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name}) 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 { func setupRouter() *mux.Router {
router := mux.NewRouter() router := mux.NewRouter()
router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir))))) router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir)))))
router.Handle("/legal", serveTemplate("legal.html")) router.Handle("/blog/{slug}", blog.ServeBlogArticle())
router.Handle("/blog", serveTemplate("blog.html")) router.Handle("/blog", blog.ServeBlogPage())
router.Handle("/", serveTemplate("about.html")) router.Handle("/legal", tpl.ServeTemplate("legal.html"))
router.NotFoundHandler = serveTemplate("notfound.html") router.Handle("/", serveAbout())
router.NotFoundHandler = tpl.ServeTemplate("notfound.html")
return router return router
} }
@@ -136,7 +106,8 @@ func start(handler http.Handler) {
func main() { func main() {
configureLog() configureLog()
logEnvConfig() logEnvConfig()
loadTemplate() tpl.LoadTemplate()
blog.InitBlog()
router := setupRouter() router := setupRouter()
corsConfig := configureCors(router) corsConfig := configureCors(router)
start(corsConfig) start(corsConfig)

View File

@@ -19,9 +19,13 @@
</section> </section>
<section> <section>
<h2>latest blog posts</h2> <h2>latest blog posts</h2>
<p> {{range $article := .Articles}}
TODO <p>
</p> <a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
</p>
{{else}}
<p>There are no blog posts yet...</p>
{{end}}
<p> <p>
<a href="/blog">View all</a> <a href="/blog">View all</a>
</p> </p>

10
template/article.html Normal file
View File

@@ -0,0 +1,10 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h1>{{.Title}}</h1>
<small>Published on {{.Published}}</small>
{{.Content}}
</section>
{{template "end.html"}}

View File

@@ -2,7 +2,18 @@
{{template "menu.html"}} {{template "menu.html"}}
<section> <section>
<p>TODO Blog</p> <h1>Blog</h1>
{{range $year, $articles := .Articles}}
<h2>{{$year}}</h2>
{{range $article := $articles}}
<p>
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
</p>
{{end}}
{{else}}
<p>There are no blog posts yet...</p>
{{end}}
</section> </section>
{{template "end.html"}} {{template "end.html"}}

View File

@@ -5,7 +5,7 @@
<h2>Page not found</h2> <h2>Page not found</h2>
<p> <p>
Nothing to see here...<br /> Nothing to see here...<br />
<a href="/">Go back Home</a> <a href="/">Return home</a>
</p> </p>
</section> </section>

68
tpl/template.go Normal file
View File

@@ -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})
}
}
}