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 "" }