package httpapi import ( "encoding/xml" "net/http" "time" "pure-note/internal/markdown" ) // ---- RSS 2.0(§7.1:仅公开;服务端 goldmark+bluemonday 渲染)---- type rssFeed 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"` Language string `xml:"language,omitempty"` LastBuild string `xml:"lastBuildDate,omitempty"` Items []rssItem `xml:"item"` } type rssItem struct { Title string `xml:"title"` Link string `xml:"link"` GUID string `xml:"guid"` PubDate string `xml:"pubDate"` // RFC 822(RSS 2.0 规范) Description string `xml:"description"` // 已清洗的 HTML(经 encoding/xml 自动转义) } // feedItemLimit RSS 条目上限(个人规模足够,避免全量渲染)。 const feedItemLimit = 50 func (s *Server) handleRSS(w http.ResponseWriter, r *http.Request) { ss, err := s.st.GetSiteSettings() if err != nil { writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } base := baseURL(r) notes, err := s.st.ListPublicNotesFull(feedItemLimit) if err != nil { writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } items := make([]rssItem, 0, len(notes)) var lastBuild time.Time for _, n := range notes { // 服务端渲染:goldmark(默认转义)→ bluemonday 白名单(§7.5) html := markdown.Render(n.Content) pub := time.Unix(n.UpdatedAt, 0) if pub.After(lastBuild) { lastBuild = pub } items = append(items, rssItem{ Title: n.Title, Link: base + "/notes/" + n.Slug, GUID: base + "/notes/" + n.Slug, PubDate: pub.Format(time.RFC1123Z), Description: html, }) } feed := rssFeed{ Version: "2.0", Channel: rssChannel{ Title: ss.SiteTitle, Link: base + "/", Description: ss.SiteDesc, Language: "zh-CN", LastBuild: lastBuild.Format(time.RFC1123Z), Items: items, }, } w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=300") w.Write([]byte(xml.Header)) enc := xml.NewEncoder(w) enc.Indent("", " ") _ = enc.Encode(feed) } // ---- sitemap(仅公开笔记)---- type urlSet struct { XMLName xml.Name `xml:"urlset"` XMLNS string `xml:"xmlns,attr"` URLs []siteURL `xml:"url"` } type siteURL struct { Loc string `xml:"loc"` LastMod time.Time `xml:"lastmod,omitempty"` } func (s *Server) handleSitemap(w http.ResponseWriter, r *http.Request) { base := baseURL(r) set := urlSet{XMLNS: "http://www.sitemaps.org/schemas/sitemap/0.9"} set.URLs = append(set.URLs, siteURL{Loc: base + "/"}) notes, _, err := s.st.ListPublicNotes(1, 10000, "") if err != nil { writeError(w, http.StatusInternalServerError, "internal", "内部错误") return } for _, n := range notes { set.URLs = append(set.URLs, siteURL{ Loc: base + "/notes/" + n.Slug, LastMod: time.Unix(n.UpdatedAt, 0), }) } w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=3600") w.Write([]byte(xml.Header)) enc := xml.NewEncoder(w) enc.Indent("", " ") _ = enc.Encode(set) } // ---- robots.txt ---- func (s *Server) handleRobots(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=86400") w.Write([]byte("User-agent: *\nAllow: /\nDisallow: /admin\n\nSitemap: " + baseURL(r) + "/sitemap.xml\n")) }