251 lines
6.7 KiB
Go
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 ""
|
|
}
|