package seoser import ( "net/http" "net/url" "strconv" "strings" "videoweb/app/service/configser" "github.com/gin-gonic/gin" ) // ResolveSiteURL 解析对外站点根地址(无尾斜杠)。 // 优先有效正式域名;跳过占位域名后回退到当前请求 Host。 func ResolveSiteURL(c *gin.Context, cfg configser.AppConfigVM) string { candidates := []string{ cfg.SeoInfo.CanonicalURL, cfg.ForeverURLAddr, cfg.NewURLAddr, } for _, raw := range candidates { if u := usableSiteURL(raw); u != "" { return u } } if c != nil && c.Request != nil { return requestOrigin(c.Request) } return "" } 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 根路径;其他页用 siteURL + 当前 path + query。 // 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 ensureTrailingSlash(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 路由。 func ContentCanonicalURL(siteURL, fallback string, data map[string]any) string { if detail, ok := asPlaylet(data["detail"]); ok { if path := playletPath("/detail", detail.Slug, detail.ID); path != "" { return AbsURL(siteURL, path) } } else if chapter, ok := asChapter(data["chapter"]); ok { playlet, _ := asPlaylet(data["playlet"]) playletID := chapter.PlayletID playletSlug := "" if playlet != nil { playletID = playlet.ID playletSlug = playlet.Slug } path := playletPath("/play", playletSlug, playletID) if path == "" { return fallback } if chapter.ID > 0 { path += strconv.FormatInt(chapter.ID, 10) + "/" } return AbsURL(siteURL, path) } return ensureTrailingSlash(fallback) } // ensureTrailingSlash 将 HTML 页面 URL 统一为目录式尾斜杠形式,并保留查询参数。 func ensureTrailingSlash(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 != "/" && !strings.HasSuffix(u.Path, "/") { 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, "/") }