diff --git a/README.md b/README.md
index 21a643d..6dd1f9e 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@ Prose is a blogging platform written in Go, which I am building to serve my own
Blog posts should be created in the format `title-slug.md`. Work in progress posts should be stored as `WIP-title-slug.md`. Static content should be stored in the `static/` folder, appropriately arranged.
-Posts will be served as `/title-slug`, and files like `static/random/file/structure.txt` will be served as `/random/file/structure.txt`. When title slugs and static files conflict, slugs will have higher precdence.
+Posts will be served as `/title-slug`, and files like `static/random/file/structure.txt` will be served as `/random/file/structure.txt`. When title slugs and static files conflict, slugs will have higher precdence. An RSS feed of the blog is available at `/rss.xml`.
To start the server:
go run .
-Server will be live on port 8080.
\ No newline at end of file
+Server will be live on port 8080.
diff --git a/server.go b/server.go
index d999c87..2df5270 100644
--- a/server.go
+++ b/server.go
@@ -7,6 +7,7 @@ import (
"net/http"
"strings"
"sync"
+ "time"
"github.com/aymerick/raymond"
"github.com/fogleman/gg"
@@ -51,7 +52,15 @@ func newServer() (*server, error) {
s.postList = posts
s.mu.Unlock()
- tpls, err := loadTemplates([]string{"page", "fullpost", "summary", "notfound", "error"})
+ tpls, err := loadTemplates([]string{
+ "page.html",
+ "fullpost.html",
+ "summary.html",
+ "notfound.html",
+ "error.html",
+ "rss-channel.xml",
+ "rss-item.xml",
+ })
if err != nil {
return nil, err
}
@@ -102,8 +111,8 @@ func (s *server) router(res http.ResponseWriter, req *http.Request) {
res = &errorCatcher{
res: res,
req: req,
- errorTpl: s.templates["error"],
- notFoundTpl: s.templates["notfound"],
+ errorTpl: s.templates["error.html"],
+ notFoundTpl: s.templates["notfound.html"],
handledError: false,
}
slug := req.URL.Path[1:]
@@ -116,6 +125,10 @@ func (s *server) router(res http.ResponseWriter, req *http.Request) {
s.renderImage(res, req, s.homeImage)
return
}
+ if slug == "rss.xml" {
+ s.renderRSS(res, req)
+ return
+ }
for _, p := range s.postList {
if slug == p.Slug {
@@ -151,12 +164,12 @@ func (s *server) createWebPage(title, subtitle, contents, path string) (string,
"contents": contents,
"path": blogURL + path,
}
- return s.templates["page"].Exec(ctx)
+ return s.templates["page.html"].Exec(ctx)
}
func (s *server) postPage(p *Post, res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html; charset=utf-8")
- contents, err := p.render(s.templates["fullpost"])
+ contents, err := p.render(s.templates["fullpost.html"])
if err != nil {
s.errorInRequest(res, req, err)
}
@@ -173,7 +186,7 @@ func (s *server) homePage(res http.ResponseWriter, req *http.Request) {
var posts string
for _, p := range s.postList {
- summary, err := p.render(s.templates["summary"])
+ summary, err := p.render(s.templates["summary.html"])
if err != nil {
log.Printf("could not render post summary for %s", p.Slug)
}
@@ -194,6 +207,41 @@ func (s *server) renderImage(res http.ResponseWriter, req *http.Request, img []b
res.Write(img)
}
+func (s *server) renderRSS(res http.ResponseWriter, req *http.Request) {
+ res.Header().Add("content-type", "text/xml; charset=utf-8")
+
+ var posts string
+
+ for _, p := range s.postList {
+ summary, err := p.render(s.templates["rss-item.xml"])
+ if err != nil {
+ log.Printf("could not render post summary for %s", p.Slug)
+ }
+ posts = posts + summary
+ }
+
+ var pubDate string
+ if len(posts) > 0 {
+ pubDate = rssDatetime(s.postList[0].Metadata.Time)
+ } else {
+ pubDate = rssDatetime(0)
+ }
+
+ page, err := s.templates["rss-channel.xml"].Exec(map[string]string{
+ "title": blogTitle,
+ "description": blogSummary,
+ "link": blogURL,
+ "pubDate": pubDate,
+ "items": posts,
+ })
+
+ if err != nil {
+ s.errorInRequest(res, req, err)
+ }
+
+ res.Write([]byte(page))
+}
+
func (s *server) loadStylesheet(res http.ResponseWriter, req *http.Request, filename string) (ok bool) {
contents, ok := s.styles[filename]
if !ok {
@@ -204,6 +252,10 @@ func (s *server) loadStylesheet(res http.ResponseWriter, req *http.Request, file
return ok
}
+func rssDatetime(timestamp int64) string {
+ return time.Unix(timestamp, 0).Format("Mon, 02 Jan 06 15:04:05 MST")
+}
+
func createImage(title, summary, url string, out io.Writer) error {
imgWidth, imgHeight, imgPaddingX, imgPaddingY := 1200, 600, 50, 100
accentHeight, spacerHeight := 12.5, 20.0
diff --git a/template.go b/template.go
index f2e7f48..5e27568 100644
--- a/template.go
+++ b/template.go
@@ -4,16 +4,15 @@ import (
"fmt"
"log"
"strconv"
- "strings"
"time"
"github.com/aymerick/raymond"
)
func loadTemplate(file string) (*raymond.Template, error) {
- tpl, err := raymond.ParseFile("templates/" + file + ".html")
+ tpl, err := raymond.ParseFile("templates/" + file)
if err != nil {
- return nil, fmt.Errorf("Could not parse %s template: %w", file, err)
+ return nil, fmt.Errorf("could not parse %s template: %w", file, err)
}
tpl.RegisterHelper("datetime", func(timeStr string) string {
timestamp, err := strconv.ParseInt(timeStr, 10, 64)
@@ -23,12 +22,23 @@ func loadTemplate(file string) (*raymond.Template, error) {
}
return time.Unix(timestamp, 0).Format("Jan 2 2006, 3:04 PM")
})
+ tpl.RegisterHelper("rssDatetime", func(timeStr string) string {
+ timestamp, err := strconv.ParseInt(timeStr, 10, 64)
+ if err != nil {
+ log.Printf("Could not parse timestamp '%v', falling back to current time", timeStr)
+ timestamp = time.Now().Unix()
+ }
+ return rssDatetime(timestamp)
+ })
+ tpl.RegisterHelper("getFullUrl", func(slug string) string {
+ return blogURL + "/" + slug
+ })
log.Printf("Loaded template: %s", file)
return tpl, nil
}
-// loadTemplates, for each f in files, loads `templates/$f.html`
-// as a handlebars HTML template. If any single template fails to
+// loadTemplates, for each f in files, loads `templates/$f`
+// as a handlebars HTML/XML template. If any single template fails to
// load, only an error is returned. Conversely, if there is no error,
// every template name passed is guaranteed to have loaded successfully.
func loadTemplates(files []string) (map[string]*raymond.Template, error) {
@@ -48,22 +58,20 @@ func newTemplateListener(update func(func(map[string]*raymond.Template))) *liste
return &listener{
folder: "templates/",
update: func(file string) error {
- tplName := strings.TrimSuffix(file, ".html")
- newTpl, err := loadTemplate(tplName)
+ newTpl, err := loadTemplate(file)
if err != nil {
return err
}
update(func(oldMap map[string]*raymond.Template) {
- oldMap[tplName] = newTpl
+ oldMap[file] = newTpl
})
return nil
},
clean: func(file string) error {
- tplName := strings.TrimSuffix(file, ".html")
update(func(oldMap map[string]*raymond.Template) {
- delete(oldMap, tplName)
+ delete(oldMap, file)
})
- log.Printf("Unloaded template: %s", tplName)
+ log.Printf("Unloaded template: %s", file)
return nil
},
}
diff --git a/templates/rss-channel.xml b/templates/rss-channel.xml
new file mode 100644
index 0000000..c70fdd6
--- /dev/null
+++ b/templates/rss-channel.xml
@@ -0,0 +1,10 @@
+
+
+ {{title}}
+ {{link}}
+ {{description}}
+ en-US
+ {{pubDate}}
+ {{{items}}}
+
+
diff --git a/templates/rss-item.xml b/templates/rss-item.xml
new file mode 100644
index 0000000..2cf4242
--- /dev/null
+++ b/templates/rss-item.xml
@@ -0,0 +1,7 @@
+-
+ {{metadata.title}}
+ {{getFullUrl slug}}
+ {{metadata.summary}}
+ mail@nsood.in
+ {{rssDatetime metadata.time}}
+