package handler import ( "net/http" "strings" "time" "git.iioio.com/freefire/jiang13-forum/service" "github.com/gin-gonic/gin" ) const seoSitemapLimit = 5000 // RobotsTxt 搜索引擎抓取规则 func (h *Handlers) RobotsTxt(c *gin.Context) { base := h.publicBaseURL(c) var b strings.Builder b.WriteString("User-agent: *\n") b.WriteString("Allow: /\n") b.WriteString("Disallow: /api/\n") b.WriteString("Disallow: /admin\n") b.WriteString("Disallow: /compose\n") b.WriteString("Disallow: /login\n") b.WriteString("Disallow: /register\n") b.WriteString("Disallow: /profile\n") b.WriteString("Disallow: /favorites\n") b.WriteString("Disallow: /oauth/\n") b.WriteString("Disallow: /media/\n") b.WriteString("Disallow: /*/edit\n") if base != "" { b.WriteString("\nSitemap: ") b.WriteString(base) b.WriteString("/sitemap.xml\n") } c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(b.String())) } // FaviconICO 约定路径 /favicon.ico:有品牌图标则 302 到实际上传 URL,否则 404 func (h *Handlers) FaviconICO(c *gin.Context) { href := strings.TrimSpace(h.Settings.SiteBranding().Favicon) if href == "" { c.Status(http.StatusNotFound) return } // 相对路径保持站内跳转;绝对 URL 也可 Redirect c.Header("Cache-Control", "public, max-age=86400") c.Redirect(http.StatusFound, href) } // SitemapXML 公开页面站点地图 func (h *Handlers) SitemapXML(c *gin.Context) { base := h.publicBaseURL(c) if base == "" { c.String(http.StatusServiceUnavailable, "未配置站点 ROOT_URL,无法生成 sitemap") return } now := time.Now().UTC() permalink := h.Settings.Permalink() urls := []service.SitemapURL{ {Loc: base + "/", LastMod: now, ChangeFreq: "hourly", Priority: "1.0"}, {Loc: base + "/projects", LastMod: now, ChangeFreq: "daily", Priority: "0.6"}, {Loc: base + "/links", LastMod: now, ChangeFreq: "weekly", Priority: "0.6"}, } if boards, err := h.Board.List(); err == nil { for _, board := range boards { urls = append(urls, service.SitemapURL{ Loc: base + service.QueryBoardHome(board.ID, permalink), LastMod: board.UpdatedAt.UTC(), ChangeFreq: "daily", Priority: "0.7", }) } } if posts, e1 := h.Post.ListSitemap(seoSitemapLimit); e1 == nil { for _, p := range posts { lm := p.UpdatedAt if lm.IsZero() { lm = p.CreatedAt } urls = append(urls, service.SitemapURL{ Loc: base + permalink.PostPath(p.ID), LastMod: lm.UTC(), ChangeFreq: "weekly", Priority: "0.8", }) } } if users, e2 := h.User.ListSitemap(seoSitemapLimit); e2 == nil { for _, u := range users { urls = append(urls, service.SitemapURL{ Loc: base + permalink.UserPath(u.ID), LastMod: u.UpdatedAt.UTC(), ChangeFreq: "weekly", Priority: "0.5", }) } } if pages, e3 := h.SitePage.ListSitemap(seoSitemapLimit); e3 == nil { for _, p := range pages { lm := p.UpdatedAt if lm.IsZero() { lm = p.CreatedAt } urls = append(urls, service.SitemapURL{ Loc: base + permalink.PagePath(p.Slug), LastMod: lm.UTC(), ChangeFreq: "monthly", Priority: "0.5", }) } } var b strings.Builder b.WriteString(``) b.WriteString(``) for _, u := range urls { b.WriteString("") b.WriteString("") b.WriteString(xmlEscape(u.Loc)) b.WriteString("") if !u.LastMod.IsZero() { b.WriteString("") b.WriteString(u.LastMod.Format("2006-01-02")) b.WriteString("") } if u.ChangeFreq != "" { b.WriteString("") b.WriteString(u.ChangeFreq) b.WriteString("") } if u.Priority != "" { b.WriteString("") b.WriteString(u.Priority) b.WriteString("") } b.WriteString("") } b.WriteString("") c.Data(http.StatusOK, "application/xml; charset=utf-8", []byte(b.String())) } func (h *Handlers) publicBaseURL(c *gin.Context) string { return h.Settings.SitePublicBaseURL(requestOrigin(c)) } func requestOrigin(c *gin.Context) string { proto := c.GetHeader("X-Forwarded-Proto") if proto == "" { if c.Request.TLS != nil { proto = "https" } else { proto = "http" } } host := c.GetHeader("X-Forwarded-Host") if host == "" { host = c.Request.Host } if host == "" { return "" } return proto + "://" + host } func xmlEscape(s string) string { r := strings.NewReplacer( `&`, "&", `<`, "<", `>`, ">", `"`, """, `'`, "'", ) return r.Replace(s) }