- Add single-column layout for homepage articles - Implement smart image positioning based on aspect ratio: * Landscape images (ratio > 1.2) displayed on top * Portrait/square images (ratio ≤ 1.2) displayed on left side - Add infinite scroll with lazy loading - Create new API endpoint /api/articles for pagination - Add loading indicator and 'no more articles' message - Optimize performance with image lazy loading and scroll throttling - Add responsive layout for mobile devices Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
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
|
|
}
|