Add native blogroll integration

This commit is contained in:
Jan-Lukas Else 2021-05-08 21:22:48 +02:00
parent 42873f8681
commit 1ef34889ae
15 changed files with 211 additions and 28 deletions

115
blogroll.go Normal file
View File

@ -0,0 +1,115 @@
package main
import (
"fmt"
"io"
"net/http"
"time"
"github.com/kaorimatz/go-opml"
"github.com/thoas/go-funk"
"golang.org/x/sync/singleflight"
)
var blogrollCacheGroup singleflight.Group
func serveBlogroll(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
c := appConfig.Blogs[blog].Blogroll
if !c.Enabled {
serve404(w, r)
return
}
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return getBlogrollOutlines(c)
})
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
if appConfig.Cache != nil && appConfig.Cache.Enable {
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
}
render(w, r, templateBlogroll, &renderData{
BlogString: blog,
Data: map[string]interface{}{
"Title": c.Title,
"Description": c.Description,
"Outlines": outlines,
"Download": c.Path + ".opml",
},
})
}
func serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string)
c := appConfig.Blogs[blog].Blogroll
if !c.Enabled {
serve404(w, r)
return
}
outlines, err, _ := blogrollCacheGroup.Do(blog, func() (interface{}, error) {
return getBlogrollOutlines(c)
})
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
if appConfig.Cache != nil && appConfig.Cache.Enable {
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
}
w.Header().Set(contentType, contentTypeXMLUTF8)
mw := minifier.Writer(contentTypeXML, w)
defer func() {
_ = mw.Close()
}()
_ = opml.Render(mw, &opml.OPML{
Version: "2.0",
DateCreated: time.Now().UTC(),
Outlines: outlines.([]*opml.Outline),
})
}
func getBlogrollOutlines(config *configBlogroll) ([]*opml.Outline, error) {
if config.cachedOutlines != nil && time.Since(config.lastCache).Minutes() < 60 {
// return cache if younger than 60 min
return config.cachedOutlines, nil
}
req, err := http.NewRequest(http.MethodGet, config.Opml, nil)
if err != nil {
return nil, err
}
if config.AuthHeader != "" && config.AuthValue != "" {
req.Header.Set(config.AuthHeader, config.AuthValue)
}
res, err := appHttpClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
res.Body.Close()
}()
if code := res.StatusCode; code < 200 || 300 <= code {
return nil, fmt.Errorf("opml request not successfull, status code: %d", code)
}
o, err := opml.Parse(res.Body)
if err != nil {
return nil, err
}
outlines := o.Outlines
if len(config.Categories) > 0 {
filtered := []*opml.Outline{}
for _, category := range config.Categories {
if outline, ok := funk.Find(outlines, func(outline *opml.Outline) bool {
return outline.Title == category || outline.Text == category
}).(*opml.Outline); ok && outline != nil {
filtered = append(filtered, outline)
}
}
outlines = filtered
}
config.cachedOutlines = outlines
config.lastCache = time.Now()
return outlines, nil
}

View File

