` + html.EscapeString(excerpt) + `
`) } if thumb != "" { b.WriteString(`package handler
import (
"encoding/json"
"fmt"
"html"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
"unicode/utf8"
"git.iioio.com/freefire/jiang13-forum/embed_static"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/service"
"github.com/gin-gonic/gin"
)
var firstImgSrcRe = regexp.MustCompile(`(?i)]+src=["']([^"']+)["']`)
// homeBootPayload 与前端 __J13_HOME_BOOT__ 对齐,供冷启动灌缓存
type homeBootPayload struct {
BoardID uint `json:"board_id"`
Sort string `json:"sort"`
Keyword string `json:"keyword"`
Tag string `json:"tag"`
Author string `json:"author"`
TitleOnly bool `json:"title_only"`
Posts []service.PostListItem `json:"posts"`
PostTotal int64 `json:"post_total"`
Page int `json:"page"`
Boards []service.BoardWithStats `json:"boards"`
Stats homeBootStats `json:"stats"`
RecentComments []service.RecentCommentItem `json:"recent_comments"`
RecentUsers []service.RecentUserItem `json:"recent_users"`
Tags []service.TagCount `json:"tags"`
Showcase []service.CommunityShowcaseItem `json:"showcase"`
Pages []service.SitePageSummary `json:"pages"`
Limits service.ForumLimitsPublic `json:"limits"`
Branding service.SiteBranding `json:"branding"`
User *model.UserSelf `json:"user"`
UnreadMessages int `json:"unread_messages"`
CheckIn *service.CheckInStatus `json:"check_in"`
}
type homeBootStats struct {
Users int64 `json:"users"`
Posts int64 `json:"posts"`
Boards int64 `json:"boards"`
Comments int64 `json:"comments"`
}
type homeSSRData struct {
boot homeBootPayload
meta homeSSRMeta
}
type homeSSRMeta struct {
boardName string
permalink service.PermalinkConfig
listStyle string
defSort string
}
// serveFeedDocument 首页/板块:文档 SSR + boot;失败则回退空 root SPA
func (h *Handlers) serveFeedDocument(c *gin.Context, meta *embed_static.SPAPageMeta, boardID uint) {
data, err := h.gatherHomeSSR(c, boardID)
if err != nil {
embed_static.ServeSPAWithMeta(c, meta)
return
}
bootJSON, err := json.Marshal(data.boot)
if err != nil {
embed_static.ServeSPAWithMeta(c, meta)
return
}
meta.RootHTML = renderHomeSSRHTML(data)
meta.BootJSON = bootJSON
embed_static.ServeSPAWithMeta(c, meta)
}
func (h *Handlers) gatherHomeSSR(c *gin.Context, boardID uint) (*homeSSRData, error) {
limits := h.Settings.PublicLimits()
brand := h.Settings.SiteBranding()
brand.SiteURL = h.publicBaseURL(c)
permalink := h.Settings.Permalink()
defSort := h.Settings.DefaultFeedSort()
sort := strings.TrimSpace(c.Query("sort"))
if sort == "" {
sort = defSort
}
keyword := strings.TrimSpace(c.Query("keyword"))
tag := strings.TrimSpace(c.Query("tag"))
author := strings.TrimSpace(c.Query("author"))
titleOnly := c.Query("title_only") == "1" || strings.EqualFold(c.Query("title_only"), "true")
if tag != "" {
keyword = ""
author = ""
titleOnly = false
}
pageSize := h.Settings.PageSizeDefault()
listStyle := limits.FeedListStyle
if listStyle == "" {
listStyle = "title"
}
out := &homeSSRData{
boot: homeBootPayload{
BoardID: boardID,
Sort: sort,
Keyword: keyword,
Tag: tag,
Author: author,
TitleOnly: titleOnly,
Page: 1,
Limits: limits,
Branding: brand,
},
meta: homeSSRMeta{
permalink: permalink,
listStyle: listStyle,
defSort: defSort,
},
}
var (
postsErr, boardsErr error
wg sync.WaitGroup
)
wg.Add(1)
go func() {
defer wg.Done()
q := service.PostListQuery{
BoardID: boardID,
Page: 1,
Size: pageSize,
Sort: sort,
Keyword: keyword,
Tag: tag,
Author: author,
TitleOnly: titleOnly,
ViewerID: h.currentUserID(c),
ViewerIsAdmin: h.isAdmin(c),
}
items, total, err := h.Post.ListItems(q)
if err != nil {
postsErr = err
return
}
if items == nil {
items = []service.PostListItem{}
}
if h.Badge != nil {
users := make([]*model.User, 0, len(items))
for i := range items {
if items[i].User.ID > 0 {
users = append(users, &items[i].User)
}
}
h.Badge.AttachBadgeSummaries(users, 2)
}
out.boot.Posts = items
out.boot.PostTotal = total
}()
wg.Add(1)
go func() {
defer wg.Done()
boards, err := h.Board.ListWithStats()
if err != nil {
boardsErr = err
return
}
if boards == nil {
boards = []service.BoardWithStats{}
}
out.boot.Boards = boards
for _, board := range boards {
if board.ID == boardID {
out.meta.boardName = board.Name
break
}
}
}()
wg.Add(1)
go func() {
defer wg.Done()
var users, posts, boardsN, comments int64
model.DB.Model(&model.User{}).Count(&users)
model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished).Count(&posts)
model.DB.Model(&model.Board{}).Count(&boardsN)
model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&comments)
out.boot.Stats = homeBootStats{Users: users, Posts: posts, Boards: boardsN, Comments: comments}
}()
wg.Add(1)
go func() {
defer wg.Done()
pages, err := h.SitePage.ListPublished()
if err != nil || pages == nil {
pages = []service.SitePageSummary{}
}
out.boot.Pages = pages
}()
widgetEnabled := map[string]bool{}
for _, w := range limits.AsideWidgets {
widgetEnabled[w.ID] = w.Enabled
}
if widgetEnabled[service.AsideWidgetRecentComments] {
wg.Add(1)
go func() {
defer wg.Done()
items, err := h.Comment.ListRecentPublic(8)
if err != nil || items == nil {
items = []service.RecentCommentItem{}
}
out.boot.RecentComments = items
}()
} else {
out.boot.RecentComments = []service.RecentCommentItem{}
}
if widgetEnabled[service.AsideWidgetRecentUsers] {
wg.Add(1)
go func() {
defer wg.Done()
items, err := h.User.ListRecentRegistered(8)
if err != nil || items == nil {
items = []service.RecentUserItem{}
}
out.boot.RecentUsers = items
}()
} else {
out.boot.RecentUsers = []service.RecentUserItem{}
}
if widgetEnabled[service.AsideWidgetTagCloud] {
wg.Add(1)
go func() {
defer wg.Done()
tags, err := h.Post.PopularTags(40)
if err != nil || tags == nil {
tags = []service.TagCount{}
}
out.boot.Tags = tags
}()
} else {
out.boot.Tags = []service.TagCount{}
}
if widgetEnabled[service.AsideWidgetShowcase] && h.Community != nil {
wg.Add(1)
go func() {
defer wg.Done()
items, err := h.Community.ListShowcase(c.Request.Host)
if err != nil || items == nil {
items = []service.CommunityShowcaseItem{}
}
out.boot.Showcase = items
}()
} else {
out.boot.Showcase = []service.CommunityShowcaseItem{}
}
uid := h.currentUserID(c)
if uid > 0 {
wg.Add(1)
go func() {
defer wg.Done()
user, err := h.User.GetByID(uid)
if err != nil || user == nil {
return
}
view := user.ToSelf()
if h.Badge != nil {
_ = h.Badge.EvaluateAuto(user.ID)
if badges, bErr := h.Badge.ListUserBadges(user.ID); bErr == nil {
view.Badges = service.BadgeViews(badges, 0)
}
}
out.boot.User = &view
}()
if h.Message != nil {
wg.Add(1)
go func() {
defer wg.Done()
total, _, _, err := h.Message.UnreadCounts(uid)
if err == nil {
out.boot.UnreadMessages = int(total)
}
}()
}
if h.Points != nil {
wg.Add(1)
go func() {
defer wg.Done()
st, err := h.Points.GetCheckInStatus(uid)
if err == nil {
out.boot.CheckIn = &st
}
}()
}
}
wg.Wait()
if postsErr != nil {
return nil, postsErr
}
if boardsErr != nil {
return nil, boardsErr
}
return out, nil
}
func renderHomeSSRHTML(data *homeSSRData) string {
b := &strings.Builder{}
b.Grow(64 * 1024)
boot := data.boot
meta := data.meta
brandName := strings.TrimSpace(boot.Branding.Name)
if brandName == "" {
brandName = "姜十三论坛"
}
logoMark := strings.TrimSpace(boot.Branding.LogoMark)
if logoMark == "" {
if r, _ := utf8.DecodeRuneInString(brandName); r != utf8.RuneError {
logoMark = string(r)
} else {
logoMark = "姜"
}
}
loggedIn := boot.User != nil && boot.User.ID > 0
b.WriteString(`
` + html.EscapeString(excerpt) + `
`) } if thumb != "" { b.WriteString(`