package main import ( "bytes" "encoding/xml" "fmt" "html" "log" "strings" "time" ) const rssFilename = "rss.xml" type rssDocument struct { XMLName xml.Name `xml:"rss"` Version string `xml:"version,attr"` Channel rssChannel `xml:"channel"` } type rssChannel struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Items []rssItem `xml:"item"` } type rssItem struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description,omitempty"` PubDate string `xml:"pubDate,omitempty"` GUID rssGUID `xml:"guid"` } type rssGUID struct { IsPermaLink bool `xml:"isPermaLink,attr"` Value string `xml:",chardata"` } func generateRSS() { items := make([]rssItem, 0, len(config.Posts)) for _, post := range config.Posts { source := post.Md if post.OutsideArticle != "" || source == "" { source = post.Thumb } title := markdownTitle(readFile(directory + "/" + config.PostsDir + source)) link := post.OutsideArticle if link == "" { filename := strings.Split(post.Md, ".")[0] + ".html" link = joinURL(config.AbsoluteUrl, filename) } pubDate, err := rssDate(post.Date) if err != nil { log.Printf("invalid RSS date for %s: %v", source, err) } items = append(items, rssItem{ Title: title, Link: link, Description: post.MetaDescr, PubDate: pubDate, GUID: rssGUID{IsPermaLink: true, Value: link}, }) } document := rssDocument{ Version: "2.0", Channel: rssChannel{ Title: config.Title, Link: joinURL(config.AbsoluteUrl), Description: config.MetaDescr, Items: items, }, } var output bytes.Buffer output.WriteString(xml.Header) encoder := xml.NewEncoder(&output) encoder.Indent("", " ") check(encoder.Encode(document)) writeFile(config.OutputDir+"/"+rssFilename, output.String()) } func addRSSDiscovery(document string) string { feedURL := joinURL(config.AbsoluteUrl, rssFilename) tag := fmt.Sprintf(``, html.EscapeString(config.Title), html.EscapeString(feedURL)) if strings.Contains(document, tag) { return document } headEnd := strings.Index(strings.ToLower(document), "") if headEnd == -1 { return document } return document[:headEnd] + tag + "\n" + document[headEnd:] } func markdownTitle(markdown string) string { firstLine := strings.SplitN(markdown, "\n", 2)[0] return strings.TrimSpace(strings.TrimLeft(firstLine, "#")) } func joinURL(parts ...string) string { if len(parts) == 0 { return "" } joined := strings.TrimRight(parts[0], "/") for _, part := range parts[1:] { part = strings.Trim(part, "/") if part == "" { continue } if joined == "" { joined = "/" + part } else { joined += "/" + part } } return joined } func rssDate(value string) (string, error) { if value == "" { return "", nil } formats := []string{time.RFC3339, "2006-01-02"} for _, format := range formats { parsed, err := time.Parse(format, value) if err == nil { return parsed.Format(time.RFC1123Z), nil } } return "", fmt.Errorf("%q must use RFC 3339 or YYYY-MM-DD format", value) }