@ -21,7 +21,7 @@ import (
)
const (
cacheInternalExpirationHeader = "GoBlog-Expire"
cacheInternalExpirationHeader = "Goblog-Expire"
)
var (
@ -212,6 +212,9 @@ func purgeCache() {
cacheR.Clear()
}
func setInternalCacheExpirationHeader(w http.ResponseWriter, expiration int) {
func setInternalCacheExpirationHeader(w http.ResponseWriter, r *http.Request, expiration int) {
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
return
}
w.Header().Set(cacheInternalExpirationHeader, strconv.Itoa(expiration))
}

View File

@ -4,7 +4,9 @@ import (
"errors"
"net/url"
"strings"
"time"
"github.com/kaorimatz/go-opml"
"github.com/spf13/viper"
)
@ -63,6 +65,7 @@ type configBlog struct {
Photos *photos `mapstructure:"photos"`
Search *search `mapstructure:"search"`
BlogStats *blogStats `mapstructure:"blogStats"`
Blogroll *configBlogroll `mapstructure:"blogroll"`
CustomPages []*customPage `mapstructure:"custompages"`
Telegram *configTelegram `mapstructure:"telegram"`
PostAsHome bool `mapstructure:"postAsHome"`
@ -115,6 +118,19 @@ type blogStats struct {
Description string `mapstructure:"description"`
}
type configBlogroll struct {
Enabled bool `mapstructure:"enabled"`
Path string `mapstructure:"path"`
Opml string `mapstructure:"opml"`
AuthHeader string `mapstructure:"authHeader"`
AuthValue string `mapstructure:"authValue"`
Categories []string `mapstructure:"categories"`
Title string `mapstructure:"title"`
Description string `mapstructure:"description"`
cachedOutlines []*opml.Outline
lastCache time.Time
}
type customPage struct {
Path string `mapstructure:"path"`
Template string `mapstructure:"template"`
@ -288,6 +304,12 @@ func initConfig() error {
b.Comments = &comments{Enabled: false}
}
}
// Check config for each blog
for _, blog := range appConfig.Blogs {
if br := blog.Blogroll; br != nil && br.Enabled && br.Opml == "" {
br.Enabled = false
}
}
return nil
}

View File

@ -8,9 +8,9 @@ func serveCustomPage(w http.ResponseWriter, r *http.Request) {
page := r.Context().Value(customPageContextKey).(*customPage)
if appConfig.Cache != nil && appConfig.Cache.Enable && page.Cache {
if page.CacheExpiration != 0 {
setInternalCacheExpirationHeader(w, page.CacheExpiration)
setInternalCacheExpirationHeader(w, r, page.CacheExpiration)
} else {
setInternalCacheExpirationHeader(w, int(appConfig.Cache.Expiration))
setInternalCacheExpirationHeader(w, r, int(appConfig.Cache.Expiration))
}
}
render(w, r, page.Template, &renderData{

View File

@ -175,6 +175,17 @@ blogs:
path: /statistics # Path
title: Statistics # Title
description: "Here are some statistics with the number of posts per year:" # Description
# Blogroll
blogroll:
enabled: true # Enable
path: /blogroll # Path
title: Blogroll # Title
description: "I follow these blog:" # Description
opml: https://example.com/blogroll.opml # Required, URL to the OPML file
authHeader: X-Auth # Optional, header to use for OPML authentication
authValue: abc # Authentication value for OPML
categories: # Optional, allow only these categories
- Blogs
# Custom pages
custompages:
- path: /blogroll # Path

1
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/gorilla/handlers v1.5.1
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/joncrlsn/dque v0.0.0-20200702023911-3e80e3146ce5
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9
github.com/kr/text v0.2.0 // indirect
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible

3
go.sum
View File

@ -178,6 +178,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9 h1:+9REu9CK9D1AQ6C/PXXwGRcoKdT04cuHR5JgGD4DKqc=
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9/go.mod h1:OvY5ZBrAC9kOvM2PZs9Lw0BH+5K7tjrT6T7SFhn27OA=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI=
@ -413,6 +415,7 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210508051633-16afe75a6701 h1:lQVgcB3+FoAXOb20Dp6zTzAIrpj1k/yOOBN7s+Zv1rA=

13
http.go
View File

@ -26,6 +26,7 @@ const (
charsetUtf8Suffix = "; charset=utf-8"
contentTypeHTML = "text/html"
contentTypeXML = "text/xml"
contentTypeJSON = "application/json"
contentTypeWWWForm = "application/x-www-form-urlencoded"
contentTypeMultipartForm = "multipart/form-data"
@ -35,6 +36,7 @@ const (
contentTypeJSONFeed = "application/feed+json"
contentTypeHTMLUTF8 = contentTypeHTML + charsetUtf8Suffix
contentTypeXMLUTF8 = contentTypeXML + charsetUtf8Suffix
contentTypeJSONUTF8 = contentTypeJSON + charsetUtf8Suffix
contentTypeASUTF8 = contentTypeAS + charsetUtf8Suffix
@ -542,6 +544,17 @@ func buildDynamicRouter() (*chi.Mux, error) {
commentsPath := blogPath + "/comment"
r.With(sbm, commentsMiddlewares[blog]).Mount(commentsPath, commentsRouter)
}
// Blogroll
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
brPath := blogPath + brConfig.Path
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(cacheMiddleware, sbm)
r.Get(brPath, serveBlogroll)
r.Get(brPath+".opml", serveBlogrollExport)
})
}
}
// Sitemap

View File

@ -17,7 +17,7 @@ func initMinify() {
minifier = minify.New()
minifier.AddFunc(contentTypeHTML, mHtml.Minify)
minifier.AddFunc("text/css", mCss.Minify)
minifier.AddFunc("text/xml", mXml.Minify)
minifier.AddFunc(contentTypeXML, mXml.Minify)
minifier.AddFunc("application/javascript", mJs.Minify)
minifier.AddFunc(contentTypeRSS, mXml.Minify)
minifier.AddFunc(contentTypeATOM, mXml.Minify)

View File

@ -8,6 +8,7 @@ import (
"html/template"
"log"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
@ -39,6 +40,7 @@ const (
templateCommentsAdmin = "commentsadmin"
templateNotificationsAdmin = "notificationsadmin"
templateWebmentionAdmin = "webmentionadmin"
templateBlogroll = "blogroll"
)
var templates map[string]*template.Template = map[string]*template.Template{}
@ -164,6 +166,9 @@ func initRendering() error {
})
return mentions
},
"urlToString": func(u url.URL) string {
return u.String()
},
}
baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt))

