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