mirror of https://github.com/jlelse/GoBlog
A lot of progress
This commit is contained in:
parent
fcac755814
commit
95909420ba
|
@ -3,4 +3,5 @@
|
|||
/config.yaml
|
||||
/data
|
||||
/GoBlog
|
||||
/tmp_assets
|
||||
/tmp_assets
|
||||
/certmagic
|
|
@ -40,6 +40,8 @@ type asAttachment struct {
|
|||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// TODO: Serve index
|
||||
|
||||
func servePostActivityStreams(w http.ResponseWriter, r *http.Request) {
|
||||
// Remove ".as" from path again
|
||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, ".as")
|
||||
|
@ -69,14 +71,14 @@ func servePostActivityStreams(w http.ResponseWriter, r *http.Request) {
|
|||
as.Type = "Note"
|
||||
}
|
||||
// Content
|
||||
if rendered, err := renderMarkdown(post.Content); err != nil {
|
||||
if rendered, err := renderMarkdown(post.Content); err == nil {
|
||||
as.Content = string(rendered)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
as.Content = string(rendered)
|
||||
}
|
||||
// Attachments
|
||||
if images := post.Parameters[appConfig.Blog.ActivityStreams.ImagesParameter]; len(images) > 0 {
|
||||
if images := post.Parameters[appConfig.Blogs[post.Blog].ActivityStreams.ImagesParameter]; len(images) > 0 {
|
||||
for _, image := range images {
|
||||
as.Attachment = append(as.Attachment, &asAttachment{
|
||||
Type: "Image",
|
||||
|
@ -97,10 +99,10 @@ func servePostActivityStreams(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
// Reply
|
||||
if replyLink := post.firstParameter(appConfig.Blog.ActivityStreams.ReplyParameter); replyLink != "" {
|
||||
if replyLink := post.firstParameter(appConfig.Blogs[post.Blog].ActivityStreams.ReplyParameter); replyLink != "" {
|
||||
as.InReplyTo = replyLink
|
||||
}
|
||||
// Send JSON
|
||||
w.Header().Add("Content-Type", contentTypeJSON)
|
||||
w.Header().Add(contentType, contentTypeJSONUTF8)
|
||||
_ = json.NewEncoder(w).Encode(as)
|
||||
}
|
||||
|
|
6
api.go
6
api.go
|
@ -16,7 +16,7 @@ func apiPostCreate(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = post.createOrReplace()
|
||||
err = post.createOrReplace(false)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
|
@ -44,7 +44,7 @@ func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = post.createOrReplace()
|
||||
err = post.createOrReplace(false)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
|
@ -64,7 +64,7 @@ func apiPostRead(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", contentTypeJSON)
|
||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
||||
err = json.NewEncoder(w).Encode(post)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
4
cache.go
4
cache.go
|
@ -108,13 +108,13 @@ func saveCache(path string, now time.Time, header map[string][]string, body []by
|
|||
_ = tx.Commit()
|
||||
}
|
||||
|
||||
func purgeCache(path string) {
|
||||
func purgeCache() {
|
||||
startWritingToDb()
|
||||
defer finishWritingToDb()
|
||||
tx, err := appDb.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = tx.Exec("delete from cache where path=?", path)
|
||||
_, _ = tx.Exec("delete from cache; vacuum;")
|
||||
_ = tx.Commit()
|
||||
}
|
||||
|
|
112
config.go
112
config.go
|
@ -1,17 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Server *configServer `mapstructure:"server"`
|
||||
Db *configDb `mapstructure:"database"`
|
||||
Cache *configCache `mapstructure:"cache"`
|
||||
Blog *configBlog `mapstructure:"blog"`
|
||||
User *configUser `mapstructure:"user"`
|
||||
Hooks *configHooks `mapstructure:"hooks"`
|
||||
Hugo *configHugo `mapstructure:"hugo"`
|
||||
Server *configServer `mapstructure:"server"`
|
||||
Db *configDb `mapstructure:"database"`
|
||||
Cache *configCache `mapstructure:"cache"`
|
||||
DefaultBlog string `mapstructure:"defaultblog"`
|
||||
Blogs map[string]*configBlog `mapstructure:"blogs"`
|
||||
User *configUser `mapstructure:"user"`
|
||||
Hooks *configHooks `mapstructure:"hooks"`
|
||||
Hugo *configHugo `mapstructure:"hugo"`
|
||||
Micropub *configMicropub `mapstructure:"micropub"`
|
||||
}
|
||||
|
||||
type configServer struct {
|
||||
|
@ -19,9 +23,9 @@ type configServer struct {
|
|||
Port int `mapstructure:"port"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
PublicAddress string `mapstructure:"publicAddress"`
|
||||
PublicHttps bool `mapstructure:"publicHttps"`
|
||||
PublicHTTPS bool `mapstructure:"publicHttps"`
|
||||
LetsEncryptMail string `mapstructure:"letsEncryptMail"`
|
||||
LocalHttps bool `mapstructure:"localHttps"`
|
||||
LocalHTTPS bool `mapstructure:"localHttps"`
|
||||
}
|
||||
|
||||
type configDb struct {
|
||||
|
@ -33,32 +37,25 @@ type configCache struct {
|
|||
Expiration int64 `mapstructure:"expiration"`
|
||||
}
|
||||
|
||||
// exposed to templates via function "blog"
|
||||
type configBlog struct {
|
||||
// Language of the blog, e.g. "en" or "de"
|
||||
Lang string `mapstructure:"lang"`
|
||||
// Title of the blog, e.g. "My blog"
|
||||
Title string `mapstructure:"title"`
|
||||
// Description of the blog
|
||||
Description string `mapstructure:"description"`
|
||||
// Number of posts per page
|
||||
Pagination int `mapstructure:"pagination"`
|
||||
// Sections
|
||||
Sections []*section `mapstructure:"sections"`
|
||||
// Taxonomies
|
||||
Taxonomies []*taxonomy `mapstructure:"taxonomies"`
|
||||
// Menus
|
||||
Menus map[string]*menu `mapstructure:"menus"`
|
||||
// Photos
|
||||
Photos *photos `mapstructure:"photos"`
|
||||
// ActivityStreams
|
||||
ActivityStreams *activityStreams `mapstructure:"activitystreams"`
|
||||
Path string `mapstructure:"path"`
|
||||
Lang string `mapstructure:"lang"`
|
||||
Title string `mapstructure:"title"`
|
||||
Description string `mapstructure:"description"`
|
||||
Pagination int `mapstructure:"pagination"`
|
||||
Sections map[string]*section `mapstructure:"sections"`
|
||||
Taxonomies []*taxonomy `mapstructure:"taxonomies"`
|
||||
Menus map[string]*menu `mapstructure:"menus"`
|
||||
Photos *photos `mapstructure:"photos"`
|
||||
ActivityStreams *activityStreams `mapstructure:"activitystreams"`
|
||||
DefaultSection string `mapstructure:"defaultsection"`
|
||||
}
|
||||
|
||||
type section struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Title string `mapstructure:"title"`
|
||||
Description string `mapstructure:"description"`
|
||||
Name string `mapstructure:"name"`
|
||||
Title string `mapstructure:"title"`
|
||||
Description string `mapstructure:"description"`
|
||||
PathTemplate string `mapstructure:"pathtemplate"`
|
||||
}
|
||||
|
||||
type taxonomy struct {
|
||||
|
@ -110,6 +107,22 @@ type frontmatter struct {
|
|||
Parameter string `mapstructure:"parameter"`
|
||||
}
|
||||
|
||||
type configMicropub struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Path string `mapstructure:"path"`
|
||||
AuthAllowed []string `mapstructure:"authAllowed"`
|
||||
TokenEndpoint string `mapstructure:"tokenEndpoint"`
|
||||
AuthEndpoint string `mapstructure:"authEndpoint"`
|
||||
Authn string `mapstructure:"authn"`
|
||||
CategoryParam string `mapstructure:"categoryParam"`
|
||||
ReplyParam string `mapstructure:"replyParam"`
|
||||
LikeParam string `mapstructure:"likeParam"`
|
||||
BookmarkParam string `mapstructure:"bookmarkParam"`
|
||||
AudioParam string `mapstructure:"audioParam"`
|
||||
PhotoParam string `mapstructure:"photoParam"`
|
||||
PhotoDescriptionParam string `mapstructure:"photoDescriptionParam"`
|
||||
}
|
||||
|
||||
var appConfig = &config{}
|
||||
|
||||
func initConfig() error {
|
||||
|
@ -119,41 +132,42 @@ func initConfig() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Defaults
|
||||
viper.SetDefault("server.logging", false)
|
||||
viper.SetDefault("server.port", 8080)
|
||||
viper.SetDefault("server.domain", "example.com")
|
||||
viper.SetDefault("server.publicAddress", "http://localhost:8080")
|
||||
viper.SetDefault("server.publicHttps", false)
|
||||
viper.SetDefault("server.letsEncryptMail", "mail@example.com")
|
||||
viper.SetDefault("server.localHttps", false)
|
||||
viper.SetDefault("database.file", "data/db.sqlite")
|
||||
viper.SetDefault("cache.enable", true)
|
||||
viper.SetDefault("cache.expiration", 600)
|
||||
viper.SetDefault("blog.lang", "en")
|
||||
viper.SetDefault("blog.title", "My blog")
|
||||
viper.SetDefault("blog.description", "This is my blog")
|
||||
viper.SetDefault("blog.pagination", 10)
|
||||
viper.SetDefault("blog.sections", []*section{{Name: "posts", Title: "Posts", Description: "**Posts** on this blog"}})
|
||||
viper.SetDefault("blog.taxonomies", []*taxonomy{{Name: "tags", Title: "Tags", Description: "**Tags** on this blog"}})
|
||||
viper.SetDefault("blog.menus", map[string]*menu{"main": {Items: []*menuItem{{Title: "Home", Link: "/"}, {Title: "Post", Link: "Posts"}}}})
|
||||
viper.SetDefault("blog.photos.enabled", true)
|
||||
viper.SetDefault("blog.photos.parameter", "images")
|
||||
viper.SetDefault("blog.photos.path", "/photos")
|
||||
viper.SetDefault("blog.photos.title", "Photos")
|
||||
viper.SetDefault("blog.photos.description", "Photos on this blog")
|
||||
viper.SetDefault("blog.activitystreams.enabled", false)
|
||||
viper.SetDefault("blog.activitystreams.replyParameter", "replylink")
|
||||
viper.SetDefault("blog.activitystreams.imagesParameter", "images")
|
||||
viper.SetDefault("user.nick", "admin")
|
||||
viper.SetDefault("user.name", "Admin")
|
||||
viper.SetDefault("user.password", "secret")
|
||||
viper.SetDefault("hooks.shell", "/bin/bash")
|
||||
viper.SetDefault("hugo.frontmatter", []*frontmatter{{Meta: "title", Parameter: "title"}, {Meta: "tags", Parameter: "tags"}})
|
||||
viper.SetDefault("micropub.enabled", true)
|
||||
viper.SetDefault("micropub.path", "/micropub")
|
||||
viper.SetDefault("micropub.authAllowed", []string{})
|
||||
viper.SetDefault("micropub.tokenEndpoint", "https://tokens.indieauth.com/token")
|
||||
viper.SetDefault("micropub.authEndpoint", "https://indieauth.com/auth")
|
||||
viper.SetDefault("micropub.categoryParam", "tags")
|
||||
viper.SetDefault("micropub.replyParam", "replylink")
|
||||
viper.SetDefault("micropub.likeParam", "likelink")
|
||||
viper.SetDefault("micropub.bookmarkParam", "link")
|
||||
viper.SetDefault("micropub.audioParam", "audio")
|
||||
viper.SetDefault("micropub.photoParam", "images")
|
||||
viper.SetDefault("micropub.photoDescriptionParam", "imagealts")
|
||||
// Unmarshal config
|
||||
err = viper.Unmarshal(appConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check config
|
||||
if len(appConfig.Blogs) == 0 {
|
||||
return errors.New("no blog configured")
|
||||
}
|
||||
if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil {
|
||||
return errors.New("no default blog or default blog not present")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/lopezator/migrator"
|
||||
)
|
||||
|
||||
func migrateDb() error {
|
||||
startWritingToDb()
|
||||
defer finishWritingToDb()
|
||||
m, err := migrator.New(
|
||||
migrator.Migrations(
|
||||
&migrator.Migration{
|
||||
Name: "00001",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE posts (path text not null primary key, content text, published text, updated text, blog text not null, section text);
|
||||
CREATE TABLE post_parameters (id integer primary key autoincrement, path text not null, parameter text not null, value text);
|
||||
CREATE INDEX index_pp_path on post_parameters (path);
|
||||
CREATE TABLE redirects (fromPath text not null, toPath text not null, primary key (fromPath, toPath));
|
||||
CREATE TABLE cache (path text not null primary key, time integer, header blob, body blob);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.Migrate(appDb); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/lopezator/migrator"
|
||||
)
|
||||
|
||||
func migrateDb() error {
|
||||
startWritingToDb()
|
||||
defer finishWritingToDb()
|
||||
m, err := migrator.New(
|
||||
migrator.Migrations(
|
||||
&migrator.Migration{
|
||||
Name: "00001",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create table posts (path text not null primary key, content text, published text, updated text);")
|
||||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00002",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create table redirects (fromPath text not null, toPath text not null, primary key (fromPath, toPath));")
|
||||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00003",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create table post_parameters (path text not null, parameter text not null, value text, primary key (path, parameter));")
|
||||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00004",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create table cache (path text not null primary key, time integer, header blob, body blob);")
|
||||
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
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00006",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create index pp_path_index on post_parameters (path);")
|
||||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00007",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("alter table posts add column section text; create trigger add_section after update on posts begin update posts set section = (select substr(path, 2, len) from (select path, instr(substr(path, 2),'/')-1 as len from (select new.path as path))) where path = new.path; end;")
|
||||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00008",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create index p_section_index on posts (section);")
|
||||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00009",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec("create trigger add_section_insert after insert on posts begin update posts set section = (select substr(path, 2, len) from (select path, instr(substr(path, 2),'/')-1 as len from (select new.path as path))) where path = new.path; end;")
|
||||
return err
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.Migrate(appDb); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -6,12 +6,14 @@ import (
|
|||
)
|
||||
|
||||
type errorData struct {
|
||||
Blog string
|
||||
Title string
|
||||
Message string
|
||||
}
|
||||
|
||||
func serve404(w http.ResponseWriter, r *http.Request) {
|
||||
render(w, templateError, &errorData{
|
||||
Blog: appConfig.DefaultBlog,
|
||||
Title: "404 Not Found",
|
||||
Message: fmt.Sprintf("`%s` was not found", r.RequestURI),
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ server:
|
|||
logging: false
|
||||
port: 8080
|
||||
domain: example.com
|
||||
publicAdress: http://localhost:8080
|
||||
publicAddress: http://localhost:8080
|
||||
publicHttps: false
|
||||
letsEncryptMail: mail@example.com
|
||||
localHttps: false
|
||||
|
@ -11,35 +11,37 @@ database:
|
|||
cache:
|
||||
enable: true
|
||||
expiration: 600
|
||||
blog:
|
||||
lang: en
|
||||
title: My blog
|
||||
description: This is my blog
|
||||
sections:
|
||||
- name: posts
|
||||
title: Posts
|
||||
description: "**Posts** on this blog"
|
||||
taxonomies:
|
||||
- name: tags
|
||||
title: Tags
|
||||
description: "**Tags** on this blog"
|
||||
menus:
|
||||
main:
|
||||
items:
|
||||
- title: Home
|
||||
link: /
|
||||
- title: Posts
|
||||
link: /posts
|
||||
photos:
|
||||
enable: true
|
||||
parameter: images
|
||||
path: /photos
|
||||
title: Photos
|
||||
description: "Photos on this blog"
|
||||
activitystreams:
|
||||
enable: true
|
||||
replyParameter: replylink
|
||||
imagesParameter: images
|
||||
blogs:
|
||||
main:
|
||||
path: /
|
||||
lang: en
|
||||
title: My blog
|
||||
description: This is my blog
|
||||
sections:
|
||||
- name: posts
|
||||
title: Posts
|
||||
description: "**Posts** on this blog"
|
||||
taxonomies:
|
||||
- name: tags
|
||||
title: Tags
|
||||
description: "**Tags** on this blog"
|
||||
menus:
|
||||
main:
|
||||
items:
|
||||
- title: Home
|
||||
link: /
|
||||
- title: Posts
|
||||
link: /posts
|
||||
photos:
|
||||
enable: true
|
||||
parameter: images
|
||||
path: /photos
|
||||
title: Photos
|
||||
description: "Photos on this blog"
|
||||
activitystreams:
|
||||
enable: true
|
||||
replyParameter: replylink
|
||||
imagesParameter: images
|
||||
user:
|
||||
nick: admin
|
||||
name: Admin
|
||||
|
@ -49,4 +51,19 @@ hugo:
|
|||
- meta: title
|
||||
parameter: title
|
||||
- meta: tags
|
||||
parameter: tags
|
||||
parameter: tags
|
||||
micropub:
|
||||
enabled: true
|
||||
path: /micropub
|
||||
authAllowed:
|
||||
- example.com
|
||||
tokenEndpoint: https://tokens.indieauth.com/token
|
||||
authEndpoint: https://indieauth.com/auth
|
||||
authn: login@example.com
|
||||
categoryParam: tags
|
||||
replyParam: replylink
|
||||
likeParam: likelink
|
||||
bookmarkParam: link
|
||||
audioParam: audio
|
||||
photoParam: images
|
||||
photoDescriptionParam: imagealts
|
35
feeds.go
35
feeds.go
|
@ -1,28 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/feeds"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
type feedType string
|
||||
|
||||
const (
|
||||
NONE feedType = ""
|
||||
RSS feedType = "rss"
|
||||
ATOM feedType = "atom"
|
||||
JSON feedType = "json"
|
||||
noFeed feedType = ""
|
||||
rssFeed feedType = "rss"
|
||||
atomFeed feedType = "atom"
|
||||
jsonFeed feedType = "json"
|
||||
)
|
||||
|
||||
func generateFeed(f feedType, w http.ResponseWriter, r *http.Request, posts []*Post, title string, description string) {
|
||||
func generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*Post, title string, description string) {
|
||||
now := time.Now()
|
||||
if title == "" {
|
||||
title = appConfig.Blog.Title
|
||||
title = appConfig.Blogs[blog].Title
|
||||
}
|
||||
if description == "" {
|
||||
description = appConfig.Blog.Description
|
||||
description = appConfig.Blogs[blog].Description
|
||||
}
|
||||
feed := &feeds.Feed{
|
||||
Title: title,
|
||||
|
@ -43,11 +44,11 @@ func generateFeed(f feedType, w http.ResponseWriter, r *http.Request, posts []*P
|
|||
var feedStr string
|
||||
var err error
|
||||
switch f {
|
||||
case RSS:
|
||||
case rssFeed:
|
||||
feedStr, err = feed.ToRss()
|
||||
case ATOM:
|
||||
case atomFeed:
|
||||
feedStr, err = feed.ToAtom()
|
||||
case JSON:
|
||||
case jsonFeed:
|
||||
feedStr, err = feed.ToJSON()
|
||||
default:
|
||||
return
|
||||
|
@ -57,12 +58,12 @@ func generateFeed(f feedType, w http.ResponseWriter, r *http.Request, posts []*P
|
|||
return
|
||||
}
|
||||
switch f {
|
||||
case RSS:
|
||||
w.Header().Add("Content-Type", "application/rss+xml; charset=utf-8")
|
||||
case ATOM:
|
||||
w.Header().Add("Content-Type", "application/atom+xml; charset=utf-8")
|
||||
case JSON:
|
||||
w.Header().Add("Content-Type", "application/feed+json; charset=utf-8")
|
||||
case rssFeed:
|
||||
w.Header().Add(contentType, "application/rss+xml; charset=utf-8")
|
||||
case atomFeed:
|
||||
w.Header().Add(contentType, "application/atom+xml; charset=utf-8")
|
||||
case jsonFeed:
|
||||
w.Header().Add(contentType, "application/feed+json; charset=utf-8")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(feedStr))
|
||||
|
|
20
go.mod
20
go.mod
|
@ -5,7 +5,7 @@ go 1.15
|
|||
require (
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/andybalholm/cascadia v1.2.0 // indirect
|
||||
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1
|
||||
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4
|
||||
github.com/caddyserver/certmagic v0.12.0
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||
|
@ -19,7 +19,7 @@ require (
|
|||
github.com/lib/pq v1.8.0 // indirect
|
||||
github.com/lopezator/migrator v0.3.0
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.3
|
||||
github.com/mattn/go-sqlite3 v1.14.4
|
||||
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,24 +27,24 @@ require (
|
|||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
||||
github.com/snabb/sitemap v1.0.0
|
||||
github.com/spf13/afero v1.4.0 // indirect
|
||||
github.com/spf13/afero v1.4.1 // indirect
|
||||
github.com/spf13/cast v1.3.1
|
||||
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.9.5
|
||||
github.com/tdewolff/minify/v2 v2.9.7
|
||||
github.com/vcraescu/go-paginator v0.0.0-20200923074551-426b20f3ae8a
|
||||
github.com/yuin/goldmark v1.2.1
|
||||
github.com/yuin/goldmark-emoji v1.0.1
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
||||
golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 // indirect
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect
|
||||
golang.org/x/tools v0.0.0-20200924224222-8d73f17870ce // indirect
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 // indirect
|
||||
golang.org/x/sync v0.0.0-20200930132711-30421366ff76 // indirect
|
||||
golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf // indirect
|
||||
golang.org/x/tools v0.0.0-20201005185003-576e169c3de7 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||
gopkg.in/ini.v1 v1.61.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||
honnef.co/go/tools v0.0.1-2020.1.5 // indirect
|
||||
|
|
45
go.sum
45
go.sum
|
@ -23,8 +23,8 @@ github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5z
|
|||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
|
||||
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
|
||||
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1 h1:TEBmxO80TM04L8IuMWk77SGL1HomBmKTdzdJLLWznxI=
|
||||
github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
|
||||
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4 h1:OkS1BqB3CzLtGRznRyvriSY8jeaVk2CrDn2ZiRQgMUI=
|
||||
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4/go.mod h1:hMAUZFIkk4B1FouGxqlogyMyU6BwY/UiVmmbbzz9Up8=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
|
@ -175,11 +175,12 @@ github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK
|
|||
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
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.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA=
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/acmez v0.1.1 h1:KQODCqk+hBn3O7qfCRPj6L96uG65T5BSS95FKNEqtdA=
|
||||
github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
|
@ -228,6 +229,7 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
|
|||
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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
|
@ -244,8 +246,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
|||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8=
|
||||
github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ=
|
||||
github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
|
@ -270,10 +272,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.9.5 h1:+fHvqLencVdv14B+zgxQGhetF9qXl/nRTN/1mcyQwpM=
|
||||
github.com/tdewolff/minify/v2 v2.9.5/go.mod h1:jshtBj/uUJH6JX1fuxTLnnHOA1RVJhF5MM+leJzDKb4=
|
||||
github.com/tdewolff/parse/v2 v2.5.3 h1:fnPIstKgEfxd3+wwHnH73sAYydsR0o/jYhcQ6c5PkrA=
|
||||
github.com/tdewolff/parse/v2 v2.5.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/minify/v2 v2.9.7 h1:r8ewdcX8VYUoNj+s9WSy4FtNNNqNPevWOkb/MksAtzQ=
|
||||
github.com/tdewolff/minify/v2 v2.9.7/go.mod h1:AcJ/ggtHex5N/QiafLI8rlIO3qwSlgbPNLi27VZSYz8=
|
||||
github.com/tdewolff/parse/v2 v2.5.4 h1:ggaQ1SVE8wErRrZwUs49I6iQ1zL/tFlb7KtYsk2I8Yk=
|
||||
github.com/tdewolff/parse/v2 v2.5.4/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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
@ -316,8 +318,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -362,8 +364,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7
|
|||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 h1:lleNcKRbcaC8MqgLwghIkzZ2JBQAb7QQ9MiwRt1BisA=
|
||||
golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -374,6 +376,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200930132711-30421366ff76 h1:JnxiSYT3Nm0BT2a8CyvYyM6cnrWpidecD1UuSYbhKm0=
|
||||
golang.org/x/sync v0.0.0-20200930132711-30421366ff76/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -393,8 +397,9 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf h1:Bg47KQy0JhTHuf4sLiQwTMKwUMfSDwgSGatrxGR7nLM=
|
||||
golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
|
@ -426,8 +431,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200924224222-8d73f17870ce h1:XRr763sMfaUSNR4EsxbddvVEqYFa9picrx6ks9pJkKw=
|
||||
golang.org/x/tools v0.0.0-20200924224222-8d73f17870ce/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201005185003-576e169c3de7 h1:YTAUHYgZh/ZOA35/OrjTDmFFKb6ddkBL1Zgtl9r8Di8=
|
||||
golang.org/x/tools v0.0.0-20201005185003-576e169c3de7/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
|
@ -463,8 +468,8 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
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.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10=
|
||||
gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
109
http.go
109
http.go
|
@ -11,8 +11,12 @@ import (
|
|||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
const contentTypeHTML = "text/html; charset=utf-8"
|
||||
const contentTypeJSON = "application/json; charset=utf-8"
|
||||
const contentType = "Content-Type"
|
||||
const contentTypeHTMLUTF8 = "text/html; charset=utf-8"
|
||||
const contentTypeJSONUTF8 = "application/json; charset=utf-8"
|
||||
const contentTypeJSON = "application/json"
|
||||
const contentTypeWWWForm = "application/x-www-form-urlencoded"
|
||||
const contentTypeMultipartForm = "multipart/form-data"
|
||||
|
||||
var d *dynamicHandler
|
||||
|
||||
|
@ -24,10 +28,10 @@ func startServer() (err error) {
|
|||
}
|
||||
d.swapHandler(h)
|
||||
localAddress := ":" + strconv.Itoa(appConfig.Server.Port)
|
||||
if appConfig.Server.PublicHttps {
|
||||
if appConfig.Server.PublicHTTPS {
|
||||
initPublicHTTPS()
|
||||
err = certmagic.HTTPS([]string{appConfig.Server.Domain}, d)
|
||||
} else if appConfig.Server.LocalHttps {
|
||||
} else if appConfig.Server.LocalHTTPS {
|
||||
err = http.ListenAndServeTLS(localAddress, "https/server.crt", "https/server.key", d)
|
||||
} else {
|
||||
err = http.ListenAndServe(localAddress, d)
|
||||
|
@ -70,6 +74,13 @@ func buildHandler() (http.Handler, error) {
|
|||
apiRouter.Post("/hugo", apiPostCreateHugo)
|
||||
})
|
||||
|
||||
// Micropub
|
||||
if appConfig.Micropub.Enabled {
|
||||
r.Get(appConfig.Micropub.Path, serveMicropubQuery)
|
||||
r.With(checkIndieAuth).Post(appConfig.Micropub.Path, serveMicropubPost)
|
||||
r.With(checkIndieAuth).Post(appConfig.Micropub.Path+micropubMediaSubPath, serveMicropubMedia)
|
||||
}
|
||||
|
||||
// Posts
|
||||
allPostPaths, err := allPostPaths()
|
||||
if err != nil {
|
||||
|
@ -104,56 +115,56 @@ func buildHandler() (http.Handler, error) {
|
|||
jsonPath := ".json"
|
||||
atomPath := ".atom"
|
||||
|
||||
// Indexes, Feeds
|
||||
for _, section := range appConfig.Blog.Sections {
|
||||
if section.Name != "" {
|
||||
path := "/" + section.Name
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path, serveSection(path, section, NONE))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveSection(path, section, RSS))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveSection(path, section, JSON))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveSection(path, section, ATOM))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveSection(path, section, NONE))
|
||||
}
|
||||
}
|
||||
for blog, blogConfig := range appConfig.Blogs {
|
||||
|
||||
for _, taxonomy := range appConfig.Blog.Taxonomies {
|
||||
if taxonomy.Name != "" {
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get("/"+taxonomy.Name, serveTaxonomy(taxonomy))
|
||||
values, err := allTaxonomyValues(taxonomy.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tv := range values {
|
||||
path := "/" + taxonomy.Name + "/" + urlize(tv)
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomyValue(path, taxonomy, tv, NONE))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveTaxonomyValue(path, taxonomy, tv, RSS))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveTaxonomyValue(path, taxonomy, tv, JSON))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveTaxonomyValue(path, taxonomy, tv, ATOM))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveTaxonomyValue(path, taxonomy, tv, NONE))
|
||||
blogPath := blogConfig.Path
|
||||
if blogPath == "/" {
|
||||
blogPath = ""
|
||||
}
|
||||
|
||||
// Indexes, Feeds
|
||||
for _, section := range blogConfig.Sections {
|
||||
if section.Name != "" {
|
||||
path := blogPath + "/" + section.Name
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path, serveSection(blog, path, section, noFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveSection(blog, path, section, rssFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveSection(blog, path, section, jsonFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveSection(blog, path, section, atomFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveSection(blog, path, section, noFeed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if appConfig.Blog.Photos.Enabled {
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(appConfig.Blog.Photos.Path, servePhotos(appConfig.Blog.Photos.Path))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(appConfig.Blog.Photos.Path+paginationPath, servePhotos(appConfig.Blog.Photos.Path))
|
||||
}
|
||||
for _, taxonomy := range blogConfig.Taxonomies {
|
||||
if taxonomy.Name != "" {
|
||||
path := blogPath + "/" + taxonomy.Name
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomy(blog, taxonomy))
|
||||
values, err := allTaxonomyValues(blog, taxonomy.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tv := range values {
|
||||
path = path + "/" + urlize(tv)
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path, serveTaxonomyValue(blog, path, taxonomy, tv, noFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+rssPath, serveTaxonomyValue(blog, path, taxonomy, tv, rssFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+jsonPath, serveTaxonomyValue(blog, path, taxonomy, tv, jsonFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+atomPath, serveTaxonomyValue(blog, path, taxonomy, tv, atomFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(path+paginationPath, serveTaxonomyValue(blog, path, taxonomy, tv, noFeed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blog
|
||||
rootPath := "/"
|
||||
blogPath := "/blog"
|
||||
if !r.Match(chi.NewRouteContext(), http.MethodGet, rootPath) {
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(rootPath, serveHome("", NONE))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(rootPath+rssPath, serveHome("", RSS))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(rootPath+jsonPath, serveHome("", JSON))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(rootPath+atomPath, serveHome("", ATOM))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(paginationPath, serveHome("", NONE))
|
||||
} else if !r.Match(chi.NewRouteContext(), http.MethodGet, blogPath) {
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath, serveHome(blogPath, NONE))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+rssPath, serveHome(blogPath, RSS))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+jsonPath, serveHome(blogPath, JSON))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+atomPath, serveHome(blogPath, ATOM))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+paginationPath, serveHome(blogPath, NONE))
|
||||
// Photos
|
||||
if blogConfig.Photos.Enabled {
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+blogConfig.Photos.Path, servePhotos(blog))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogPath+blogConfig.Photos.Path+paginationPath, servePhotos(blog))
|
||||
}
|
||||
|
||||
// Blog
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path, serveHome(blog, blogPath, noFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+rssPath, serveHome(blog, blogPath, rssFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+jsonPath, serveHome(blog, blogPath, jsonFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+atomPath, serveHome(blog, blogPath, atomFeed))
|
||||
r.With(cacheMiddleware, minifier.Middleware).Get(blogConfig.Path+paginationPath, serveHome(blog, blogPath, noFeed))
|
||||
}
|
||||
|
||||
// Sitemap
|
||||
|
|
7
hugo.go
7
hugo.go
|
@ -2,14 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jeremywohl/flatten"
|
||||
"github.com/spf13/cast"
|
||||
"gopkg.in/yaml.v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseHugoFile(fileContent string, path string) (*Post, error) {
|
||||
// TODO: Add option to set blog, slug
|
||||
if path == "" {
|
||||
return nil, errors.New("empty path")
|
||||
}
|
||||
|
@ -57,6 +59,7 @@ func parseHugoFile(fileContent string, path string) (*Post, error) {
|
|||
}
|
||||
}
|
||||
// Create redirects
|
||||
// TODO: Move redirect creation after post creation
|
||||
var aliases []string
|
||||
for fk, value := range flat {
|
||||
if strings.HasPrefix(fk, "aliases") {
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type indieAuthTokenResponse struct {
|
||||
Me string `json:"me"`
|
||||
ClientID string `json:"client_id"`
|
||||
Scope string `json:"scope"`
|
||||
IssuedBy string `json:"issued_by"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func checkIndieAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
bearerToken := r.Header.Get("Authorization")
|
||||
if len(bearerToken) == 0 {
|
||||
if accessToken := r.URL.Query().Get("access_token"); len(accessToken) > 0 {
|
||||
bearerToken = "Bearer " + accessToken
|
||||
}
|
||||
}
|
||||
tokenResponse, err := verifyIndieAuthAccessToken(bearerToken)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if tokenResponse.StatusCode != http.StatusOK {
|
||||
http.Error(w, "Failed to retrieve authentication information", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
authorized := false
|
||||
for _, allowed := range appConfig.Micropub.AuthAllowed {
|
||||
if err := compareHostnames(tokenResponse.Me, allowed); err == nil {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authorized {
|
||||
http.Error(w, "Forbidden", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func verifyIndieAuthAccessToken(bearerToken string) (*indieAuthTokenResponse, error) {
|
||||
if len(bearerToken) == 0 {
|
||||
return nil, errors.New("no token")
|
||||
}
|
||||
req, err := http.NewRequest("GET", appConfig.Micropub.TokenEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add(contentType, contentTypeWWWForm)
|
||||
req.Header.Add("Authorization", bearerToken)
|
||||
req.Header.Add("Accept", contentTypeJSON)
|
||||
c := http.Client{
|
||||
Timeout: time.Duration(10 * time.Second),
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokenRes := indieAuthTokenResponse{StatusCode: resp.StatusCode}
|
||||
err = json.NewDecoder(resp.Body).Decode(&tokenRes)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tokenRes, nil
|
||||
}
|
||||
|
||||
func compareHostnames(a string, allowed string) error {
|
||||
h1, err := url.Parse(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ToLower(h1.Hostname()) != strings.ToLower(allowed) {
|
||||
return fmt.Errorf("hostnames do not match, %s is not %s", h1, allowed)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const micropubMediaSubPath = "/media"
|
||||
|
||||
type micropubConfig struct {
|
||||
MediaEndpoint string `json:"media-endpoint,omitempty"`
|
||||
}
|
||||
|
||||
func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||
if q := r.URL.Query().Get("q"); q == "config" {
|
||||
w.Header().Add(contentType, contentTypeJSON)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(µpubConfig{
|
||||
// TODO: Uncomment when media endpoint implemented
|
||||
// MediaEndpoint: appConfig.Server.PublicAddress + micropubMediaPath,
|
||||
})
|
||||
} else {
|
||||
w.Header().Add(contentType, contentTypeJSON)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("{}"))
|
||||
}
|
||||
}
|
||||
|
||||
func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
var post *Post
|
||||
if ct := r.Header.Get(contentType); strings.Contains(ct, contentTypeWWWForm) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
post, err = convertMPValueMapToPost(r.Form)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else if strings.Contains(ct, contentTypeMultipartForm) {
|
||||
err := r.ParseMultipartForm(1024 * 1024 * 16)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
post, err = convertMPValueMapToPost(r.MultipartForm.Value)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else if strings.Contains(ct, contentTypeJSON) {
|
||||
parsedMfItem := µformatItem{}
|
||||
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
post, err = convertMPMfToPost(parsedMfItem)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "wrong content type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := post.createOrReplace(true)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Add("Location", appConfig.Server.PublicAddress+post.Path)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
|
||||
func convertMPValueMapToPost(values map[string][]string) (*Post, error) {
|
||||
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
|
||||
return nil, errors.New("only entry type is supported so far")
|
||||
}
|
||||
entry := &Post{
|
||||
Parameters: map[string][]string{},
|
||||
}
|
||||
if content, ok := values["content"]; ok {
|
||||
entry.Content = content[0]
|
||||
}
|
||||
// Parameter
|
||||
if name, ok := values["name"]; ok {
|
||||
entry.Parameters["title"] = name
|
||||
}
|
||||
if category, ok := values["category"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.CategoryParam] = category
|
||||
} else if categories, ok := values["category[]"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.CategoryParam] = categories
|
||||
}
|
||||
if inReplyTo, ok := values["in-reply-to"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.ReplyParam] = inReplyTo
|
||||
}
|
||||
if likeOf, ok := values["like-of"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.LikeParam] = likeOf
|
||||
}
|
||||
if bookmarkOf, ok := values["bookmark-of"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.BookmarkParam] = bookmarkOf
|
||||
}
|
||||
if audio, ok := values["audio"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.AudioParam] = audio
|
||||
} else if audio, ok := values["audio[]"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.AudioParam] = audio
|
||||
}
|
||||
if photo, ok := values["photo"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.PhotoParam] = photo
|
||||
} else if photos, ok := values["photo[]"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.PhotoParam] = photos
|
||||
}
|
||||
if photoAlt, ok := values["mp-photo-alt"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = photoAlt
|
||||
} else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
|
||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = photoAlts
|
||||
}
|
||||
if slug, ok := values["mp-slug"]; ok {
|
||||
entry.Slug = slug[0]
|
||||
}
|
||||
err := computeExtraPostParameters(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
type microformatItem struct {
|
||||
Type []string `json:"type"`
|
||||
Properties *microformatProperties `json:"properties"`
|
||||
}
|
||||
|
||||
type microformatProperties struct {
|
||||
Name []string `json:"name"`
|
||||
Published []string `json:"published"`
|
||||
Updated []string `json:"updated"`
|
||||
Category []string `json:"category"`
|
||||
Content []string `json:"content"`
|
||||
URL []string `json:"url"`
|
||||
InReplyTo []string `json:"in-reply-to"`
|
||||
LikeOf []string `json:"like-of"`
|
||||
BookmarkOf []string `json:"bookmark-of"`
|
||||
MpSlug []string `json:"mp-slug"`
|
||||
Photo []interface{} `json:"photo"`
|
||||
Audio []string `json:"audio"`
|
||||
}
|
||||
|
||||
func convertMPMfToPost(mf *microformatItem) (*Post, error) {
|
||||
if len(mf.Type) != 1 || mf.Type[0] != "h-entry" {
|
||||
return nil, errors.New("only entry type is supported so far")
|
||||
}
|
||||
entry := &Post{}
|
||||
// Content
|
||||
if mf.Properties != nil && len(mf.Properties.Content) == 1 && len(mf.Properties.Content[0]) > 0 {
|
||||
entry.Content = mf.Properties.Content[0]
|
||||
}
|
||||
// Parameter
|
||||
if len(mf.Properties.Name) == 1 {
|
||||
entry.Parameters["title"] = mf.Properties.Name
|
||||
}
|
||||
if len(mf.Properties.Category) > 0 {
|
||||
entry.Parameters[appConfig.Micropub.CategoryParam] = mf.Properties.Category
|
||||
}
|
||||
if len(mf.Properties.InReplyTo) == 1 {
|
||||
entry.Parameters[appConfig.Micropub.ReplyParam] = mf.Properties.InReplyTo
|
||||
}
|
||||
if len(mf.Properties.LikeOf) == 1 {
|
||||
entry.Parameters[appConfig.Micropub.LikeParam] = mf.Properties.LikeOf
|
||||
}
|
||||
if len(mf.Properties.BookmarkOf) == 1 {
|
||||
entry.Parameters[appConfig.Micropub.BookmarkParam] = mf.Properties.BookmarkOf
|
||||
}
|
||||
if len(mf.Properties.Audio) > 0 {
|
||||
entry.Parameters[appConfig.Micropub.AudioParam] = mf.Properties.Audio
|
||||
}
|
||||
if len(mf.Properties.Photo) > 0 {
|
||||
for _, photo := range mf.Properties.Photo {
|
||||
if theString, justString := photo.(string); justString {
|
||||
entry.Parameters[appConfig.Micropub.PhotoParam] = append(entry.Parameters[appConfig.Micropub.PhotoParam], theString)
|
||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = append(entry.Parameters[appConfig.Micropub.PhotoDescriptionParam], "")
|
||||
} else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
|
||||
entry.Parameters[appConfig.Micropub.PhotoParam] = append(entry.Parameters[appConfig.Micropub.PhotoParam], cast.ToString(thePhoto["value"]))
|
||||
entry.Parameters[appConfig.Micropub.PhotoDescriptionParam] = append(entry.Parameters[appConfig.Micropub.PhotoDescriptionParam], cast.ToString(thePhoto["alt"]))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(mf.Properties.MpSlug) == 1 {
|
||||
entry.Slug = mf.Properties.MpSlug[0]
|
||||
}
|
||||
err := computeExtraPostParameters(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entry, nil
|
||||
|
||||
}
|
||||
|
||||
func computeExtraPostParameters(entry *Post) error {
|
||||
// Add images not in content
|
||||
images := entry.Parameters[appConfig.Micropub.PhotoParam]
|
||||
imageAlts := entry.Parameters[appConfig.Micropub.PhotoDescriptionParam]
|
||||
useAlts := len(images) == len(imageAlts)
|
||||
for i, image := range images {
|
||||
if !strings.Contains(entry.Content, image) {
|
||||
if useAlts && len(imageAlts[i]) > 0 {
|
||||
entry.Content += "\n\n![" + imageAlts[i] + "](" + image + " \"" + imageAlts[i] + "\")"
|
||||
} else {
|
||||
entry.Content += "\n\n![](" + image + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
sep := "---\n"
|
||||
if split := strings.Split(entry.Content, sep); len(split) > 2 {
|
||||
// Contains frontmatter
|
||||
fm := split[1]
|
||||
meta := map[string]interface{}{}
|
||||
err := yaml.Unmarshal([]byte(fm), &meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Find section and copy frontmatter to params
|
||||
for key, value := range meta {
|
||||
if a, ok := value.([]interface{}); ok {
|
||||
for _, ae := range a {
|
||||
entry.Parameters[key] = append(entry.Parameters[key], cast.ToString(ae))
|
||||
}
|
||||
} else {
|
||||
entry.Parameters[key] = append(entry.Parameters[key], cast.ToString(value))
|
||||
}
|
||||
}
|
||||
// Remove frontmatter from content
|
||||
entry.Content = strings.Replace(entry.Content, split[0]+sep+split[1]+sep, "", 1)
|
||||
}
|
||||
// Check settings
|
||||
if blog := entry.Parameters["blog"]; len(blog) == 1 && blog[0] != "" {
|
||||
entry.Blog = blog[0]
|
||||
delete(entry.Parameters, "blog")
|
||||
} else {
|
||||
entry.Blog = appConfig.DefaultBlog
|
||||
}
|
||||
if path := entry.Parameters["path"]; len(path) == 1 && path[0] != "" {
|
||||
entry.Path = path[0]
|
||||
delete(entry.Parameters, "path")
|
||||
}
|
||||
if section := entry.Parameters["section"]; len(section) == 1 && section[0] != "" {
|
||||
entry.Section = section[0]
|
||||
delete(entry.Parameters, "section")
|
||||
}
|
||||
if slug := entry.Parameters["slug"]; len(slug) == 1 && slug[0] != "" {
|
||||
entry.Slug = slug[0]
|
||||
delete(entry.Parameters, "slug")
|
||||
}
|
||||
if entry.Path == "" && entry.Section == "" {
|
||||
entry.Section = appConfig.Blogs[entry.Blog].DefaultSection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Implement media server
|
||||
}
|
65
posts.go
65
posts.go
|
@ -5,12 +5,13 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/vcraescu/go-paginator"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/vcraescu/go-paginator"
|
||||
)
|
||||
|
||||
var errPostNotFound = errors.New("post not found")
|
||||
|
@ -21,6 +22,10 @@ type Post struct {
|
|||
Published string `json:"published"`
|
||||
Updated string `json:"updated"`
|
||||
Parameters map[string][]string `json:"parameters"`
|
||||
Blog string `json:"blog"`
|
||||
Section string `json:"section"`
|
||||
// Not persisted
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
func servePost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -41,6 +46,7 @@ func servePost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
type indexTemplateData struct {
|
||||
Blog string
|
||||
Title string
|
||||
Description string
|
||||
Posts []*Post
|
||||
|
@ -78,24 +84,26 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro
|
|||
return err
|
||||
}
|
||||
|
||||
func serveHome(path string, ft feedType) func(w http.ResponseWriter, r *http.Request) {
|
||||
func serveHome(blog string, path string, ft feedType) func(w http.ResponseWriter, r *http.Request) {
|
||||
return serveIndex(&indexConfig{
|
||||
blog: blog,
|
||||
path: path,
|
||||
feed: ft,
|
||||
})
|
||||
}
|
||||
|
||||
func serveSection(path string, section *section, ft feedType) func(w http.ResponseWriter, r *http.Request) {
|
||||
func serveSection(blog string, path string, section *section, ft feedType) func(w http.ResponseWriter, r *http.Request) {
|
||||
return serveIndex(&indexConfig{
|
||||
blog: blog,
|
||||
path: path,
|
||||
section: section,
|
||||
feed: ft,
|
||||
})
|
||||
}
|
||||
|
||||
func serveTaxonomy(tax *taxonomy) func(w http.ResponseWriter, r *http.Request) {
|
||||
func serveTaxonomy(blog string, tax *taxonomy) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
allValues, err := allTaxonomyValues(tax.Name)
|
||||
allValues, err := allTaxonomyValues(blog, tax.Name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -103,15 +111,18 @@ func serveTaxonomy(tax *taxonomy) func(w http.ResponseWriter, r *http.Request) {
|
|||
render(w, templateTaxonomy, struct {
|
||||
Taxonomy *taxonomy
|
||||
TaxonomyValues []string
|
||||
Blog string
|
||||
}{
|
||||
Taxonomy: tax,
|
||||
TaxonomyValues: allValues,
|
||||
Blog: blog,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serveTaxonomyValue(path string, tax *taxonomy, value string, ft feedType) func(w http.ResponseWriter, r *http.Request) {
|
||||
func serveTaxonomyValue(blog string, path string, tax *taxonomy, value string, ft feedType) func(w http.ResponseWriter, r *http.Request) {
|
||||
return serveIndex(&indexConfig{
|
||||
blog: blog,
|
||||
path: path,
|
||||
tax: tax,
|
||||
taxValue: value,
|
||||
|
@ -119,15 +130,17 @@ func serveTaxonomyValue(path string, tax *taxonomy, value string, ft feedType) f
|
|||
})
|
||||
}
|
||||
|
||||
func servePhotos(path string) func(w http.ResponseWriter, r *http.Request) {
|
||||
func servePhotos(blog string) func(w http.ResponseWriter, r *http.Request) {
|
||||
return serveIndex(&indexConfig{
|
||||
path: path,
|
||||
onlyWithParameter: appConfig.Blog.Photos.Parameter,
|
||||
blog: blog,
|
||||
path: appConfig.Blogs[blog].Photos.Path,
|
||||
onlyWithParameter: appConfig.Blogs[blog].Photos.Path,
|
||||
template: templatePhotos,
|
||||
})
|
||||
}
|
||||
|
||||
type indexConfig struct {
|
||||
blog string
|
||||
path string
|
||||
section *section
|
||||
tax *taxonomy
|
||||
|
@ -141,16 +154,21 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
pageNoString := chi.URLParam(r, "page")
|
||||
pageNo, _ := strconv.Atoi(pageNoString)
|
||||
sections := appConfig.Blog.Sections
|
||||
var sections []string
|
||||
if ic.section != nil {
|
||||
sections = []*section{ic.section}
|
||||
sections = []string{ic.section.Name}
|
||||
} else {
|
||||
for sectionKey := range appConfig.Blogs[ic.blog].Sections {
|
||||
sections = append(sections, sectionKey)
|
||||
}
|
||||
}
|
||||
p := paginator.New(&postPaginationAdapter{context: r.Context(), config: &postsRequestConfig{
|
||||
blog: ic.blog,
|
||||
sections: sections,
|
||||
taxonomy: ic.tax,
|
||||
taxonomyValue: ic.taxValue,
|
||||
onlyWithParameter: ic.onlyWithParameter,
|
||||
}}, appConfig.Blog.Pagination)
|
||||
}}, appConfig.Blogs[ic.blog].Pagination)
|
||||
p.SetPage(pageNo)
|
||||
var posts []*Post
|
||||
err := p.Results(&posts)
|
||||
|
@ -167,8 +185,8 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) {
|
|||
description = ic.section.Description
|
||||
}
|
||||
// Check if feed
|
||||
if ic.feed != NONE {
|
||||
generateFeed(ic.feed, w, r, posts, title, description)
|
||||
if ic.feed != noFeed {
|
||||
generateFeed(ic.blog, ic.feed, w, r, posts, title, description)
|
||||
return
|
||||
}
|
||||
// Navigation
|
||||
|
@ -185,6 +203,7 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) {
|
|||
template = templateIndex
|
||||
}
|
||||
render(w, template, &indexTemplateData{
|
||||
Blog: ic.blog,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Posts: posts,
|
||||
|
@ -208,10 +227,11 @@ func getPost(context context.Context, path string) (*Post, error) {
|
|||
}
|
||||
|
||||
type postsRequestConfig struct {
|
||||
blog string
|
||||
path string
|
||||
limit int
|
||||
offset int
|
||||
sections []*section
|
||||
sections []string
|
||||
taxonomy *taxonomy
|
||||
taxonomyValue string
|
||||
onlyWithParameter string
|
||||
|
@ -220,8 +240,11 @@ type postsRequestConfig struct {
|
|||
func getPosts(context context.Context, config *postsRequestConfig) (posts []*Post, err error) {
|
||||
paths := make(map[string]int)
|
||||
var rows *sql.Rows
|
||||
defaultSelection := "select p.path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(parameter, ''), coalesce(value, '') "
|
||||
defaultSelection := "select p.path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(blog, ''), coalesce(section, ''), coalesce(parameter, ''), coalesce(value, '') "
|
||||
postsTable := "posts"
|
||||
if config.blog != "" {
|
||||
postsTable = "(select * from " + postsTable + " where blog = '" + config.blog + "')"
|
||||
}
|
||||
if config.onlyWithParameter != "" {
|
||||
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = '" + config.onlyWithParameter + "' and length(coalesce(pp.value, '')) > 1)"
|
||||
}
|
||||
|
@ -234,7 +257,7 @@ func getPosts(context context.Context, config *postsRequestConfig) (posts []*Pos
|
|||
if i > 0 {
|
||||
postsTable += " or"
|
||||
}
|
||||
postsTable += " section='" + section.Name + "'"
|
||||
postsTable += " section='" + section + "'"
|
||||
}
|
||||
postsTable += ")"
|
||||
}
|
||||
|
@ -259,7 +282,7 @@ func getPosts(context context.Context, config *postsRequestConfig) (posts []*Pos
|
|||
for rows.Next() {
|
||||
post := &Post{}
|
||||
var parameterName, parameterValue string
|
||||
err = rows.Scan(&post.Path, &post.Content, &post.Published, &post.Updated, ¶meterName, ¶meterValue)
|
||||
err = rows.Scan(&post.Path, &post.Content, &post.Published, &post.Updated, &post.Blog, &post.Section, ¶meterName, ¶meterValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -295,9 +318,9 @@ func allPostPaths() ([]string, error) {
|
|||
return postPaths, nil
|
||||
}
|
||||
|
||||
func allTaxonomyValues(taxonomy string) ([]string, error) {
|
||||
func allTaxonomyValues(blog string, 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)
|
||||
rows, err := appDb.Query("select distinct pp.value from posts p left outer join post_parameters pp on p.path = pp.path where pp.parameter = ? and length(coalesce(pp.value, '')) > 1 and blog = ?", taxonomy, blog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
83
postsDb.go
83
postsDb.go
|
@ -1,21 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/araddon/dateparse"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/araddon/dateparse"
|
||||
)
|
||||
|
||||
func (p *Post) checkPost() error {
|
||||
func (p *Post) checkPost(new bool) error {
|
||||
if p == nil {
|
||||
return errors.New("no post")
|
||||
}
|
||||
if p.Path == "" || !strings.HasPrefix(p.Path, "/") {
|
||||
return errors.New("wrong path")
|
||||
}
|
||||
// Fix path
|
||||
p.Path = strings.TrimSuffix(p.Path, "/")
|
||||
now := time.Now()
|
||||
// Fix content
|
||||
p.Content = strings.TrimSuffix(strings.TrimPrefix(p.Content, "\n"), "\n")
|
||||
// Fix date strings
|
||||
|
@ -26,6 +27,9 @@ func (p *Post) checkPost() error {
|
|||
}
|
||||
p.Published = d.String()
|
||||
}
|
||||
if p.Published == "" {
|
||||
p.Published = now.String()
|
||||
}
|
||||
if p.Updated != "" {
|
||||
d, err := dateparse.ParseIn(p.Updated, time.Local)
|
||||
if err != nil {
|
||||
|
@ -33,11 +37,63 @@ func (p *Post) checkPost() error {
|
|||
}
|
||||
p.Updated = d.String()
|
||||
}
|
||||
// Check blog
|
||||
if p.Blog == "" {
|
||||
p.Blog = appConfig.DefaultBlog
|
||||
}
|
||||
// Check path
|
||||
p.Path = strings.TrimSuffix(p.Path, "/")
|
||||
if p.Path == "" {
|
||||
if p.Section == "" {
|
||||
p.Section = appConfig.Blogs[p.Blog].DefaultSection
|
||||
}
|
||||
if p.Slug == "" {
|
||||
random := generateRandomString(now, 5)
|
||||
p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
|
||||
}
|
||||
pathVars := struct {
|
||||
BlogPath string
|
||||
Year int
|
||||
Month int
|
||||
Slug string
|
||||
Section string
|
||||
}{
|
||||
BlogPath: appConfig.Blogs[p.Blog].Path,
|
||||
Year: now.Year(),
|
||||
Month: int(now.Month()),
|
||||
Slug: p.Slug,
|
||||
Section: p.Section,
|
||||
}
|
||||
pathTmplString := appConfig.Blogs[p.Blog].Sections[p.Section].PathTemplate
|
||||
if pathTmplString == "" {
|
||||
return errors.New("path template empty")
|
||||
}
|
||||
pathTmpl, err := template.New("location").Parse(pathTmplString)
|
||||
if err != nil {
|
||||
return errors.New("failed to parse location template")
|
||||
}
|
||||
var pathBuffer bytes.Buffer
|
||||
err = pathTmpl.Execute(&pathBuffer, pathVars)
|
||||
if err != nil {
|
||||
return errors.New("failed to execute location template")
|
||||
}
|
||||
p.Path = pathBuffer.String()
|
||||
}
|
||||
if p.Path != "" && !strings.HasPrefix(p.Path, "/") {
|
||||
return errors.New("wrong path")
|
||||
}
|
||||
// Check if post with path already exists
|
||||
if new {
|
||||
post, _ := getPost(context.Background(), p.Path)
|
||||
if post != nil {
|
||||
return errors.New("path already exists")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Post) createOrReplace() error {
|
||||
err := p.checkPost()
|
||||
func (p *Post) createOrReplace(new bool) error {
|
||||
err := p.checkPost(new)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -47,7 +103,7 @@ func (p *Post) createOrReplace() error {
|
|||
finishWritingToDb()
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec("insert or replace into posts (path, content, published, updated) values (?, ?, ?, ?)", p.Path, p.Content, p.Published, p.Updated)
|
||||
_, err = tx.Exec("insert or replace into posts (path, content, published, updated, blog, section) values (?, ?, ?, ?, ?, ?)", p.Path, p.Content, p.Published, p.Updated, p.Blog, p.Section)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
finishWritingToDb()
|
||||
|
@ -77,12 +133,13 @@ func (p *Post) createOrReplace() error {
|
|||
return err
|
||||
}
|
||||
finishWritingToDb()
|
||||
go purgeCache(p.Path)
|
||||
go purgeCache()
|
||||
return reloadRouter()
|
||||
}
|
||||
|
||||
func (p *Post) delete() error {
|
||||
err := p.checkPost()
|
||||
// TODO
|
||||
err := p.checkPost(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -110,6 +167,6 @@ func (p *Post) delete() error {
|
|||
return err
|
||||
}
|
||||
finishWritingToDb()
|
||||
go purgeCache(p.Path)
|
||||
go purgeCache()
|
||||
return reloadRouter()
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ func serveRedirect(w http.ResponseWriter, r *http.Request) {
|
|||
// Send redirect
|
||||
w.Header().Set("Location", redirect)
|
||||
render(w, templateRedirect, struct {
|
||||
Blog string
|
||||
Permalink string
|
||||
}{
|
||||
Blog: appConfig.DefaultBlog,
|
||||
Permalink: redirect,
|
||||
})
|
||||
w.WriteHeader(http.StatusFound)
|
||||
|
|
19
render.go
19
render.go
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/araddon/dateparse"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -11,6 +10,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/araddon/dateparse"
|
||||
)
|
||||
|
||||
const templatesDir = "templates"
|
||||
|
@ -29,11 +30,17 @@ var templateFunctions template.FuncMap
|
|||
|
||||
func initRendering() error {
|
||||
templateFunctions = template.FuncMap{
|
||||
"blog": func() *configBlog {
|
||||
return appConfig.Blog
|
||||
"blogs": func() map[string]*configBlog {
|
||||
return appConfig.Blogs
|
||||
},
|
||||
"menu": func(id string) *menu {
|
||||
return appConfig.Blog.Menus[id]
|
||||
"blog": func(blog string) *configBlog {
|
||||
return appConfig.Blogs[blog]
|
||||
},
|
||||
"micropub": func() *configMicropub {
|
||||
return appConfig.Micropub
|
||||
},
|
||||
"menu": func(blog, id string) *menu {
|
||||
return appConfig.Blogs[blog].Menus[id]
|
||||
},
|
||||
"md": func(content string) template.HTML {
|
||||
htmlContent, err := renderMarkdown(content)
|
||||
|
@ -104,7 +111,7 @@ func render(w http.ResponseWriter, template string, data interface{}) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
// Set content type (needed for minification middleware
|
||||
w.Header().Set("Content-Type", contentTypeHTML)
|
||||
w.Header().Set(contentType, contentTypeHTMLUTF8)
|
||||
// Write buffered response
|
||||
_, _ = w.Write(buffer.Bytes())
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
{{ define "base" }}
|
||||
<!doctype html>
|
||||
<html lang={{ blog.Lang }}>
|
||||
<html lang={{ (blog .Blog).Lang }}>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv=x-ua-compatible content="IE=edge">
|
||||
<link rel="stylesheet" href="{{ asset "css/styles.css" }}">
|
||||
{{ template "title" . }}
|
||||
{{ if micropub.Enabled }}
|
||||
{{ include "micropub" . }}
|
||||
{{ end }}
|
||||
{{ include "header" . }}
|
||||
{{ template "main" . }}
|
||||
{{ end }}
|
|
@ -1,5 +1,5 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ with .Title }}{{ . }} - {{end}}{{ blog.Title }}</title>
|
||||
<title>{{ with .Title }}{{ . }}{{end}}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{{ define "header" }}
|
||||
<header>
|
||||
<h1><a href="/" rel="home" title="{{ blog.Title }}">{{ blog.Title }}</a></h1>
|
||||
{{ with blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
|
||||
{{ $blog := (blog .Blog) }}
|
||||
<h1><a href="/" rel="home" title="{{ $blog.Title }}">{{ $blog.Title }}</a></h1>
|
||||
{{ with $blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
|
||||
{{ include "menu" . }}
|
||||
</header>
|
||||
{{ end }}
|
|
@ -1,8 +1,8 @@
|
|||
{{ define "title" }}
|
||||
{{ if .Title }}
|
||||
<title>{{ .Title }} - {{ blog.Title }}</title>
|
||||
<title>{{ .Title }} - {{ (blog .Blog).Title }}</title>
|
||||
{{ else }}
|
||||
<title>{{ blog.Title }}</title>
|
||||
<title>{{ (blog .Blog).Title }}</title>
|
||||
{{ end }}
|
||||
<link rel="alternate"
|
||||
type="application/rss+xml"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{ define "menu" }}
|
||||
<nav>
|
||||
{{ with menu "main" }}
|
||||
{{ with menu .Blog "main" }}
|
||||
{{ $first := true }}
|
||||
{{ range $i, $item := .Items }}
|
||||
{{ if ne $first true }} • {{ end }}<a
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{{ define "micropub" }}
|
||||
{{ with micropub.Path }}
|
||||
<link rel="micropub" href="{{ . }}" />
|
||||
{{ end }}
|
||||
{{ with micropub.AuthEndpoint }}
|
||||
<link rel="authorization_endpoint" href="{{ . }}" />
|
||||
{{ end }}
|
||||
{{ with micropub.TokenEndpoint }}
|
||||
<link rel="token_endpoint" href="{{ . }}" />
|
||||
{{ end }}
|
||||
{{ with micropub.Authn }}
|
||||
<link href="{{ . }}" rel="me authn">
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -1,16 +1,18 @@
|
|||
{{ define "title" }}
|
||||
{{ if blog.Photos.Title }}
|
||||
<title>{{ blog.Photos.Title }} - {{ blog.Title }}</title>
|
||||
{{ $blog := (blog .Blog) }}
|
||||
{{ if $blog.Photos.Title }}
|
||||
<title>{{ $blog.Photos.Title }} - {{ $blog.Title }}</title>
|
||||
{{ else }}
|
||||
<title>{{ blog.Title }}</title>
|
||||
<title>{{ $blog.Title }}</title>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main>
|
||||
{{ with blog.Photos.Title }}<h1>{{ . }}</h1>{{ end }}
|
||||
{{ with blog.Photos.Description }}{{ md . }}{{ end }}
|
||||
{{ if (or blog.Photos.Title blog.Photos.Description) }}
|
||||
{{ $blog := (blog .Blog) }}
|
||||
{{ with $blog.Photos.Title }}<h1>{{ . }}</h1>{{ end }}
|
||||
{{ with $blog.Photos.Description }}{{ md . }}{{ end }}
|
||||
{{ if (or $blog.Photos.Title $blog.Photos.Description) }}
|
||||
<hr>
|
||||
{{ end }}
|
||||
{{ range $i, $post := .Posts }}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<article>
|
||||
{{ with p . "title" }}<h2>{{ . }}</h2>{{ end }}
|
||||
{{ with .Published }}<p>{{ dateformat . "02. Jan 2006" }}</p>{{ end }}
|
||||
{{ range $i, $photo := ( ps . blog.Photos.Parameter ) }}
|
||||
{{ range $i, $photo := ( ps . (blog .Blog).Photos.Parameter ) }}
|
||||
{{ md ( printf "![](%s)" $photo ) }}
|
||||
{{ end }}
|
||||
<p>{{ summary . }}</p>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ with p . "title" }}{{ . }} - {{end}}{{ blog.Title }}</title>
|
||||
<title>{{ with p . "title" }}{{ . }} - {{end}}{{ (blog .Blog).Title }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
|
@ -14,7 +14,7 @@
|
|||
<div class=e-content>{{ md . }}</div>
|
||||
{{ end }}
|
||||
</article>
|
||||
{{ $taxonomies := blog.Taxonomies }}
|
||||
{{ $taxonomies := (blog .Blog).Taxonomies }}
|
||||
{{ $post := . }}
|
||||
{{ range $i, $tax := $taxonomies }}
|
||||
{{ $tvs := ps $post $tax.Name }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{ define "redirect" }}
|
||||
<!doctype html>
|
||||
<html lang={{ blog.Lang }}>
|
||||
<html lang={{ (blog .Blog).Lang }}>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="robots" content="noindex">
|
||||
<title>{{ .Permalink }}</title>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ .Taxonomy.Title }} - {{ blog.Title }}</title>
|
||||
<title>{{ .Taxonomy.Title }} - {{ (blog .Blog).Title }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
|
|
14
utils.go
14
utils.go
|
@ -1,8 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func urlize(str string) string {
|
||||
|
@ -20,7 +22,7 @@ func urlize(str string) string {
|
|||
func firstSentences(value string, count int) string {
|
||||
for i := range value {
|
||||
if value[i] == '.' || value[i] == '!' || value[i] == '?' {
|
||||
count -= 1
|
||||
count--
|
||||
if count == 0 && i < len(value) {
|
||||
return value[0 : i+1]
|
||||
}
|
||||
|
@ -35,3 +37,13 @@ func sortedStrings(s []string) []string {
|
|||
})
|
||||
return s
|
||||
}
|
||||
|
||||
func generateRandomString(now time.Time, n int) string {
|
||||
rand.Seed(now.UnixNano())
|
||||
letters := []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue