A lot of progress

This commit is contained in:
Jan-Lukas Else 2020-10-06 19:07:48 +02:00
parent fcac755814
commit 95909420ba
32 changed files with 825 additions and 334 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
/config.yaml
/data
/GoBlog
/tmp_assets
/tmp_assets
/certmagic

View File

@ -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
View File

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

View File

@ -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
View File

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

36
databaseMigrations.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -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
View File

@ -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
View File

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

View File

@ -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") {

92
indieauth.go Normal file
View File

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

271
micropub.go Normal file
View File

@ -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(&micropubConfig{
// 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 := &microformatItem{}
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
}

View File

@ -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, &parameterName, &parameterValue)
err = rows.Scan(&post.Path, &post.Content, &post.Published, &post.Updated, &post.Blog, &post.Section, &parameterName, &parameterValue)
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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{{ define "title" }}
<title>{{ with .Title }}{{ . }} - {{end}}{{ blog.Title }}</title>
<title>{{ with .Title }}{{ . }}{{end}}</title>
{{ end }}
{{ define "main" }}

View File

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

View File

@ -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"

View File

@ -1,6 +1,6 @@
{{ define "menu" }}
<nav>
{{ with menu "main" }}
{{ with menu .Blog "main" }}
{{ $first := true }}
{{ range $i, $item := .Items }}
{{ if ne $first true }} &bull; {{ end }}<a

14
templates/micropub.gohtml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{{ define "title" }}
<title>{{ .Taxonomy.Title }} - {{ blog.Title }}</title>
<title>{{ .Taxonomy.Title }} - {{ (blog .Blog).Title }}</title>
{{ end }}
{{ define "main" }}

View File

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