package handlers import ( "encoding/xml" "fmt" "html" "net/http" "strings" "time" "github.com/gin-gonic/gin" "go_blog/models" "gorm.io/gorm" ) // RSS 2.0 XML structure definitions. // RSS is the root element of an RSS 2.0 feed. type RSS struct { XMLName xml.Name `xml:"rss"` Version string `xml:"version,attr"` Channel *Channel `xml:"channel"` } // Channel represents the RSS channel containing feed metadata and items. type Channel struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Language string `xml:"language"` LastBuildDate string `xml:"lastBuildDate,omitempty"` Items []Item `xml:"item"` } // Item represents a single article in the RSS feed. type Item struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Author string `xml:"author,omitempty"` PubDate string `xml:"pubDate"` GUID string `xml:"guid"` } // RSSFeed generates an RSS 2.0 feed of the latest published articles. func RSSFeed(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // Determine the current language for site metadata. lang, exists := c.Get("lang") if !exists { lang = "en" } langStr := lang.(string) // Get site settings for feed metadata. siteSetting := &models.SiteSetting{} db.First(siteSetting) // Construct the base URL from the request. scheme := "http" if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { scheme = "https" } baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host) // Get the latest 20 published articles. var articles []models.Article db.Where("status = ?", models.ArticlePublished). Preload("Author"). Order(publishedArticleOrder). Limit(20). Find(&articles) // Build channel metadata. channel := &Channel{ Title: siteSetting.LogoText(langStr), Link: baseURL, Description: siteSetting.HomeSubtitle(langStr), Language: getRSSLanguage(langStr), Items: make([]Item, 0, len(articles)), } // Set lastBuildDate to the most recent article's publish date. if len(articles) > 0 && articles[0].PublishedAt != nil { channel.LastBuildDate = formatRSSTime(*articles[0].PublishedAt) } // Convert articles to RSS items. for _, article := range articles { item := Item{ Title: article.Title, Link: fmt.Sprintf("%s/article/%s", baseURL, article.Slug), Description: getArticleDescription(&article), PubDate: formatRSSTime(getArticlePubDate(&article)), GUID: fmt.Sprintf("%s/article/%s", baseURL, article.Slug), } // Add author information. if article.Author.DisplayName != "" { item.Author = article.Author.DisplayName } else { item.Author = article.Author.Username } channel.Items = append(channel.Items, item) } // Build the RSS feed. feed := &RSS{ Version: "2.0", Channel: channel, } // Set the correct content type and return XML. c.Header("Content-Type", "application/rss+xml; charset=utf-8") c.XML(http.StatusOK, feed) } } // getRSSLanguage converts the internal language code to RSS language format. func getRSSLanguage(lang string) string { switch lang { case "zh": return "zh-CN" case "en": return "en-US" default: return "en-US" } } // formatRSSTime formats a time.Time to RFC1123Z format required by RSS 2.0. func formatRSSTime(t time.Time) string { return t.Format(time.RFC1123Z) } // getArticlePubDate returns the article's publish date, falling back to created date. func getArticlePubDate(article *models.Article) time.Time { if article.PublishedAt != nil { return *article.PublishedAt } return article.CreatedAt } // getArticleDescription returns the article description for RSS. // Prefers the summary field; falls back to truncated content. func getArticleDescription(article *models.Article) string { if article.Summary != "" { return html.EscapeString(article.Summary) } // Strip HTML tags and truncate content to 200 characters. content := stripHTMLTags(article.Content) if len(content) > 200 { content = content[:200] + "..." } return html.EscapeString(content) } // stripHTMLTags removes HTML tags from a string (basic implementation). func stripHTMLTags(s string) string { // Remove HTML tags by finding < and > pairs. var result strings.Builder inTag := false for _, r := range s { if r == '<' { inTag = true continue } if r == '>' { inTag = false continue } if !inTag { result.WriteRune(r) } } // Clean up multiple spaces and trim. cleaned := strings.Join(strings.Fields(result.String()), " ") return cleaned }