@@ -0,0 +1,250 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user