diff --git a/handlers/home.go b/handlers/home.go index 6c49bee..92e61e7 100644 --- a/handlers/home.go +++ b/handlers/home.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "time" @@ -33,6 +34,39 @@ func HomePage(db *gorm.DB) gin.HandlerFunc { } } +// HomeArticlesAPI returns articles in JSON format for infinite scroll. +func HomeArticlesAPI(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + page := 1 + if p, ok := c.GetQuery("page"); ok { + var parsed int + if _, err := fmt.Sscanf(p, "%d", &parsed); err == nil && parsed > 0 { + page = parsed + } + } + + pageSize := 10 + offset := (page - 1) * pageSize + + var articles []models.Article + db.Where("status = ?", models.ArticlePublished). + Order(publishedArticleOrder). + Limit(pageSize). + Offset(offset). + Find(&articles) + + var total int64 + db.Model(&models.Article{}). + Where("status = ?", models.ArticlePublished). + Count(&total) + + c.JSON(http.StatusOK, gin.H{ + "articles": articles, + "hasMore": int64(offset+len(articles)) < total, + }) + } +} + // ArticleDetail renders a single published article by its slug. func ArticleDetail(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/handlers/rss.go b/handlers/rss.go new file mode 100644 index 0000000..f600f69 --- /dev/null +++ b/handlers/rss.go @@ -0,0 +1,181 @@ +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 +} diff --git a/main.go b/main.go index 45d7130..87ed78a 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,9 @@ func main() { // 8. Register routes. router.GET("/", handlers.HomePage(db)) + router.GET("/api/articles", handlers.HomeArticlesAPI(db)) + router.GET("/rss", handlers.RSSFeed(db)) + router.GET("/feed", handlers.RSSFeed(db)) router.GET("/login", handlers.LoginPage()) router.POST("/login", handlers.Login(db)) router.POST("/logout", handlers.Logout()) diff --git a/templates/layouts/base.html b/templates/layouts/base.html index 4d0a955..c3e6e45 100644 --- a/templates/layouts/base.html +++ b/templates/layouts/base.html @@ -5,6 +5,7 @@