- 编辑页标题改 filled 变体;标签改 tags 选择器(可勾选既有/输入新建) - 笔记发布日期可自选(schema v2 新增 published_at 并回填),前台展示发布日期 - 登录页移除「请输入管理密码以继续」「连续失败将被暂时锁定」文案 - 站点设置每页条数收敛为 10/20/30/50 选择器,后端白名单同源校验 - 站点设置两卡片宽屏左右/窄屏上下;修改密码按钮改常规大小 - 后台各页顶栏固定高度,切换页面不再抖动 - 站点 Logo:settings 新增 site_logo(站内路径),gc/孤儿清单豁免 Logo 图片, 博客 header 站点名前展示,设置页支持上传/更换/清除 - 回收站右上角一键清空(DELETE /api/admin/trash,含确认弹窗) - pn init 写入 Markdown 语法示例文档(slug welcome,公开)与程序生成的示例图片
135 lines
3.8 KiB
Go
135 lines
3.8 KiB
Go
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,
|
||
})
|
||
}
|
||
// 无公开笔记时省略 lastBuildDate(零值 Format 会产出公元 1 年的非法日期)
|
||
lastBuildStr := ""
|
||
if !lastBuild.IsZero() {
|
||
lastBuildStr = lastBuild.Format(time.RFC1123Z)
|
||
}
|
||
feed := rssFeed{
|
||
Version: "2.0",
|
||
Channel: rssChannel{
|
||
Title: ss.SiteTitle,
|
||
Link: base + "/",
|
||
Description: ss.SiteDesc,
|
||
Language: "zh-CN",
|
||
LastBuild: lastBuildStr,
|
||
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"))
|
||
}
|