Files
谢宇宁andClaude Opus 5.5 3639d2184a Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:47:17 +07:00

251 lines
6.7 KiB
Go

package web
import (
"net/http"
"strconv"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/configser"
"videoweb/app/service/playletser"
"videoweb/app/service/seoser"
"videoweb/app/service/translationser"
"github.com/gin-gonic/gin"
)
// HTML 渲染页面并注入 footer / 导航 / 公共配置,并确保存在游客会话。
func HTML(c *gin.Context, code int, name string, data gin.H) {
if data == nil {
data = gin.H{}
}
if _, ok := data["footer"]; !ok {
footer := configser.FetchFooterOrEmpty(c)
data["footer"] = footer
}
headerNav, ok := data["headerNav"].([]translationser.HeaderNavItemVM)
if !ok {
headerNav = translationser.FetchHeaderNavOrDefault(c)
data["headerNav"] = headerNav
}
activeKey, _ := data["navActive"].(string)
resolvedNavActive := translationser.ResolveNavigationActiveKey(activeKey, c.Request.URL.Path)
headerNav = translationser.MarkHeaderNavActive(headerNav, c.Request.URL.Path)
data["headerNav"] = headerNav
data["resolvedNavActive"] = resolvedNavActive
data["pageConsoleData"] = buildPageConsoleData(c.Request.URL.RequestURI(), code, name, data)
seoInfo := pageSEOFromData(data["seoInfo"])
seoChanged := false
if navSEO, found := translationser.FindHeaderNavSEO(headerNav, activeKey, c.Request.URL.Path); found {
seoInfo = playletser.MergeSeoInfo(playletser.SeoInfo{
Title: navSEO.Title,
Description: navSEO.Description,
Keywords: navSEO.Keyworld,
}, seoInfo)
seoChanged = true
}
if selections, ok := data["seoSelections"].([]playletser.SEOSelection); ok && len(selections) > 0 {
seoInfo = playletser.DifferentiateSeoInfo(seoInfo, selections)
seoChanged = true
}
if pageNum := paginationPage(data["pagination"]); pageNum > 1 {
pageTitle, _ := data["title"].(string)
pageTitle = playletser.PaginatedTitle(pageTitle, pageNum)
if pageTitle != "" {
data["title"] = pageTitle
}
seoInfo = playletser.PaginateSeoInfo(seoInfo, paginationDescription(data, pageTitle), pageNum)
seoChanged = true
}
if seoChanged {
data["seoInfo"] = seoInfo
if seoInfo.Title != "" {
data["title"] = seoInfo.Title
}
}
if _, ok := data["appConfig"]; !ok {
data["appConfig"] = configser.FetchAppConfigVMOrEmpty(c)
}
cfg, ok := data["appConfig"].(configser.AppConfigVM)
if !ok {
cfg = configser.FetchAppConfigVMOrEmpty(c)
}
data["appConfig"] = cfg
siteURL := seoser.ResolveSiteURL(c, cfg)
data["foreverUrlAddr"] = siteURL
data["siteURL"] = siteURL
attachPaginationLinks(data, siteURL)
canonicalURL := seoser.CanonicalWithoutQuery(seoser.PageCanonicalURL(siteURL, cfg.ForeverURLAddr, c))
canonicalURL = seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data))
data["canonicalURL"] = canonicalURL
data["seoVerify"] = seoser.VerifyFromConfig()
noindex := pageNoIndex(data)
if noindex {
data["noindex"] = true
}
if code >= http.StatusOK && code < http.StatusMultipleChoices && !noindex && seoser.IsIndexablePath(c.Request.URL.Path) {
data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg)
}
// 无 token 时静默建立游客会话(POST /api/app/auth/guest),上游内容接口依赖该会话。
authser.EnsureGuestOnEnter(c)
c.HTML(code, name, data)
}
// buildPageConsoleData 构建浏览器控制台使用的页面返回数据快照。
// 快照不包含公共配置等大字段。
func buildPageConsoleData(path string, code int, templateName string, data gin.H) gin.H {
pageData := make(gin.H, len(data))
for key, value := range data {
if pageConsoleExcludedKey(key) {
continue
}
pageData[key] = value
}
payload := gin.H{
"path": path,
"status": code,
"template": templateName,
"data": pageData,
}
if headerNav, ok := data["headerNav"]; ok {
payload["headerNav"] = headerNav
}
if footer, ok := data["footer"]; ok {
payload["footer"] = footer
}
return payload
}
func pageConsoleExcludedKey(key string) bool {
switch key {
case "pageConsoleData", "appConfig", "footer", "headerNav", "jsonLD", "seoVerify":
return true
default:
return false
}
}
func pageSEOFromData(value any) playletser.SeoInfo {
switch seo := value.(type) {
case playletser.SeoInfo:
return seo
case *playletser.SeoInfo:
if seo != nil {
return *seo
}
}
return playletser.SeoInfo{}
}
func paginationPage(value any) int {
switch pagination := value.(type) {
case playletser.PaginationVM:
return pagination.Page
case *playletser.PaginationVM:
if pagination != nil {
return pagination.Page
}
}
return 0
}
func paginationDescription(data gin.H, pageTitle string) string {
for _, key := range []string{"description", "categoryDescription", "tagDescription", "topicDescription"} {
if value, _ := data[key].(string); strings.TrimSpace(value) != "" {
return value
}
}
pageTitle = strings.TrimSpace(pageTitle)
if pageTitle == "" {
return "浏览本页内容与站内相关推荐。"
}
return "浏览" + pageTitle + "的本页内容与站内相关推荐。"
}
func attachPaginationLinks(data gin.H, siteURL string) {
var pagination playletser.PaginationVM
switch value := data["pagination"].(type) {
case playletser.PaginationVM:
pagination = value
case *playletser.PaginationVM:
if value == nil {
return
}
pagination = *value
default:
return
}
if !pagination.Visible {
return
}
if pagination.HasPrev && strings.TrimSpace(pagination.PrevURL) != "" {
data["paginationPrevURL"] = seoser.AbsURL(siteURL, pagination.PrevURL)
}
if pagination.HasNext && strings.TrimSpace(pagination.NextURL) != "" {
data["paginationNextURL"] = seoser.AbsURL(siteURL, pagination.NextURL)
}
}
// HTMLOK 200 快捷方法。
func HTMLOK(c *gin.Context, name string, data gin.H) {
HTML(c, http.StatusOK, name, data)
}
// setNoIndex 统一设置页面级索引策略,并让公共渲染层同步关闭 JSON-LD。
func setNoIndex(data gin.H, follow bool) {
if data == nil {
return
}
data["noindex"] = true
if follow {
data["robots"] = "noindex,follow"
return
}
data["robots"] = "noindex,nofollow"
}
func pageNoIndex(data gin.H) bool {
if noindex, _ := data["noindex"].(bool); noindex {
return true
}
robots, _ := data["robots"].(string)
for _, directive := range strings.Split(strings.ToLower(robots), ",") {
if strings.TrimSpace(directive) == "noindex" {
return true
}
}
return false
}
func toInt64(v any) int64 {
switch n := v.(type) {
case int64:
return n
case int:
return int64(n)
case int32:
return int64(n)
case float64:
return int64(n)
case string:
i, _ := strconv.ParseInt(strings.TrimSpace(n), 10, 64)
return i
default:
return 0
}
}
func toInt(v any) int {
return int(toInt64(v))
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}