blogo/rss.go
rss.goRaw
1package main
2
3import (
4 "bytes"
5 "encoding/xml"
6 "fmt"
7 "html"
8 "log"
9 "strings"
10 "time"
11)
12
13const rssFilename = "rss.xml"
14
15type rssDocument struct {
16 XMLName xml.Name `xml:"rss"`
17 Version string `xml:"version,attr"`
18 Channel rssChannel `xml:"channel"`
19}
20
21type rssChannel struct {
22 Title string `xml:"title"`
23 Link string `xml:"link"`
24 Description string `xml:"description"`
25 Items []rssItem `xml:"item"`
26}
27
28type rssItem struct {
29 Title string `xml:"title"`
30 Link string `xml:"link"`
31 Description string `xml:"description,omitempty"`
32 PubDate string `xml:"pubDate,omitempty"`
33 GUID rssGUID `xml:"guid"`
34}
35
36type rssGUID struct {
37 IsPermaLink bool `xml:"isPermaLink,attr"`
38 Value string `xml:",chardata"`
39}
40
41func generateRSS() {
42 items := make([]rssItem, 0, len(config.Posts))
43 for _, post := range config.Posts {
44 source := post.Md
45 if post.OutsideArticle != "" || source == "" {
46 source = post.Thumb
47 }
48
49 title := markdownTitle(readFile(directory + "/" + config.PostsDir + source))
50 link := post.OutsideArticle
51 if link == "" {
52 filename := strings.Split(post.Md, ".")[0] + ".html"
53 link = joinURL(config.AbsoluteUrl, filename)
54 }
55
56 pubDate, err := rssDate(post.Date)
57 if err != nil {
58 log.Printf("invalid RSS date for %s: %v", source, err)
59 }
60
61 items = append(items, rssItem{
62 Title: title,
63 Link: link,
64 Description: post.MetaDescr,
65 PubDate: pubDate,
66 GUID: rssGUID{IsPermaLink: true, Value: link},
67 })
68 }
69
70 document := rssDocument{
71 Version: "2.0",
72 Channel: rssChannel{
73 Title: config.Title,
74 Link: joinURL(config.AbsoluteUrl),
75 Description: config.MetaDescr,
76 Items: items,
77 },
78 }
79
80 var output bytes.Buffer
81 output.WriteString(xml.Header)
82 encoder := xml.NewEncoder(&output)
83 encoder.Indent("", " ")
84 check(encoder.Encode(document))
85 writeFile(config.OutputDir+"/"+rssFilename, output.String())
86}
87
88func addRSSDiscovery(document string) string {
89 feedURL := joinURL(config.AbsoluteUrl, rssFilename)
90 tag := fmt.Sprintf(`<link rel="alternate" type="application/rss+xml" title="%s RSS" href="%s">`,
91 html.EscapeString(config.Title), html.EscapeString(feedURL))
92 if strings.Contains(document, tag) {
93 return document
94 }
95
96 headEnd := strings.Index(strings.ToLower(document), "</head>")
97 if headEnd == -1 {
98 return document
99 }
100 return document[:headEnd] + tag + "\n" + document[headEnd:]
101}
102
103func markdownTitle(markdown string) string {
104 firstLine := strings.SplitN(markdown, "\n", 2)[0]
105 return strings.TrimSpace(strings.TrimLeft(firstLine, "#"))
106}
107
108func joinURL(parts ...string) string {
109 if len(parts) == 0 {
110 return ""
111 }
112
113 joined := strings.TrimRight(parts[0], "/")
114 for _, part := range parts[1:] {
115 part = strings.Trim(part, "/")
116 if part == "" {
117 continue
118 }
119 if joined == "" {
120 joined = "/" + part
121 } else {
122 joined += "/" + part
123 }
124 }
125 return joined
126}
127
128func rssDate(value string) (string, error) {
129 if value == "" {
130 return "", nil
131 }
132
133 formats := []string{time.RFC3339, "2006-01-02"}
134 for _, format := range formats {
135 parsed, err := time.Parse(format, value)
136 if err == nil {
137 return parsed.Format(time.RFC1123Z), nil
138 }
139 }
140 return "", fmt.Errorf("%q must use RFC 3339 or YYYY-MM-DD format", value)
141}