server: add RSS feed support
Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
parent
8f90a0a4d4
commit
a1ad26124c
5 changed files with 96 additions and 19 deletions
|
@ -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.
|
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:
|
To start the server:
|
||||||
|
|
||||||
go run .
|
go run .
|
||||||
|
|
||||||
Server will be live on port 8080.
|
Server will be live on port 8080.
|
||||||
|
|
64
server.go
64
server.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
"github.com/aymerick/raymond"
|
||||||
"github.com/fogleman/gg"
|
"github.com/fogleman/gg"
|
||||||
|
@ -51,7 +52,15 @@ func newServer() (*server, error) {
|
||||||
s.postList = posts
|
s.postList = posts
|
||||||
s.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -102,8 +111,8 @@ func (s *server) router(res http.ResponseWriter, req *http.Request) {
|
||||||
res = &errorCatcher{
|
res = &errorCatcher{
|
||||||
res: res,
|
res: res,
|
||||||
req: req,
|
req: req,
|
||||||
errorTpl: s.templates["error"],
|
errorTpl: s.templates["error.html"],
|
||||||
notFoundTpl: s.templates["notfound"],
|
notFoundTpl: s.templates["notfound.html"],
|
||||||
handledError: false,
|
handledError: false,
|
||||||
}
|
}
|
||||||
slug := req.URL.Path[1:]
|
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)
|
s.renderImage(res, req, s.homeImage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if slug == "rss.xml" {
|
||||||
|
s.renderRSS(res, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range s.postList {
|
for _, p := range s.postList {
|
||||||
if slug == p.Slug {
|
if slug == p.Slug {
|
||||||
|
@ -151,12 +164,12 @@ func (s *server) createWebPage(title, subtitle, contents, path string) (string,
|
||||||
"contents": contents,
|
"contents": contents,
|
||||||
"path": blogURL + path,
|
"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) {
|
func (s *server) postPage(p *Post, res http.ResponseWriter, req *http.Request) {
|
||||||
res.Header().Add("content-type", "text/html; charset=utf-8")
|
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 {
|
if err != nil {
|
||||||
s.errorInRequest(res, req, err)
|
s.errorInRequest(res, req, err)
|
||||||
}
|
}
|
||||||
|
@ -173,7 +186,7 @@ func (s *server) homePage(res http.ResponseWriter, req *http.Request) {
|
||||||
var posts string
|
var posts string
|
||||||
|
|
||||||
for _, p := range s.postList {
|
for _, p := range s.postList {
|
||||||
summary, err := p.render(s.templates["summary"])
|
summary, err := p.render(s.templates["summary.html"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("could not render post summary for %s", p.Slug)
|
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)
|
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) {
|
func (s *server) loadStylesheet(res http.ResponseWriter, req *http.Request, filename string) (ok bool) {
|
||||||
contents, ok := s.styles[filename]
|
contents, ok := s.styles[filename]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -204,6 +252,10 @@ func (s *server) loadStylesheet(res http.ResponseWriter, req *http.Request, file
|
||||||
return ok
|
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 {
|
func createImage(title, summary, url string, out io.Writer) error {
|
||||||
imgWidth, imgHeight, imgPaddingX, imgPaddingY := 1200, 600, 50, 100
|
imgWidth, imgHeight, imgPaddingX, imgPaddingY := 1200, 600, 50, 100
|
||||||
accentHeight, spacerHeight := 12.5, 20.0
|
accentHeight, spacerHeight := 12.5, 20.0
|
||||||
|
|
30
template.go
30
template.go
|
@ -4,16 +4,15 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aymerick/raymond"
|
"github.com/aymerick/raymond"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadTemplate(file string) (*raymond.Template, error) {
|
func loadTemplate(file string) (*raymond.Template, error) {
|
||||||
tpl, err := raymond.ParseFile("templates/" + file + ".html")
|
tpl, err := raymond.ParseFile("templates/" + file)
|
||||||
if err != nil {
|
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 {
|
tpl.RegisterHelper("datetime", func(timeStr string) string {
|
||||||
timestamp, err := strconv.ParseInt(timeStr, 10, 64)
|
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")
|
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)
|
log.Printf("Loaded template: %s", file)
|
||||||
return tpl, nil
|
return tpl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadTemplates, for each f in files, loads `templates/$f.html`
|
// loadTemplates, for each f in files, loads `templates/$f`
|
||||||
// as a handlebars HTML template. If any single template fails to
|
// as a handlebars HTML/XML template. If any single template fails to
|
||||||
// load, only an error is returned. Conversely, if there is no error,
|
// load, only an error is returned. Conversely, if there is no error,
|
||||||
// every template name passed is guaranteed to have loaded successfully.
|
// every template name passed is guaranteed to have loaded successfully.
|
||||||
func loadTemplates(files []string) (map[string]*raymond.Template, error) {
|
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{
|
return &listener{
|
||||||
folder: "templates/",
|
folder: "templates/",
|
||||||
update: func(file string) error {
|
update: func(file string) error {
|
||||||
tplName := strings.TrimSuffix(file, ".html")
|
newTpl, err := loadTemplate(file)
|
||||||
newTpl, err := loadTemplate(tplName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
update(func(oldMap map[string]*raymond.Template) {
|
update(func(oldMap map[string]*raymond.Template) {
|
||||||
oldMap[tplName] = newTpl
|
oldMap[file] = newTpl
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
clean: func(file string) error {
|
clean: func(file string) error {
|
||||||
tplName := strings.TrimSuffix(file, ".html")
|
|
||||||
update(func(oldMap map[string]*raymond.Template) {
|
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
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
10
templates/rss-channel.xml
Normal file
10
templates/rss-channel.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<link>{{link}}</link>
|
||||||
|
<description>{{description}}</description>
|
||||||
|
<language>en-US</language>
|
||||||
|
<pubDate>{{pubDate}}</pubDate>
|
||||||
|
{{{items}}}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
7
templates/rss-item.xml
Normal file
7
templates/rss-item.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<item>
|
||||||
|
<title>{{metadata.title}}</title>
|
||||||
|
<link>{{getFullUrl slug}}</link>
|
||||||
|
<description>{{metadata.summary}}</description>
|
||||||
|
<author>mail@nsood.in</author>
|
||||||
|
<pubDate>{{rssDatetime metadata.time}}</pubDate>
|
||||||
|
</item>
|
Loading…
Add table
Add a link
Reference in a new issue