View File

@ -114,6 +114,12 @@ func serveSitemap(w http.ResponseWriter, r *http.Request) {
Loc: appConfig.Server.PublicAddress + bc.getRelativePath(bc.BlogStats.Path),
})
}
// Blogroll
if bc.Blogroll != nil && bc.Blogroll.Enabled {
sm.Add(&sitemap.URL{
Loc: appConfig.Server.PublicAddress + bc.getRelativePath(bc.Blogroll.Path),
})
}
// Custom pages
for _, cp := range bc.CustomPages {
sm.Add(&sitemap.URL{

24
templates/blogroll.gohtml Normal file
View File

@ -0,0 +1,24 @@
{{ define "title" }}
<title>{{ .Data.Title }} - {{ .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
{{ with .Data.Title }}<h1>{{ . }}</h1>{{ end }}
{{ with .Data.Description }}{{ md . }}{{ end }}
<p><a href="{{ blogrelative .Blog .Data.Download }}" class="button" download>{{ string .Blog.Lang "download" }}</a></p>
{{ $lang := .Blog.Lang }}
{{ range .Data.Outlines }}
<h2>{{ with .Title }}{{ . }}{{ else }}{{ with .Text }}{{ . }}{{ end }}{{ end }}</h2>
<ul>
{{ range .Outlines }}
<li><a href="{{ urlToString .HTMLURL }}" target="_blank">{{ with .Title }}{{ . }}{{ else }}{{ with .Text }}{{ . }}{{ end }}{{ end }}</a> (<a href="{{ urlToString .XMLURL }}" target="_blank">{{ string $lang "feed" }}</a>)</li>
{{ end }}
</ul>
{{ end }}
</main>
{{ end }}
{{ define "blogroll" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,23 +0,0 @@
{{ define "title" }}
<title>{{ .Data.Title }} - {{ .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<h1>{{ .Data.Title }}</h1>
{{ md .Data.Description }}
{{ $opmlJson := index ( jsonFile "data/opml.json" ) "outline" }}
{{ range $opmlJson }}
{{ md (printf "%s %s" "##" ._text) }}
<ul>
{{ range (index . "outline") }}
<li><a href="{{ ._htmlUrl }}" target="_blank">{{ ._title }}</a> (<a href="{{ ._xmlUrl }}" target="_blank">Feed</a>)</li>
{{ end }}
</ul>
{{ end }}
</main>
{{ end }}
{{ define "blogroll" }}
{{ template "base" . }}
{{ end }}

View File

@ -6,6 +6,7 @@ confirmdelete: "Löschen bestätigen"
create: "Erstellen"
delete: "Löschen"
docomment: "Kommentieren"
download: "Herunterladen"
drafts: "Entwürfe"
editor: "Editor"
interactions: "Interaktionen & Kommentare"

View File

@ -11,8 +11,10 @@ confirmdelete: "Confirm deletion"
create: "Create"
delete: "Delete"
docomment: "Comment"
download: "Download"
drafts: "Drafts"
editor: "Editor"
feed: "Feed"
indieauth: "IndieAuth"
interactions: "Interactions & Comments"
interactionslabel: "Have you published a response to this? Paste the URL here."