mirror of
https://github.com/jlelse/GoBlog
synced 2024-06-18 04:25:00 +00:00
Micropub get, update, delete (with scopes), and some other fixes
This commit is contained in:
parent
e611a5008d
commit
9e97ec3a2b
58
api.go
58
api.go
|
@ -6,25 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiPostCreate(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer func() {
|
|
||||||
_ = r.Body.Close()
|
|
||||||
}()
|
|
||||||
post := &Post{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(post)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = post.createOrReplace(false)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Location", appConfig.Server.PublicAddress+post.Path)
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) {
|
func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) {
|
||||||
blog := r.URL.Query().Get("blog")
|
blog := r.URL.Query().Get("blog")
|
||||||
path := r.URL.Query().Get("path")
|
path := r.URL.Query().Get("path")
|
||||||
|
@ -49,7 +30,7 @@ func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) {
|
||||||
post.Section = section
|
post.Section = section
|
||||||
post.Slug = slug
|
post.Slug = slug
|
||||||
aliases = append(aliases, alias)
|
aliases = append(aliases, alias)
|
||||||
err = post.createOrReplace(false)
|
err = post.replace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -67,40 +48,3 @@ func apiPostCreateHugo(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Location", appConfig.Server.PublicAddress+post.Path)
|
w.Header().Set("Location", appConfig.Server.PublicAddress+post.Path)
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiPostRead(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Query().Get("path")
|
|
||||||
if path == "" {
|
|
||||||
http.Error(w, "No path defined", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
post, err := getPost(r.Context(), path)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set(contentType, contentTypeJSONUTF8)
|
|
||||||
err = json.NewEncoder(w).Encode(post)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiPostDelete(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer func() {
|
|
||||||
_ = r.Body.Close()
|
|
||||||
}()
|
|
||||||
post := &Post{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(post)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = post.delete()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
26
config.go
26
config.go
|
@ -117,16 +117,13 @@ type frontmatter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type configMicropub struct {
|
type configMicropub struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
CategoryParam string `mapstructure:"categoryParam"`
|
||||||
Path string `mapstructure:"path"`
|
ReplyParam string `mapstructure:"replyParam"`
|
||||||
AuthAllowed []string `mapstructure:"authAllowed"`
|
LikeParam string `mapstructure:"likeParam"`
|
||||||
CategoryParam string `mapstructure:"categoryParam"`
|
BookmarkParam string `mapstructure:"bookmarkParam"`
|
||||||
ReplyParam string `mapstructure:"replyParam"`
|
AudioParam string `mapstructure:"audioParam"`
|
||||||
LikeParam string `mapstructure:"likeParam"`
|
PhotoParam string `mapstructure:"photoParam"`
|
||||||
BookmarkParam string `mapstructure:"bookmarkParam"`
|
PhotoDescriptionParam string `mapstructure:"photoDescriptionParam"`
|
||||||
AudioParam string `mapstructure:"audioParam"`
|
|
||||||
PhotoParam string `mapstructure:"photoParam"`
|
|
||||||
PhotoDescriptionParam string `mapstructure:"photoDescriptionParam"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var appConfig = &config{}
|
var appConfig = &config{}
|
||||||
|
@ -138,6 +135,7 @@ func initConfig() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Defaults
|
||||||
viper.SetDefault("server.logging", false)
|
viper.SetDefault("server.logging", false)
|
||||||
viper.SetDefault("server.debug", false)
|
viper.SetDefault("server.debug", false)
|
||||||
viper.SetDefault("server.port", 8080)
|
viper.SetDefault("server.port", 8080)
|
||||||
|
@ -152,8 +150,6 @@ func initConfig() error {
|
||||||
viper.SetDefault("user.password", "secret")
|
viper.SetDefault("user.password", "secret")
|
||||||
viper.SetDefault("hooks.shell", "/bin/bash")
|
viper.SetDefault("hooks.shell", "/bin/bash")
|
||||||
viper.SetDefault("hugo.frontmatter", []*frontmatter{{Meta: "title", Parameter: "title"}, {Meta: "tags", Parameter: "tags"}})
|
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.categoryParam", "tags")
|
viper.SetDefault("micropub.categoryParam", "tags")
|
||||||
viper.SetDefault("micropub.replyParam", "replylink")
|
viper.SetDefault("micropub.replyParam", "replylink")
|
||||||
viper.SetDefault("micropub.likeParam", "likelink")
|
viper.SetDefault("micropub.likeParam", "likelink")
|
||||||
|
@ -167,14 +163,14 @@ func initConfig() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Check config
|
// Check config
|
||||||
|
if appConfig.Server.Domain == "" {
|
||||||
|
return errors.New("no domain configured")
|
||||||
|
}
|
||||||
if len(appConfig.Blogs) == 0 {
|
if len(appConfig.Blogs) == 0 {
|
||||||
return errors.New("no blog configured")
|
return errors.New("no blog configured")
|
||||||
}
|
}
|
||||||
if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil {
|
if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil {
|
||||||
return errors.New("no default blog or default blog not present")
|
return errors.New("no default blog or default blog not present")
|
||||||
}
|
}
|
||||||
if len(appConfig.Micropub.AuthAllowed) == 0 {
|
|
||||||
appConfig.Micropub.AuthAllowed = []string{appConfig.Server.Domain}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,10 +54,6 @@ hugo:
|
||||||
- meta: tags
|
- meta: tags
|
||||||
parameter: tags
|
parameter: tags
|
||||||
micropub:
|
micropub:
|
||||||
enabled: true
|
|
||||||
path: /micropub
|
|
||||||
authAllowed:
|
|
||||||
- example.com
|
|
||||||
categoryParam: tags
|
categoryParam: tags
|
||||||
replyParam: replylink
|
replyParam: replylink
|
||||||
likeParam: likelink
|
likeParam: likelink
|
||||||
|
|
17
http.go
17
http.go
|
@ -76,22 +76,21 @@ func buildHandler() (http.Handler, error) {
|
||||||
|
|
||||||
// API
|
// API
|
||||||
r.Route("/api", func(apiRouter chi.Router) {
|
r.Route("/api", func(apiRouter chi.Router) {
|
||||||
apiRouter.Use(authMiddleware)
|
apiRouter.Use(middleware.NoCache, authMiddleware)
|
||||||
apiRouter.Post("/post", apiPostCreate)
|
|
||||||
apiRouter.Get("/post", apiPostRead)
|
|
||||||
apiRouter.Delete("/post", apiPostDelete)
|
|
||||||
apiRouter.Post("/hugo", apiPostCreateHugo)
|
apiRouter.Post("/hugo", apiPostCreateHugo)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Micropub
|
// Micropub
|
||||||
if appConfig.Micropub.Enabled {
|
r.Route(micropubPath, func(mpRouter chi.Router) {
|
||||||
r.Get(appConfig.Micropub.Path, serveMicropubQuery)
|
mpRouter.Use(middleware.NoCache, checkIndieAuth)
|
||||||
r.With(checkIndieAuth).Post(appConfig.Micropub.Path, serveMicropubPost)
|
mpRouter.Get("/", serveMicropubQuery)
|
||||||
r.With(checkIndieAuth).Post(appConfig.Micropub.Path+micropubMediaSubPath, serveMicropubMedia)
|
mpRouter.Post("/", serveMicropubPost)
|
||||||
}
|
mpRouter.Post(micropubMediaSubPath, serveMicropubMedia)
|
||||||
|
})
|
||||||
|
|
||||||
// IndieAuth
|
// IndieAuth
|
||||||
r.Route("/indieauth", func(indieauthRouter chi.Router) {
|
r.Route("/indieauth", func(indieauthRouter chi.Router) {
|
||||||
|
indieauthRouter.Use(middleware.NoCache)
|
||||||
indieauthRouter.With(authMiddleware).Get("/", indieAuthAuth)
|
indieauthRouter.With(authMiddleware).Get("/", indieAuthAuth)
|
||||||
indieauthRouter.With(authMiddleware).Post("/accept", indieAuthAccept)
|
indieauthRouter.With(authMiddleware).Post("/accept", indieAuthAccept)
|
||||||
indieauthRouter.Post("/", indieAuthAuth)
|
indieauthRouter.Post("/", indieAuthAuth)
|
||||||
|
|
13
indieauth.go
13
indieauth.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -18,18 +19,12 @@ func checkIndieAuth(next http.Handler) http.Handler {
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
authorized := false
|
if err := compareHostnames(tokenData.Me, appConfig.Server.Domain); err != nil {
|
||||||
for _, allowed := range appConfig.Micropub.AuthAllowed {
|
|
||||||
if err := compareHostnames(tokenData.Me, allowed); err == nil {
|
|
||||||
authorized = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !authorized {
|
|
||||||
http.Error(w, "Forbidden", http.StatusUnauthorized)
|
http.Error(w, "Forbidden", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
ctx := context.WithValue(r.Context(), "scope", strings.Join(tokenData.Scopes, " "))
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
378
micropub.go
378
micropub.go
|
@ -2,13 +2,19 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const micropubPath = "/micropub"
|
||||||
const micropubMediaSubPath = "/media"
|
const micropubMediaSubPath = "/media"
|
||||||
|
|
||||||
type micropubConfig struct {
|
type micropubConfig struct {
|
||||||
|
@ -16,27 +22,91 @@ type micropubConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
if q := r.URL.Query().Get("q"); q == "config" {
|
switch r.URL.Query().Get("q") {
|
||||||
|
case "config":
|
||||||
w.Header().Add(contentType, contentTypeJSON)
|
w.Header().Add(contentType, contentTypeJSON)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_ = json.NewEncoder(w).Encode(µpubConfig{
|
_ = json.NewEncoder(w).Encode(µpubConfig{
|
||||||
// TODO: Uncomment when media endpoint implemented
|
// TODO: Uncomment when media endpoint implemented
|
||||||
// MediaEndpoint: appConfig.Server.PublicAddress + micropubMediaPath,
|
// MediaEndpoint: appConfig.Server.PublicAddress + micropubMediaPath,
|
||||||
})
|
})
|
||||||
} else {
|
case "source":
|
||||||
|
var mf interface{}
|
||||||
|
if urlString := r.URL.Query().Get("url"); urlString != "" {
|
||||||
|
u, err := url.Parse(r.URL.Query().Get("url"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
post, err := getPost(r.Context(), u.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mf = post.toMfItem()
|
||||||
|
} else {
|
||||||
|
posts, err := getPosts(r.Context(), &postsRequestConfig{})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list := map[string][]*microformatItem{}
|
||||||
|
for _, post := range posts {
|
||||||
|
list["items"] = append(list["items"], post.toMfItem())
|
||||||
|
}
|
||||||
|
mf = list
|
||||||
|
}
|
||||||
w.Header().Add(contentType, contentTypeJSON)
|
w.Header().Add(contentType, contentTypeJSON)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write([]byte("{}"))
|
_ = json.NewEncoder(w).Encode(mf)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post *Post) toMfItem() *microformatItem {
|
||||||
|
params := post.Parameters
|
||||||
|
params["path"] = []string{post.Path}
|
||||||
|
params["section"] = []string{post.Section}
|
||||||
|
params["blog"] = []string{post.Blog}
|
||||||
|
pb, _ := yaml.Marshal(post.Parameters)
|
||||||
|
content := fmt.Sprintf("---\n%s---\n%s", string(pb), post.Content)
|
||||||
|
return µformatItem{
|
||||||
|
Type: []string{"h-entry"},
|
||||||
|
Properties: µformatProperties{
|
||||||
|
Name: post.Parameters["title"],
|
||||||
|
Published: []string{post.Published},
|
||||||
|
Updated: []string{post.Updated},
|
||||||
|
Content: []string{content},
|
||||||
|
MpSlug: []string{post.Slug},
|
||||||
|
Category: post.Parameters[appConfig.Micropub.CategoryParam],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
var post *Post
|
var post *Post
|
||||||
if ct := r.Header.Get(contentType); strings.Contains(ct, contentTypeWWWForm) {
|
if ct := r.Header.Get(contentType); strings.Contains(ct, contentTypeWWWForm) || strings.Contains(ct, contentTypeMultipartForm) {
|
||||||
err := r.ParseForm()
|
var err error
|
||||||
if err != nil {
|
r.ParseForm()
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
if strings.Contains(ct, contentTypeMultipartForm) {
|
||||||
|
err := r.ParseMultipartForm(0)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if action := micropubAction(r.Form.Get("action")); action != "" {
|
||||||
|
u, err := url.Parse(r.Form.Get("url"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
if action == actionDelete {
|
||||||
|
micropubDelete(w, r, u)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Action not supported", http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post, err = convertMPValueMapToPost(r.Form)
|
post, err = convertMPValueMapToPost(r.Form)
|
||||||
|
@ -44,17 +114,6 @@ func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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) {
|
} else if strings.Contains(ct, contentTypeJSON) {
|
||||||
parsedMfItem := µformatItem{}
|
parsedMfItem := µformatItem{}
|
||||||
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
err := json.NewDecoder(r.Body).Decode(parsedMfItem)
|
||||||
|
@ -62,6 +121,22 @@ func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if parsedMfItem.Action != "" {
|
||||||
|
u, err := url.Parse(parsedMfItem.URL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
if parsedMfItem.Action == actionDelete {
|
||||||
|
micropubDelete(w, r, u)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if parsedMfItem.Action == actionUpdate {
|
||||||
|
micropubUpdate(w, r, u, parsedMfItem)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Action not supported", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
post, err = convertMPMfToPost(parsedMfItem)
|
post, err = convertMPMfToPost(parsedMfItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -71,7 +146,10 @@ func serveMicropubPost(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "wrong content type", http.StatusBadRequest)
|
http.Error(w, "wrong content type", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := post.createOrReplace(true)
|
if !strings.Contains(r.Context().Value("scope").(string), "create") {
|
||||||
|
http.Error(w, "create scope missing", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
err := post.create()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -133,31 +211,43 @@ func convertMPValueMapToPost(values map[string][]string) (*Post, error) {
|
||||||
if slug, ok := values["mp-slug"]; ok {
|
if slug, ok := values["mp-slug"]; ok {
|
||||||
entry.Slug = slug[0]
|
entry.Slug = slug[0]
|
||||||
}
|
}
|
||||||
err := computeExtraPostParameters(entry)
|
err := entry.computeExtraPostParameters()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type micropubAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
actionUpdate micropubAction = "update"
|
||||||
|
actionDelete = "delete"
|
||||||
|
)
|
||||||
|
|
||||||
type microformatItem struct {
|
type microformatItem struct {
|
||||||
Type []string `json:"type"`
|
Type []string `json:"type,omitempty"`
|
||||||
Properties *microformatProperties `json:"properties"`
|
URL string `json:"url,omitempty"`
|
||||||
|
Action micropubAction `json:"action,omitempty"`
|
||||||
|
Properties *microformatProperties `json:"properties,omitempty"`
|
||||||
|
Replace map[string][]interface{} `json:"replace,omitempty"`
|
||||||
|
Add map[string][]interface{} `json:"add,omitempty"`
|
||||||
|
Delete interface{} `json:"delete,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type microformatProperties struct {
|
type microformatProperties struct {
|
||||||
Name []string `json:"name"`
|
Name []string `json:"name,omitempty"`
|
||||||
Published []string `json:"published"`
|
Published []string `json:"published,omitempty"`
|
||||||
Updated []string `json:"updated"`
|
Updated []string `json:"updated,omitempty"`
|
||||||
Category []string `json:"category"`
|
Category []string `json:"category,omitempty"`
|
||||||
Content []string `json:"content"`
|
Content []string `json:"content,omitempty"`
|
||||||
URL []string `json:"url"`
|
URL []string `json:"url,omitempty"`
|
||||||
InReplyTo []string `json:"in-reply-to"`
|
InReplyTo []string `json:"in-reply-to,omitempty"`
|
||||||
LikeOf []string `json:"like-of"`
|
LikeOf []string `json:"like-of,omitempty"`
|
||||||
BookmarkOf []string `json:"bookmark-of"`
|
BookmarkOf []string `json:"bookmark-of,omitempty"`
|
||||||
MpSlug []string `json:"mp-slug"`
|
MpSlug []string `json:"mp-slug,omitempty"`
|
||||||
Photo []interface{} `json:"photo"`
|
Photo []interface{} `json:"photo,omitempty"`
|
||||||
Audio []string `json:"audio"`
|
Audio []string `json:"audio,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertMPMfToPost(mf *microformatItem) (*Post, error) {
|
func convertMPMfToPost(mf *microformatItem) (*Post, error) {
|
||||||
|
@ -208,7 +298,7 @@ func convertMPMfToPost(mf *microformatItem) (*Post, error) {
|
||||||
if len(mf.Properties.MpSlug) == 1 {
|
if len(mf.Properties.MpSlug) == 1 {
|
||||||
entry.Slug = mf.Properties.MpSlug[0]
|
entry.Slug = mf.Properties.MpSlug[0]
|
||||||
}
|
}
|
||||||
err := computeExtraPostParameters(entry)
|
err := entry.computeExtraPostParameters()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -216,22 +306,9 @@ func convertMPMfToPost(mf *microformatItem) (*Post, error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeExtraPostParameters(entry *Post) error {
|
func (post *Post) computeExtraPostParameters() error {
|
||||||
// Add images not in content
|
post.Content = regexp.MustCompile("\r\n").ReplaceAllString(post.Content, "\n")
|
||||||
images := entry.Parameters[appConfig.Micropub.PhotoParam]
|
if split := strings.Split(post.Content, "---\n"); len(split) >= 3 && len(strings.TrimSpace(split[0])) == 0 {
|
||||||
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
|
// Contains frontmatter
|
||||||
fm := split[1]
|
fm := split[1]
|
||||||
meta := map[string]interface{}{}
|
meta := map[string]interface{}{}
|
||||||
|
@ -241,42 +318,205 @@ func computeExtraPostParameters(entry *Post) error {
|
||||||
}
|
}
|
||||||
// Find section and copy frontmatter to params
|
// Find section and copy frontmatter to params
|
||||||
for key, value := range meta {
|
for key, value := range meta {
|
||||||
|
// Delete existing content - replace
|
||||||
|
post.Parameters[key] = []string{}
|
||||||
if a, ok := value.([]interface{}); ok {
|
if a, ok := value.([]interface{}); ok {
|
||||||
for _, ae := range a {
|
for _, ae := range a {
|
||||||
entry.Parameters[key] = append(entry.Parameters[key], cast.ToString(ae))
|
post.Parameters[key] = append(post.Parameters[key], cast.ToString(ae))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.Parameters[key] = append(entry.Parameters[key], cast.ToString(value))
|
post.Parameters[key] = append(post.Parameters[key], cast.ToString(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remove frontmatter from content
|
// Remove frontmatter from content
|
||||||
entry.Content = strings.Replace(entry.Content, split[0]+sep+split[1]+sep, "", 1)
|
post.Content = strings.Join(split[2:], "---\n")
|
||||||
}
|
}
|
||||||
// Check settings
|
// Check settings
|
||||||
if blog := entry.Parameters["blog"]; len(blog) == 1 && blog[0] != "" {
|
if blog := post.Parameters["blog"]; len(blog) == 1 && blog[0] != "" {
|
||||||
entry.Blog = blog[0]
|
post.Blog = blog[0]
|
||||||
delete(entry.Parameters, "blog")
|
delete(post.Parameters, "blog")
|
||||||
} else {
|
} else {
|
||||||
entry.Blog = appConfig.DefaultBlog
|
post.Blog = appConfig.DefaultBlog
|
||||||
}
|
}
|
||||||
if path := entry.Parameters["path"]; len(path) == 1 && path[0] != "" {
|
if path := post.Parameters["path"]; len(path) == 1 && path[0] != "" {
|
||||||
entry.Path = path[0]
|
post.Path = path[0]
|
||||||
delete(entry.Parameters, "path")
|
delete(post.Parameters, "path")
|
||||||
}
|
}
|
||||||
if section := entry.Parameters["section"]; len(section) == 1 && section[0] != "" {
|
if section := post.Parameters["section"]; len(section) == 1 && section[0] != "" {
|
||||||
entry.Section = section[0]
|
post.Section = section[0]
|
||||||
delete(entry.Parameters, "section")
|
delete(post.Parameters, "section")
|
||||||
}
|
}
|
||||||
if slug := entry.Parameters["slug"]; len(slug) == 1 && slug[0] != "" {
|
if slug := post.Parameters["slug"]; len(slug) == 1 && slug[0] != "" {
|
||||||
entry.Slug = slug[0]
|
post.Slug = slug[0]
|
||||||
delete(entry.Parameters, "slug")
|
delete(post.Parameters, "slug")
|
||||||
}
|
}
|
||||||
if entry.Path == "" && entry.Section == "" {
|
if post.Path == "" && post.Section == "" {
|
||||||
entry.Section = appConfig.Blogs[entry.Blog].DefaultSection
|
// Has no path or section -> default section
|
||||||
|
post.Section = appConfig.Blogs[post.Blog].DefaultSection
|
||||||
|
}
|
||||||
|
if post.Published == "" && post.Section != "" {
|
||||||
|
// Has no published date, but section -> published now
|
||||||
|
post.Published = time.Now().String()
|
||||||
|
}
|
||||||
|
// Add images not in content
|
||||||
|
images := post.Parameters[appConfig.Micropub.PhotoParam]
|
||||||
|
imageAlts := post.Parameters[appConfig.Micropub.PhotoDescriptionParam]
|
||||||
|
useAlts := len(images) == len(imageAlts)
|
||||||
|
for i, image := range images {
|
||||||
|
if !strings.Contains(post.Content, image) {
|
||||||
|
if useAlts && len(imageAlts[i]) > 0 {
|
||||||
|
post.Content += "\n\n![" + imageAlts[i] + "](" + image + " \"" + imageAlts[i] + "\")"
|
||||||
|
} else {
|
||||||
|
post.Content += "\n\n![](" + image + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func micropubDelete(w http.ResponseWriter, r *http.Request, u *url.URL) {
|
||||||
|
if !strings.Contains(r.Context().Value("scope").(string), "delete") {
|
||||||
|
http.Error(w, "delete scope missing", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
err := deletePost(u.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *microformatItem) {
|
||||||
|
if !strings.Contains(r.Context().Value("scope").(string), "update") {
|
||||||
|
http.Error(w, "update scope missing", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
post, err := getPost(r.Context(), u.Path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if mf.Replace != nil {
|
||||||
|
for key, value := range mf.Replace {
|
||||||
|
switch key {
|
||||||
|
case "content":
|
||||||
|
post.Content = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
|
case "published":
|
||||||
|
post.Published = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
|
case "updated":
|
||||||
|
post.Updated = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
|
case "name":
|
||||||
|
post.Parameters["title"] = cast.ToStringSlice(value)
|
||||||
|
case "category":
|
||||||
|
post.Parameters[appConfig.Micropub.CategoryParam] = cast.ToStringSlice(value)
|
||||||
|
case "in-reply-to":
|
||||||
|
post.Parameters[appConfig.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
||||||
|
case "like-of":
|
||||||
|
post.Parameters[appConfig.Micropub.LikeParam] = cast.ToStringSlice(value)
|
||||||
|
case "bookmark-of":
|
||||||
|
post.Parameters[appConfig.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
||||||
|
case "audio":
|
||||||
|
post.Parameters[appConfig.Micropub.AudioParam] = cast.ToStringSlice(value)
|
||||||
|
// TODO: photo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mf.Add != nil {
|
||||||
|
for key, value := range mf.Add {
|
||||||
|
switch key {
|
||||||
|
case "content":
|
||||||
|
post.Content += strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
|
case "published":
|
||||||
|
post.Published = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
|
case "updated":
|
||||||
|
post.Updated = strings.TrimSpace(strings.Join(cast.ToStringSlice(value), " "))
|
||||||
|
case "category":
|
||||||
|
category := post.Parameters[appConfig.Micropub.CategoryParam]
|
||||||
|
if category == nil {
|
||||||
|
category = []string{}
|
||||||
|
}
|
||||||
|
post.Parameters[appConfig.Micropub.CategoryParam] = append(category, cast.ToStringSlice(value)...)
|
||||||
|
case "in-reply-to":
|
||||||
|
post.Parameters[appConfig.Micropub.ReplyParam] = cast.ToStringSlice(value)
|
||||||
|
case "like-of":
|
||||||
|
post.Parameters[appConfig.Micropub.LikeParam] = cast.ToStringSlice(value)
|
||||||
|
case "bookmark-of":
|
||||||
|
post.Parameters[appConfig.Micropub.BookmarkParam] = cast.ToStringSlice(value)
|
||||||
|
case "audio":
|
||||||
|
audio := post.Parameters[appConfig.Micropub.CategoryParam]
|
||||||
|
if audio == nil {
|
||||||
|
audio = []string{}
|
||||||
|
}
|
||||||
|
post.Parameters[appConfig.Micropub.AudioParam] = append(audio, cast.ToStringSlice(value)...)
|
||||||
|
// TODO: photo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if del := mf.Delete; del != nil {
|
||||||
|
if reflect.TypeOf(del).Kind() == reflect.Slice {
|
||||||
|
toDelete, ok := del.([]interface{})
|
||||||
|
if ok {
|
||||||
|
for _, key := range toDelete {
|
||||||
|
switch key {
|
||||||
|
case "content":
|
||||||
|
post.Content = ""
|
||||||
|
case "published":
|
||||||
|
post.Published = ""
|
||||||
|
case "updated":
|
||||||
|
post.Updated = ""
|
||||||
|
case "category":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.CategoryParam)
|
||||||
|
case "in-reply-to":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.ReplyParam)
|
||||||
|
case "like-of":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.LikeParam)
|
||||||
|
case "bookmark-of":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.BookmarkParam)
|
||||||
|
case "audio":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.AudioParam)
|
||||||
|
case "photo":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.PhotoParam)
|
||||||
|
delete(post.Parameters, appConfig.Micropub.PhotoDescriptionParam)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toDelete, ok := del.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
for key := range toDelete {
|
||||||
|
if ok {
|
||||||
|
switch key {
|
||||||
|
case "content":
|
||||||
|
post.Content = ""
|
||||||
|
case "published":
|
||||||
|
post.Published = ""
|
||||||
|
case "updated":
|
||||||
|
post.Updated = ""
|
||||||
|
case "in-reply-to":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.ReplyParam)
|
||||||
|
case "like-of":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.LikeParam)
|
||||||
|
case "bookmark-of":
|
||||||
|
delete(post.Parameters, appConfig.Micropub.BookmarkParam)
|
||||||
|
// Use content to edit other parameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = post.computeExtraPostParameters()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = post.replace()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: Implement media server
|
// TODO: Implement media server
|
||||||
}
|
}
|
||||||
|
|
41
postsDb.go
41
postsDb.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -12,7 +11,7 @@ import (
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Post) checkPost(new bool) error {
|
func (p *Post) checkPost() error {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return errors.New("no post")
|
return errors.New("no post")
|
||||||
}
|
}
|
||||||
|
@ -27,9 +26,6 @@ func (p *Post) checkPost(new bool) error {
|
||||||
}
|
}
|
||||||
p.Published = d.String()
|
p.Published = d.String()
|
||||||
}
|
}
|
||||||
if p.Published == "" {
|
|
||||||
p.Published = now.String()
|
|
||||||
}
|
|
||||||
if p.Updated != "" {
|
if p.Updated != "" {
|
||||||
d, err := dateparse.ParseIn(p.Updated, time.Local)
|
d, err := dateparse.ParseIn(p.Updated, time.Local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -85,18 +81,19 @@ func (p *Post) checkPost(new bool) error {
|
||||||
if p.Path != "" && !strings.HasPrefix(p.Path, "/") {
|
if p.Path != "" && !strings.HasPrefix(p.Path, "/") {
|
||||||
return errors.New("wrong 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Post) create() error {
|
||||||
|
return p.createOrReplace(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) replace() error {
|
||||||
|
return p.createOrReplace(false)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Post) createOrReplace(new bool) error {
|
func (p *Post) createOrReplace(new bool) error {
|
||||||
err := p.checkPost(new)
|
err := p.checkPost()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -106,7 +103,11 @@ func (p *Post) createOrReplace(new bool) error {
|
||||||
finishWritingToDb()
|
finishWritingToDb()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, 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)
|
sqlCommand := "insert"
|
||||||
|
if !new {
|
||||||
|
sqlCommand = "insert or replace"
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(sqlCommand+" into posts (path, content, published, updated, blog, section) values (?, ?, ?, ?, ?, ?)", p.Path, p.Content, p.Published, p.Updated, p.Blog, p.Section)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
finishWritingToDb()
|
finishWritingToDb()
|
||||||
|
@ -140,11 +141,9 @@ func (p *Post) createOrReplace(new bool) error {
|
||||||
return reloadRouter()
|
return reloadRouter()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Post) delete() error {
|
func deletePost(path string) error {
|
||||||
// TODO
|
if path == "" {
|
||||||
err := p.checkPost(false)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
startWritingToDb()
|
startWritingToDb()
|
||||||
tx, err := appDb.Begin()
|
tx, err := appDb.Begin()
|
||||||
|
@ -152,13 +151,13 @@ func (p *Post) delete() error {
|
||||||
finishWritingToDb()
|
finishWritingToDb()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = tx.Exec("delete from posts where path=?", p.Path)
|
_, err = tx.Exec("delete from posts where path=?", path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
finishWritingToDb()
|
finishWritingToDb()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = tx.Exec("delete from post_parameters where path=?", p.Path)
|
_, err = tx.Exec("delete from post_parameters where path=?", path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
finishWritingToDb()
|
finishWritingToDb()
|
||||||
|
|
12
render.go
12
render.go
|
@ -33,9 +33,6 @@ var templateFunctions template.FuncMap
|
||||||
|
|
||||||
func initRendering() error {
|
func initRendering() error {
|
||||||
templateFunctions = template.FuncMap{
|
templateFunctions = template.FuncMap{
|
||||||
"micropub": func() *configMicropub {
|
|
||||||
return appConfig.Micropub
|
|
||||||
},
|
|
||||||
"menu": func(blog *configBlog, id string) *menu {
|
"menu": func(blog *configBlog, id string) *menu {
|
||||||
return blog.Menus[id]
|
return blog.Menus[id]
|
||||||
},
|
},
|
||||||
|
@ -94,19 +91,18 @@ func initRendering() error {
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
},
|
},
|
||||||
"jsonFile": func(filename string) *interface{} {
|
"jsonFile": func(filename string) *map[string]interface{} {
|
||||||
parsed := []*interface{}{}
|
parsed := &map[string]interface{}{}
|
||||||
content, err := ioutil.ReadFile(filename)
|
content, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
contentString := "[" + string(content) + "]"
|
err = json.Unmarshal(content, parsed)
|
||||||
err = json.Unmarshal([]byte(contentString), &parsed)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return parsed[0]
|
return parsed
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
<meta http-equiv=x-ua-compatible content="IE=edge">
|
<meta http-equiv=x-ua-compatible content="IE=edge">
|
||||||
<link rel="stylesheet" href="{{ asset "css/styles.css" }}">
|
<link rel="stylesheet" href="{{ asset "css/styles.css" }}">
|
||||||
{{ template "title" . }}
|
{{ template "title" . }}
|
||||||
{{ if micropub.Enabled }}
|
{{ include "micropub" .Blog .Data }}
|
||||||
{{ include "micropub" .Blog .Data }}
|
|
||||||
{{ end }}
|
|
||||||
{{ include "header" .Blog .Data }}
|
{{ include "header" .Blog .Data }}
|
||||||
{{ template "main" . }}
|
{{ template "main" . }}
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -1,7 +1,5 @@
|
||||||
{{ define "micropub" }}
|
{{ define "micropub" }}
|
||||||
{{ with micropub.Path }}
|
<link rel="micropub" href="/micropub" />
|
||||||
<link rel="micropub" href="{{ . }}" />
|
|
||||||
{{ end }}
|
|
||||||
<link rel="authorization_endpoint" href="/indieauth" />
|
<link rel="authorization_endpoint" href="/indieauth" />
|
||||||
<link rel="token_endpoint" href="/indieauth/token" />
|
<link rel="token_endpoint" href="/indieauth/token" />
|
||||||
{{ end }}
|
{{ end }}
|
Loading…
Reference in New Issue
Block a user