diff --git a/.gitignore b/.gitignore
index a919301..5f09b60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
/config.yaml
/data
/GoBlog
-/tmp_assets
\ No newline at end of file
+/tmp_assets
+/certmagic
\ No newline at end of file
diff --git a/activitystreams.go b/activitystreams.go
index 65031c5..21652a5 100644
--- a/activitystreams.go
+++ b/activitystreams.go
@@ -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)
}
diff --git a/api.go b/api.go
index c1e67f8..4fef4b4 100644
--- a/api.go
+++ b/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)
diff --git a/cache.go b/cache.go
index a3e4b92..26ca894 100644
--- a/cache.go
+++ b/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()
}
diff --git a/config.go b/config.go
index a72ded6..90f29ed 100644
--- a/config.go
+++ b/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
}
diff --git a/databaseMigrations.go b/databaseMigrations.go
new file mode 100644
index 0000000..2ef3860
--- /dev/null
+++ b/databaseMigrations.go
@@ -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
+}
diff --git a/database_migrations.go b/database_migrations.go
deleted file mode 100644
index a0cfa6f..0000000
--- a/database_migrations.go
+++ /dev/null
@@ -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
-}
diff --git a/errors.go b/errors.go
index 55a814c..ff8fec6 100644
--- a/errors.go
+++ b/errors.go
@@ -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),
})
diff --git a/example-config.yaml b/example-config.yaml
index 92d15b0..361a91b 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -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
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/feeds.go b/feeds.go
index 62dd878..27e28a4 100644
--- a/feeds.go
+++ b/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))
diff --git a/go.mod b/go.mod
index 6d63893..c32b498 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 85529ba..874329e 100644
--- a/go.sum
+++ b/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=
diff --git a/http.go b/http.go
index 6c0eb85..eeebbaf 100644
--- a/http.go
+++ b/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
diff --git a/hugo.go b/hugo.go
index f11b820..7317dbd 100644
--- a/hugo.go
+++ b/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") {
diff --git a/indieauth.go b/indieauth.go
new file mode 100644
index 0000000..f855c84
--- /dev/null
+++ b/indieauth.go
@@ -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
+}
diff --git a/micropub.go b/micropub.go
new file mode 100644
index 0000000..03d22c1
--- /dev/null
+++ b/micropub.go
@@ -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
+}
diff --git a/posts.go b/posts.go
index 62711c7..d0968e0 100644
--- a/posts.go
+++ b/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
}
diff --git a/postsDb.go b/postsDb.go
index dd13a27..a58ad29 100644
--- a/postsDb.go
+++ b/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()
}
diff --git a/redirects.go b/redirects.go
index 35b4283..68dcd1e 100644
--- a/redirects.go
+++ b/redirects.go
@@ -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)
diff --git a/render.go b/render.go
index fa2e80d..f54b631 100644
--- a/render.go
+++ b/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())
}
diff --git a/templates/base.gohtml b/templates/base.gohtml
index c2f845e..5dd60f9 100644
--- a/templates/base.gohtml
+++ b/templates/base.gohtml
@@ -1,11 +1,14 @@
{{ define "base" }}
-
+
{{ template "title" . }}
+ {{ if micropub.Enabled }}
+ {{ include "micropub" . }}
+ {{ end }}
{{ include "header" . }}
{{ template "main" . }}
{{ end }}
\ No newline at end of file
diff --git a/templates/error.gohtml b/templates/error.gohtml
index a655bf5..9f7698d 100644
--- a/templates/error.gohtml
+++ b/templates/error.gohtml
@@ -1,5 +1,5 @@
{{ define "title" }}
-
{{ with .Title }}{{ . }} - {{end}}{{ blog.Title }}
+ {{ with .Title }}{{ . }}{{end}}
{{ end }}
{{ define "main" }}
diff --git a/templates/header.gohtml b/templates/header.gohtml
index 1e355c4..24b3572 100644
--- a/templates/header.gohtml
+++ b/templates/header.gohtml
@@ -1,7 +1,8 @@
{{ define "header" }}
-
- {{ with blog.Description }}{{ . }}
{{ end }}
+ {{ $blog := (blog .Blog) }}
+
+ {{ with $blog.Description }}{{ . }}
{{ end }}
{{ include "menu" . }}
{{ end }}
\ No newline at end of file
diff --git a/templates/index.gohtml b/templates/index.gohtml
index 6462faf..da07c32 100644
--- a/templates/index.gohtml
+++ b/templates/index.gohtml
@@ -1,8 +1,8 @@
{{ define "title" }}
{{ if .Title }}
- {{ .Title }} - {{ blog.Title }}
+ {{ .Title }} - {{ (blog .Blog).Title }}
{{ else }}
- {{ blog.Title }}
+ {{ (blog .Blog).Title }}
{{ end }}
- {{ with menu "main" }}
+ {{ with menu .Blog "main" }}
{{ $first := true }}
{{ range $i, $item := .Items }}
{{ if ne $first true }} • {{ end }}
+ {{ end }}
+ {{ with micropub.AuthEndpoint }}
+
+ {{ end }}
+ {{ with micropub.TokenEndpoint }}
+
+ {{ end }}
+ {{ with micropub.Authn }}
+
+ {{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/photos.gohtml b/templates/photos.gohtml
index ea15a79..9f6d156 100644
--- a/templates/photos.gohtml
+++ b/templates/photos.gohtml
@@ -1,16 +1,18 @@
{{ define "title" }}
- {{ if blog.Photos.Title }}
- {{ blog.Photos.Title }} - {{ blog.Title }}
+ {{ $blog := (blog .Blog) }}
+ {{ if $blog.Photos.Title }}
+ {{ $blog.Photos.Title }} - {{ $blog.Title }}
{{ else }}
- {{ blog.Title }}
+ {{ $blog.Title }}
{{ end }}
{{ end }}
{{ define "main" }}
- {{ with blog.Photos.Title }}{{ . }}
{{ end }}
- {{ with blog.Photos.Description }}{{ md . }}{{ end }}
- {{ if (or blog.Photos.Title blog.Photos.Description) }}
+ {{ $blog := (blog .Blog) }}
+ {{ with $blog.Photos.Title }}{{ . }}
{{ end }}
+ {{ with $blog.Photos.Description }}{{ md . }}{{ end }}
+ {{ if (or $blog.Photos.Title $blog.Photos.Description) }}
{{ end }}
{{ range $i, $post := .Posts }}
diff --git a/templates/photosummary.gohtml b/templates/photosummary.gohtml
index 772a82c..dee12b4 100644
--- a/templates/photosummary.gohtml
+++ b/templates/photosummary.gohtml
@@ -2,7 +2,7 @@
{{ with p . "title" }}{{ . }}
{{ end }}
{{ with .Published }}{{ dateformat . "02. Jan 2006" }}
{{ end }}
- {{ range $i, $photo := ( ps . blog.Photos.Parameter ) }}
+ {{ range $i, $photo := ( ps . (blog .Blog).Photos.Parameter ) }}
{{ md ( printf "![](%s)" $photo ) }}
{{ end }}
{{ summary . }}
diff --git a/templates/post.gohtml b/templates/post.gohtml
index d43834c..5f2176c 100644
--- a/templates/post.gohtml
+++ b/templates/post.gohtml
@@ -1,5 +1,5 @@
{{ define "title" }}
- {{ with p . "title" }}{{ . }} - {{end}}{{ blog.Title }}
+ {{ with p . "title" }}{{ . }} - {{end}}{{ (blog .Blog).Title }}
{{ end }}
{{ define "main" }}
@@ -14,7 +14,7 @@
{{ md . }}
{{ end }}
- {{ $taxonomies := blog.Taxonomies }}
+ {{ $taxonomies := (blog .Blog).Taxonomies }}
{{ $post := . }}
{{ range $i, $tax := $taxonomies }}
{{ $tvs := ps $post $tax.Name }}
diff --git a/templates/redirect.gohtml b/templates/redirect.gohtml
index d5564dc..c8c7c39 100644
--- a/templates/redirect.gohtml
+++ b/templates/redirect.gohtml
@@ -1,6 +1,6 @@
{{ define "redirect" }}
-
+
{{ .Permalink }}
diff --git a/templates/taxonomy.gohtml b/templates/taxonomy.gohtml
index ea5d1eb..6c055e4 100644
--- a/templates/taxonomy.gohtml
+++ b/templates/taxonomy.gohtml
@@ -1,5 +1,5 @@
{{ define "title" }}
- {{ .Taxonomy.Title }} - {{ blog.Title }}
+ {{ .Taxonomy.Title }} - {{ (blog .Blog).Title }}
{{ end }}
{{ define "main" }}
diff --git a/utils.go b/utils.go
index b82620d..6eaee94 100644
--- a/utils.go
+++ b/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)
+}