Files
web_huangguo_web/app/service/seoser/site.go
T
fish03andClaude Opus 5.5 bef9f8b4ed Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:43:16 +07:00

216 lines
5.0 KiB
Go

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, "/")
}