移除旧版 HTML 模板与兼容层,并完善私信、举报、媒体存储与 SEO。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
121
service/seo.go
Normal file
121
service/seo.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.iioio.com/freefire/jiang13-forum/model"
|
||||
)
|
||||
|
||||
var (
|
||||
imgSrcRe = regexp.MustCompile(`(?i)<img[^>]+src=["']([^"']+)["']`)
|
||||
)
|
||||
|
||||
// SitemapURL 站点地图条目
|
||||
type SitemapURL struct {
|
||||
Loc string
|
||||
LastMod time.Time
|
||||
ChangeFreq string
|
||||
Priority string
|
||||
}
|
||||
|
||||
// SitePublicBaseURL 公开站点根地址(无尾斜杠)
|
||||
// 优先 OIDC / 配置中的 RootURL,否则根据请求 Host 推断
|
||||
func (s *ForumSettingsService) SitePublicBaseURL(cfgRoot, requestOrigin string) string {
|
||||
root := normalizeRootURL(s.getString(SettingOIDCRootURL, ""))
|
||||
if root == "" {
|
||||
root = normalizeRootURL(cfgRoot)
|
||||
}
|
||||
if root == "" {
|
||||
root = normalizeRootURL(requestOrigin)
|
||||
}
|
||||
return strings.TrimRight(root, "/")
|
||||
}
|
||||
|
||||
// AbsoluteURL 将相对路径拼成绝对 URL
|
||||
func AbsoluteURL(base, pathOrURL string) string {
|
||||
pathOrURL = strings.TrimSpace(pathOrURL)
|
||||
if pathOrURL == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://") {
|
||||
return pathOrURL
|
||||
}
|
||||
base = strings.TrimRight(base, "/")
|
||||
if !strings.HasPrefix(pathOrURL, "/") {
|
||||
pathOrURL = "/" + pathOrURL
|
||||
}
|
||||
return base + pathOrURL
|
||||
}
|
||||
|
||||
// TruncateRunes 按 rune 截断并加省略号
|
||||
func TruncateRunes(s string, max int) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if max <= 0 || s == "" {
|
||||
return s
|
||||
}
|
||||
if utf8.RuneCountInString(s) <= max {
|
||||
return s
|
||||
}
|
||||
runes := []rune(s)
|
||||
if max < 2 {
|
||||
return string(runes[:max])
|
||||
}
|
||||
return string(runes[:max-1]) + "…"
|
||||
}
|
||||
|
||||
// ExcerptFromHTML 从 HTML 生成摘要(剥离标签)
|
||||
func ExcerptFromHTML(htmlContent string, maxRunes int) string {
|
||||
plain := StripHTMLForSearch(htmlContent)
|
||||
return TruncateRunes(plain, maxRunes)
|
||||
}
|
||||
|
||||
// FirstImageURL 提取正文中第一张图片的 src
|
||||
func FirstImageURL(htmlContent string) string {
|
||||
m := imgSrcRe.FindStringSubmatch(htmlContent)
|
||||
if len(m) < 2 {
|
||||
return ""
|
||||
}
|
||||
src := strings.TrimSpace(m[1])
|
||||
// 忽略 data: 内联图
|
||||
if strings.HasPrefix(src, "data:") {
|
||||
return ""
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// DisplayName 用户展示名
|
||||
func DisplayName(u *model.User) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
if n := strings.TrimSpace(u.Nickname); n != "" {
|
||||
return n
|
||||
}
|
||||
return strings.TrimSpace(u.Username)
|
||||
}
|
||||
|
||||
// QueryBoardHome 板块首页相对路径
|
||||
func QueryBoardHome(boardID uint) string {
|
||||
if boardID == 0 {
|
||||
return "/"
|
||||
}
|
||||
return "/?board=" + url.QueryEscape(itoaUint(boardID))
|
||||
}
|
||||
|
||||
func itoaUint(n uint) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
Reference in New Issue
Block a user