Files
web_huangguo2_h5/app/service/seoser/site.go
T
谢宇宁andClaude Opus 5.5 0fbdba5ccf Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:47:47 +07:00

227 lines
5.6 KiB
Go

package seoser
import (
"net/http"
"net/url"
"strings"
"videoweb/app/service/configser"
"videoweb/app/service/videoser"
"github.com/gin-gonic/gin"
)
// ResolveSiteURL 解析对外站点根地址(无尾斜杠)。
// 优先有效正式域名;跳过占位域名后回退到当前请求 Host。
func ResolveSiteURL(c *gin.Context, cfg configser.AppConfigVM) string {
if siteURL := PrimarySiteURL(cfg); siteURL != "" {
return siteURL
}
if c != nil && c.Request != nil {
return requestOrigin(c.Request)
}
return ""
}
// PrimarySiteURL 返回配置接口动态下发的永久地址,供 canonical、OG 等站点元数据使用。
// 页面是否允许索引由页面类型和内容状态决定,与请求 Host 是否匹配此地址无关。
func PrimarySiteURL(cfg configser.AppConfigVM) string {
return usableSiteURL(cfg.ForeverURLAddr)
}
func usableSiteURL(raw string) string {
u := sanitizeBase(raw)
if u == "" {
return ""
}
host := strings.ToLower(originHost(u))
if isPlaceholderHost(host) {
return ""
}
return u
}
func originHost(raw string) string {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
return ""
}
return u.Hostname()
}
func isPlaceholderHost(host string) bool {
host = strings.TrimSpace(strings.ToLower(host))
switch host {
case "", "localhost", "127.0.0.1", "0.0.0.0", "www.xx.xxx", "xx.xxx", "example.com", "www.example.com":
return true
}
return strings.HasSuffix(host, ".example.com") || strings.HasSuffix(host, ".local")
}
// AbsURL 将 path 或完整 URL 转为绝对地址。
func AbsURL(siteURL, raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
if siteURL == "" {
return "/"
}
return siteURL + "/"
}
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
return raw
}
if siteURL == "" {
if strings.HasPrefix(raw, "/") {
return raw
}
return "/" + raw
}
if strings.HasPrefix(raw, "/") {
return siteURL + raw
}
return siteURL + "/" + raw
}
// PageCanonicalURL 首页用 siteURL 根路径;其他页使用当前 router 的无尾斜杠规范路径。
// siteURL 为空时回退 foreverURLAddr。
func PageCanonicalURL(siteURL, foreverURLAddr string, c *gin.Context) string {
base := strings.TrimRight(strings.TrimSpace(siteURL), "/")
if base == "" {
base = strings.TrimRight(strings.TrimSpace(usableSiteURL(foreverURLAddr)), "/")
}
if c == nil || c.Request == nil || c.Request.URL == nil {
return AbsURL(base, "/")
}
path := c.Request.URL.Path
if path == "" || path == "/" {
return AbsURL(base, "/")
}
return normalizePageURL(AbsURL(base, relativeRequestURI(c.Request.URL)))
}
// CanonicalWithoutQuery 移除 canonical 中的查询参数与片段,避免参数变体形成重复 URL。
func CanonicalWithoutQuery(raw string) string {
raw = strings.TrimSpace(raw)
u, err := url.Parse(raw)
if err != nil {
return raw
}
u.RawQuery = ""
u.ForceQuery = false
u.Fragment = ""
return u.String()
}
// ContentCanonicalURL 由实际内容生成 slug 路由。
// 短剧规范入口是 /play/{slug},与 sitemap-playletdetail 一致;各集页 noindex,不能标成 canonical。
func ContentCanonicalURL(siteURL, fallback string, data map[string]any) string {
if detail, ok := asPlaylet(data["detail"]); ok {
if path := playletPath("/play", detail.Slug, detail.ID); path != "" {
return AbsURL(siteURL, path)
}
}
if playlet, ok := asPlaylet(data["playlet"]); ok {
if path := playletPath("/play", playlet.Slug, playlet.ID); path != "" {
return AbsURL(siteURL, path)
}
}
if video, ok := asVideo(data["video"]); ok {
if path := videoser.VideoPlayURL(video.Slug, video.ID); path != "/series" {
return AbsURL(siteURL, path)
}
}
if chapter, ok := asChapter(data["chapter"]); ok {
if path := playletPath("/play", "", chapter.PlayletID); path != "" {
return AbsURL(siteURL, path)
}
}
return normalizePageURL(fallback)
}
func asVideo(v any) (*videoser.VideoInfoResp, bool) {
switch value := v.(type) {
case *videoser.VideoInfoResp:
if value != nil {
return value, true
}
case videoser.VideoInfoResp:
return &value, true
}
return nil, false
}
// normalizePageURL 去掉非首页 URL 的尾斜杠,并保留查询参数。
func normalizePageURL(raw string) string {
raw = strings.TrimSpace(raw)
u, err := url.Parse(raw)
if err != nil {
return raw
}
if u.Path == "" {
u.Path = "/"
}
if u.Path != "/" {
u.Path = strings.TrimRight(u.Path, "/")
}
return u.String()
}
func relativeRequestURI(u *url.URL) string {
if u == nil {
return "/"
}
path := u.EscapedPath()
if path == "" {
path = "/"
}
if u.RawQuery != "" {
return path + "?" + u.RawQuery
}
return path
}
func sanitizeBase(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
if !strings.Contains(raw, "://") && strings.Contains(raw, ".") {
return sanitizeBase("https://" + strings.TrimPrefix(raw, "/"))
}
return ""
}
if u.Scheme != "http" && u.Scheme != "https" {
return ""
}
return strings.TrimRight(u.Scheme+"://"+u.Host, "/")
}
func requestOrigin(r *http.Request) string {
if r == nil {
return ""
}
proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))
if proto == "" {
if r.TLS != nil {
proto = "https"
} else {
proto = "http"
}
}
if i := strings.IndexByte(proto, ','); i >= 0 {
proto = strings.TrimSpace(proto[:i])
}
host := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
if host == "" {
host = strings.TrimSpace(r.Host)
}
if i := strings.IndexByte(host, ','); i >= 0 {
host = strings.TrimSpace(host[:i])
}
if host == "" {
return ""
}
return strings.TrimRight(proto+"://"+host, "/")
}