601 lines
21 KiB
Go
601 lines
21 KiB
Go
package web
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"videoweb/app/service/configser"
|
||
"videoweb/app/service/playletser"
|
||
"videoweb/app/service/seoser"
|
||
"videoweb/app/service/translationser"
|
||
"videoweb/app/service/videoser"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// HTML 渲染页面并注入公共导航、配置与 SEO 数据。
|
||
func HTML(c *gin.Context, code int, name string, data gin.H) {
|
||
if data == nil {
|
||
data = gin.H{}
|
||
}
|
||
|
||
// 导航分类:GET /api/app/translation/category/list → categoryList(包含分类 seoInfo)
|
||
if _, ok := data["categoryList"]; !ok {
|
||
data["categoryList"] = translationser.FetchHeaderCategoryListOrDefault(c)
|
||
}
|
||
if _, ok := data["requestPath"]; !ok {
|
||
data["requestPath"] = c.Request.URL.Path
|
||
}
|
||
// 抽屉题材:GET /api/app/theme/list
|
||
if _, ok := data["themeList"]; !ok {
|
||
data["themeList"] = playletser.FetchThemesOrEmpty(c, 0)
|
||
}
|
||
// 顶栏短剧下拉快捷入口:homeConfigData type=1
|
||
if _, ok := data["homeConfigPlaylets"]; !ok {
|
||
if configs, ok := data["homeConfigData"].([]playletser.HomeConfigData); ok {
|
||
data["homeConfigPlaylets"] = playletser.HomeConfigsByType(configs, 1)
|
||
} else {
|
||
home := playletser.FetchHomeOrEmpty(c)
|
||
data["homeConfigPlaylets"] = playletser.HomeConfigsByType(home.HomeConfigData, 1)
|
||
}
|
||
}
|
||
|
||
// PC 搜索下拉热搜:GET /api/app/playlet/hot-like
|
||
if _, ok := data["searchTrends"]; !ok {
|
||
trends := hotSearchTrends(playletser.FetchHotLikeOrEmpty(c).HotSearch)
|
||
if len(trends) > 8 {
|
||
trends = trends[:8]
|
||
}
|
||
data["searchTrends"] = trends
|
||
}
|
||
|
||
// 公共配置:GET /api/app/config/info → config / appConfig
|
||
if _, ok := data["appConfig"]; !ok {
|
||
vm := configser.FetchAppConfigVMOrEmpty(c)
|
||
data["appConfig"] = vm
|
||
if _, ok := data["config"]; !ok {
|
||
data["config"] = vm.Config
|
||
}
|
||
} else if _, ok := data["config"]; !ok {
|
||
if vm, ok := data["appConfig"].(configser.AppConfigVM); ok {
|
||
data["config"] = vm.Config
|
||
}
|
||
}
|
||
|
||
attachPageSEO(c, code, data)
|
||
c.HTML(code, name, data)
|
||
}
|
||
|
||
// HTMLOK 以 200 渲染页面。
|
||
func HTMLOK(c *gin.Context, name string, data gin.H) {
|
||
if pageRepresentsMissingResource(c, data) {
|
||
NotFound(c)
|
||
return
|
||
}
|
||
HTML(c, http.StatusOK, name, data)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
// attachPageSEO 在一个入口统一生成页面级 SEO 信号,避免各模板各自维护后产生冲突。
|
||
func attachPageSEO(c *gin.Context, code int, data gin.H) {
|
||
if c == nil || c.Request == nil {
|
||
return
|
||
}
|
||
|
||
cfg, ok := data["appConfig"].(configser.AppConfigVM)
|
||
if !ok {
|
||
cfg = configser.FetchAppConfigVMOrEmpty(c)
|
||
data["appConfig"] = cfg
|
||
}
|
||
path := c.Request.URL.Path
|
||
pageTitle, _ := data["title"].(string)
|
||
pageTitle = strings.TrimSpace(pageTitle)
|
||
|
||
seoInfo := pageSEOFromData(data["seoInfo"])
|
||
siteName := firstNonEmptySEO(seoInfo.WebSiteName, cfg.SeoInfo.WebSiteName, "黄果短剧")
|
||
routeConfigSEO, _ := data["routeConfigSEO"].(bool)
|
||
if !routeConfigSEO {
|
||
seoInfo = replaceSitewideSEOWithRouteSEO(path, data, seoInfo, pageSEOFromData(cfg.SeoInfo), siteName)
|
||
}
|
||
navigationSEOApplied := false
|
||
if categories, ok := data["categoryList"].([]translationser.TranslationCategory); ok {
|
||
headerNav := translationser.BuildHeaderNav(&translationser.AppTranslationInfo{CategoryList: categories})
|
||
activeKey, _ := data["navActive"].(string)
|
||
if navSEO, found := translationser.FindHeaderNavSEOByPath(headerNav, path); found {
|
||
// 与 jha-1040-cgdj-web 一致:导航分类页面以分类接口 TDK 为主,页面接口只补缺。
|
||
seoInfo = playletser.MergeSeoInfo(playletser.SeoInfo{
|
||
Title: navSEO.Title,
|
||
Description: navSEO.Description,
|
||
Keywords: navSEO.Keyworld,
|
||
}, seoInfo)
|
||
navigationSEOApplied = true
|
||
} else if !routeConfigSEO {
|
||
// 当前项目的详情/播放页会复用 navActive;这类页面保持内容 TDK 为主,导航只补缺。
|
||
if navSEO, found := translationser.FindHeaderNavSEO(headerNav, activeKey, path); found {
|
||
seoInfo = playletser.MergeSeoInfo(seoInfo, playletser.SeoInfo{
|
||
Title: navSEO.Title,
|
||
Description: navSEO.Description,
|
||
Keywords: navSEO.Keyworld,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
if selections, ok := data["seoSelections"].([]playletser.SEOSelection); ok && len(selections) > 0 {
|
||
seoInfo = playletser.DifferentiateSeoInfo(seoInfo, selections)
|
||
}
|
||
if seoInfo.Title == "" {
|
||
seoInfo.Title = firstNonEmptySEO(pageTitle, defaultPageTitle(path, siteName))
|
||
}
|
||
if seoInfo.Description == "" {
|
||
seoInfo.Description = defaultMetaDescription(path, seoInfo.Title, data, siteName)
|
||
}
|
||
if len(seoInfo.Keywords) == 0 {
|
||
seoInfo.Keywords = defaultMetaKeywords(path, siteName)
|
||
}
|
||
if pageNum := paginationPage(data["pagination"]); pageNum > 1 {
|
||
seoInfo = playletser.PaginateSeoInfo(seoInfo, seoInfo.Description, pageNum)
|
||
}
|
||
metaTitle := seoInfo.Title
|
||
if !navigationSEOApplied {
|
||
metaTitle = ensureBrandTitle(metaTitle, siteName)
|
||
}
|
||
data["title"] = metaTitle
|
||
data["metaTitle"] = metaTitle
|
||
data["metaDescription"] = seoInfo.Description
|
||
data["metaKeywords"] = strings.Join(seoInfo.Keywords, ",")
|
||
data["siteName"] = siteName
|
||
data["seoInfo"] = seoInfo
|
||
|
||
siteURL := seoser.ResolveSiteURL(c, cfg)
|
||
data["siteURL"] = siteURL
|
||
data["foreverUrlAddr"] = siteURL
|
||
data["seoVerify"] = seoser.VerifyFromConfig()
|
||
data["ogType"] = pageOGType(path)
|
||
data["ogImage"] = pageOGImage(siteURL, path, data)
|
||
attachPaginationLinks(data, siteURL)
|
||
|
||
robots, _ := data["robots"].(string)
|
||
robots = strings.TrimSpace(robots)
|
||
if robots == "" {
|
||
robots = defaultRobotsForPath(path, code)
|
||
}
|
||
if !robotsHasNoIndex(robots) && pageShouldNotIndex(c, data) {
|
||
robots = "noindex,follow"
|
||
}
|
||
data["robots"] = robots
|
||
noindex := robotsHasNoIndex(robots)
|
||
if noindex {
|
||
data["noindex"] = true
|
||
c.Header("X-Robots-Tag", strings.ReplaceAll(robots, ",", ", "))
|
||
} else {
|
||
delete(data, "noindex")
|
||
}
|
||
|
||
canonicalEligible := code >= http.StatusOK && code < http.StatusMultipleChoices && !noindex && seoser.IsIndexablePath(path)
|
||
if canonicalEligible {
|
||
canonicalURL := seoser.CanonicalWithoutQuery(seoser.PageCanonicalURL(siteURL, cfg.ForeverURLAddr, c))
|
||
if canonicalPath, _ := data["canonicalPath"].(string); strings.HasPrefix(strings.TrimSpace(canonicalPath), "/") {
|
||
canonicalURL = seoser.CanonicalWithoutQuery(seoser.AbsURL(siteURL, strings.TrimSpace(canonicalPath)))
|
||
}
|
||
canonicalURL = seoser.CanonicalWithoutQuery(seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data)))
|
||
data["canonicalURL"] = canonicalURL
|
||
data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg)
|
||
} else {
|
||
delete(data, "canonicalURL")
|
||
delete(data, "jsonLD")
|
||
}
|
||
}
|
||
|
||
func pageShouldNotIndex(c *gin.Context, data gin.H) bool {
|
||
if isNavPage, _ := data["isNavPage"].(bool); isNavPage {
|
||
return true
|
||
}
|
||
if c != nil && c.Request != nil {
|
||
path := c.Request.URL.Path
|
||
if isEpisodePlayPath(path, c.Param("ep")) {
|
||
return true
|
||
}
|
||
if strings.HasPrefix(path, "/series-play/") {
|
||
if canWatch, ok := data["canWatch"].(bool); ok && !canWatch {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return pageRepresentsMissingResource(c, data)
|
||
}
|
||
|
||
// pageRepresentsMissingResource 识别不存在的实体、非法分页与越界分页。
|
||
// 这些请求必须返回真实 404,不能仅依赖 noindex 掩盖 200 软 404。
|
||
func pageRepresentsMissingResource(c *gin.Context, data gin.H) bool {
|
||
if valid, exists := data["seoEntityValid"].(bool); exists && !valid {
|
||
return true
|
||
}
|
||
pageNum := int(toInt64(data["pageNum"]))
|
||
if c != nil {
|
||
if rawPage := strings.TrimSpace(c.Param("page")); rawPage != "" {
|
||
requestedPage, err := strconv.Atoi(rawPage)
|
||
// 分页二级路由只允许第 2 页及以后;第 1 页使用更短的规范地址。
|
||
if err != nil || requestedPage <= 1 {
|
||
return true
|
||
}
|
||
pageNum = requestedPage
|
||
}
|
||
}
|
||
pageSize := int(toInt64(data["pageSize"]))
|
||
totalCount, hasTotal := data["totalCount"]
|
||
return hasTotal && playletser.PageOutOfRange(pageNum, pageSize, toInt64(totalCount))
|
||
}
|
||
|
||
func pageSEOFromData(value any) playletser.SeoInfo {
|
||
var seo playletser.SeoInfo
|
||
switch value := value.(type) {
|
||
case playletser.SeoInfo:
|
||
seo = value
|
||
case *playletser.SeoInfo:
|
||
if value != nil {
|
||
seo = *value
|
||
}
|
||
case videoser.SeoInfo:
|
||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||
case *videoser.SeoInfo:
|
||
if value != nil {
|
||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||
}
|
||
case configser.SeoInfo:
|
||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||
case *configser.SeoInfo:
|
||
if value != nil {
|
||
seo = playletser.SeoInfo{Title: value.Title, Description: value.Description, Keywords: value.Keywords, CanonicalURL: value.CanonicalURL, WebSiteName: value.WebSiteName}
|
||
}
|
||
}
|
||
seo.Normalize()
|
||
return seo
|
||
}
|
||
|
||
// replaceSitewideSEOWithRouteSEO 只替换接口误复用的全站首页 TDK。
|
||
// 页面接口真正下发的独立 SEO 仍保持最高优先级。
|
||
func replaceSitewideSEOWithRouteSEO(path string, data gin.H, seo, siteDefault playletser.SeoInfo, siteName string) playletser.SeoInfo {
|
||
routeSEO, ok := routeSpecificSEO(path, data, siteName)
|
||
if !ok {
|
||
return seo
|
||
}
|
||
if sameNonEmptySEOValue(seo.Title, siteDefault.Title) {
|
||
seo.Title = routeSEO.Title
|
||
}
|
||
if sameNonEmptySEOValue(seo.Description, siteDefault.Description) {
|
||
seo.Description = routeSEO.Description
|
||
}
|
||
if sameSEOKeywords(seo.Keywords, siteDefault.Keywords) {
|
||
seo.Keywords = append([]string(nil), routeSEO.Keywords...)
|
||
}
|
||
seo.Normalize()
|
||
return seo
|
||
}
|
||
|
||
func isEpisodePlayPath(path, epParam string) bool {
|
||
if strings.TrimSpace(epParam) != "" {
|
||
return true
|
||
}
|
||
segments := strings.Split(strings.Trim(path, "/"), "/")
|
||
return len(segments) >= 3 && segments[0] == "play" && segments[1] != "" && segments[2] != ""
|
||
}
|
||
|
||
func routeSpecificSEO(path string, data gin.H, siteName string) (playletser.SeoInfo, bool) {
|
||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||
title := ""
|
||
switch {
|
||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||
title = firstNonEmptySEO(dataString(data, "pageHeading"), "全部短剧")
|
||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||
title = firstNonEmptySEO(dataString(data, "pageHeading"), "剧集与动画")
|
||
case path == "/topic" || strings.HasPrefix(path, "/topic/"):
|
||
title = "专题剧单"
|
||
case strings.HasPrefix(path, "/topic-detail/"):
|
||
if name := dataString(data, "name"); name != "" {
|
||
title = name + "专题短剧"
|
||
} else {
|
||
title = "短剧专题"
|
||
}
|
||
case strings.HasPrefix(path, "/tags/"):
|
||
if name := dataString(data, "tagName"); name != "" {
|
||
title = name + "标签短剧"
|
||
} else {
|
||
title = "标签短剧"
|
||
}
|
||
case strings.HasPrefix(path, "/more-list/"):
|
||
title = firstNonEmptySEO(dataString(data, "configName"), "精选短剧")
|
||
case strings.HasPrefix(path, "/more-series-list/"):
|
||
title = firstNonEmptySEO(dataString(data, "configName"), "精选剧集")
|
||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||
title = "短剧排行榜"
|
||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||
title = "热门短剧推荐"
|
||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||
title = "最近上新短剧"
|
||
default:
|
||
return playletser.SeoInfo{}, false
|
||
}
|
||
return playletser.SeoInfo{
|
||
Title: title,
|
||
Description: defaultMetaDescription(path, title, data, siteName),
|
||
Keywords: defaultMetaKeywords(path, siteName),
|
||
}, true
|
||
}
|
||
|
||
func dataString(data gin.H, key string) string {
|
||
value, _ := data[key].(string)
|
||
return strings.TrimSpace(value)
|
||
}
|
||
|
||
func sameNonEmptySEOValue(value, generic string) bool {
|
||
value = strings.TrimSpace(value)
|
||
generic = strings.TrimSpace(generic)
|
||
return generic != "" && strings.EqualFold(value, generic)
|
||
}
|
||
|
||
func sameSEOKeywords(value, generic []string) bool {
|
||
if len(generic) == 0 || len(value) != len(generic) {
|
||
return false
|
||
}
|
||
for i := range value {
|
||
if !strings.EqualFold(strings.TrimSpace(value[i]), strings.TrimSpace(generic[i])) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func paginationPage(value any) int {
|
||
switch value := value.(type) {
|
||
case playletser.PaginationVM:
|
||
return value.Page
|
||
case *playletser.PaginationVM:
|
||
if value != nil {
|
||
return value.Page
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
func defaultRobotsForPath(path string, code int) string {
|
||
if code >= http.StatusBadRequest {
|
||
return "noindex,follow"
|
||
}
|
||
if seoser.IsIndexablePath(path) {
|
||
return "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1"
|
||
}
|
||
if seoser.IsPrivatePath(path) {
|
||
return "noindex,nofollow"
|
||
}
|
||
return "noindex,follow"
|
||
}
|
||
|
||
func robotsHasNoIndex(robots string) bool {
|
||
for _, directive := range strings.Split(strings.ToLower(robots), ",") {
|
||
if strings.TrimSpace(directive) == "noindex" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func defaultPageTitle(path, siteName string) string {
|
||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||
if siteName == "" {
|
||
siteName = "黄果短剧"
|
||
}
|
||
switch {
|
||
case path == "":
|
||
return siteName + " - 热门短剧在线看"
|
||
case path == "/about":
|
||
return "关于我们"
|
||
case path == "/nav" || path == "/dh":
|
||
return siteName + " · 官方导航"
|
||
case path == "/help":
|
||
return "常见问题"
|
||
case path == "/terms":
|
||
return "用户条款"
|
||
case path == "/privacy":
|
||
return "隐私政策"
|
||
case path == "/2257":
|
||
return "2257 合规声明"
|
||
case path == "/dmca":
|
||
return "DMCA 版权声明"
|
||
case path == "/search" || strings.HasPrefix(path, "/search/") || path == "/search-mobile":
|
||
return "搜索"
|
||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||
return "全部短剧"
|
||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||
return "剧集与动画"
|
||
case path == "/topic" || strings.HasPrefix(path, "/topic/"):
|
||
return "专题剧单"
|
||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||
return "短剧排行榜"
|
||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||
return "最近上新"
|
||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||
return "热门推荐"
|
||
case strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/"):
|
||
return "在线播放"
|
||
default:
|
||
return siteName
|
||
}
|
||
}
|
||
|
||
func defaultMetaDescription(path, title string, data gin.H, siteName string) string {
|
||
for _, key := range []string{"description", "desc", "heroDesc", "categoryDescription", "tagDescription", "topicDescription"} {
|
||
if value, _ := data[key].(string); strings.TrimSpace(value) != "" {
|
||
return strings.TrimSpace(value)
|
||
}
|
||
}
|
||
if siteName == "" {
|
||
siteName = "黄果短剧"
|
||
}
|
||
cleanTitle := strings.TrimSpace(strings.TrimSuffix(title, " - "+siteName))
|
||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||
if isNavPage, _ := data["isNavPage"].(bool); isNavPage {
|
||
return siteName + "官方导航页,提供最新访问线路、永久地址、APP 下载与官方联系方式。"
|
||
}
|
||
switch {
|
||
case path == "":
|
||
return siteName + "汇集热门短剧、最近上新、排行榜与精选专题推荐,支持高清在线播放。"
|
||
case path == "/about":
|
||
return "了解" + siteName + "的平台理念、内容特色与联系方式。"
|
||
case path == "/nav" || path == "/dh":
|
||
return siteName + "官方导航页,提供最新访问线路、永久地址、APP 下载与官方联系方式。"
|
||
case path == "/help":
|
||
return "查看" + siteName + "常见问题、播放帮助、账号使用与客服支持信息。"
|
||
case path == "/terms":
|
||
return "查看" + siteName + "用户服务条款及平台使用规则。"
|
||
case path == "/privacy":
|
||
return "查看" + siteName + "隐私政策与个人信息保护说明。"
|
||
case path == "/2257":
|
||
return "查看" + siteName + " 18 U.S.C. 第 2257 章合规声明、年龄限制与内容反馈处理原则。"
|
||
case path == "/dmca":
|
||
return "查看" + siteName + " DMCA 版权声明、投诉材料要求与处理流程说明。"
|
||
case path == "/search" || strings.HasPrefix(path, "/search/") || path == "/search-mobile":
|
||
return "在" + siteName + "搜索剧名、演员、标签与相关作品。"
|
||
case strings.HasPrefix(path, "/play/"):
|
||
return "在线观看《" + cleanTitle + "》,查看选集、剧情信息与相关推荐。"
|
||
case strings.HasPrefix(path, "/series-play/"):
|
||
return "在线观看《" + cleanTitle + "》,浏览视频信息与相关推荐。"
|
||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||
return "浏览" + siteName + "全部短剧,按题材、更新状态和热度筛选精彩作品。"
|
||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||
return "浏览" + siteName + "精选剧集与动画,按类型、专题和热度发现更多内容。"
|
||
case path == "/topic" || strings.HasPrefix(path, "/topic/") || strings.HasPrefix(path, "/topic-detail/"):
|
||
return "浏览" + siteName + "精选专题剧单,发现同题材热门作品与持续更新内容。"
|
||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||
return "查看" + siteName + "实时更新的短剧排行榜,浏览热播榜、新剧榜与追剧榜。"
|
||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||
return "浏览" + siteName + "热门推荐,发现近期高热度、广受欢迎的精选短剧。"
|
||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||
return "查看" + siteName + "最近上新的短剧与剧集,及时追更最新内容。"
|
||
default:
|
||
return cleanTitle + ",浏览" + siteName + "相关内容与推荐。"
|
||
}
|
||
}
|
||
|
||
func defaultMetaKeywords(path, siteName string) []string {
|
||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||
if siteName == "" {
|
||
siteName = "黄果短剧"
|
||
}
|
||
switch {
|
||
case path == "":
|
||
return []string{siteName, "短剧", "在线短剧", "热门短剧"}
|
||
case path == "/list" || strings.HasPrefix(path, "/list/"):
|
||
return []string{"短剧列表", "短剧分类", "在线短剧", siteName}
|
||
case path == "/series" || strings.HasPrefix(path, "/series/"):
|
||
return []string{"剧集", "动画", "在线观看", siteName}
|
||
case strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/"):
|
||
return []string{"在线播放", "短剧播放", siteName}
|
||
case path == "/topic" || strings.HasPrefix(path, "/topic/") || strings.HasPrefix(path, "/topic-detail/"):
|
||
return []string{"短剧专题", "专题剧单", siteName}
|
||
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
||
return []string{"短剧排行榜", "热门短剧", siteName}
|
||
case path == "/popular-recommend" || strings.HasPrefix(path, "/popular-recommend/"):
|
||
return []string{"热门短剧", "短剧推荐", siteName}
|
||
case path == "/recently-added" || strings.HasPrefix(path, "/recently-added/"):
|
||
return []string{"最新短剧", "最近上新", siteName}
|
||
case strings.HasPrefix(path, "/tags/"):
|
||
return []string{"标签短剧", "短剧分类", siteName}
|
||
case strings.HasPrefix(path, "/more-list/"):
|
||
return []string{"精选短剧", "热门短剧", siteName}
|
||
case strings.HasPrefix(path, "/more-series-list/"):
|
||
return []string{"精选剧集", "剧集推荐", siteName}
|
||
default:
|
||
return []string{siteName, "在线短剧"}
|
||
}
|
||
}
|
||
|
||
func ensureBrandTitle(title, siteName string) string {
|
||
title = strings.TrimSpace(title)
|
||
siteName = strings.TrimSpace(siteName)
|
||
if title == "" {
|
||
return siteName
|
||
}
|
||
if siteName == "" || strings.Contains(title, siteName) || strings.Contains(title, "黄果短剧") {
|
||
return title
|
||
}
|
||
return title + " - " + siteName
|
||
}
|
||
|
||
func pageOGType(path string) string {
|
||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||
if strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/") {
|
||
return "video.episode"
|
||
}
|
||
if path == "/terms" || path == "/privacy" || path == "/2257" || path == "/dmca" {
|
||
return "article"
|
||
}
|
||
return "website"
|
||
}
|
||
|
||
func pageOGImage(siteURL string, path string, data gin.H) string {
|
||
path = strings.TrimRight(strings.TrimSpace(path), "/")
|
||
// 播放页分享小卡片统一用品牌 OG 图,避免微信等抓取剧集封面。
|
||
if strings.HasPrefix(path, "/play/") || strings.HasPrefix(path, "/series-play/") {
|
||
return seoser.AbsURL(siteURL, "/static/og-default.png")
|
||
}
|
||
if playlet, ok := data["playlet"].(*playletser.PlayletInfoResp); ok && playlet != nil {
|
||
if cover := firstNonEmptySEO(playlet.Cover, playlet.Banner); cover != "" {
|
||
return seoser.AbsURL(siteURL, cover)
|
||
}
|
||
}
|
||
if cover, _ := data["cover"].(string); strings.TrimSpace(cover) != "" {
|
||
return seoser.AbsURL(siteURL, cover)
|
||
}
|
||
return seoser.AbsURL(siteURL, "/static/og-default.png")
|
||
}
|
||
|
||
func firstNonEmptySEO(values ...string) string {
|
||
for _, value := range values {
|
||
if value = strings.TrimSpace(value); value != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|