Files
jiang13-forum/service/seo.go
freefire 2392768868 feat(ssr): 真 SSR 收尾——干净 hydration 与浏览器原生滚动恢复
- 修复 onRenderClient 引入 serverClient 导致浏览器 process is not defined:
  抽出双端安全的 ssr/feedSeed.ts 作为唯一数据播种入口,serverClient 加 SSR 守卫
- 修复 React 19 Fizz Suspense Outlining:Suspense 边界从布局 Outlet 下移到
  各懒加载路由,SSR 输出不再出现 <!--$?--> / $RB / template 探针标记
- 修复窄屏 React #418 hydration 致命错误:useMediaQuery 首帧恒 false,
  useLayoutEffect 绘制前同步真实值;主题改 cookie 存储供 SSR 直出,
  head 内联防闪烁/缩放脚本移除(缩放锁改为客户端 zoomGuard 模块)
- 清理假 SSR 残留:删除 StaticFeedList 镜像组件、SSR 图标生成脚本、
  探针与死代码;列表接口裁剪正文,仅下发 excerpt/first_image
- Feed 与帖子详情改为窗口滚动(app-shell--doc-scroll):删除虚拟列表、
  自研滚动快照与全局滚轮 hack;F5/前进后退滚动位置全部交给浏览器原生
  历史恢复,HTML 响应头 no-cache 配合;锚点/楼层用 scrollIntoView +
  scroll-margin-top 适配 sticky 顶栏
2026-09-09 20:53:28 +08:00

133 lines
3.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"html"
"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 中的 ROOT_URL,否则根据请求 Host 推断
func (s *ForumSettingsService) SitePublicBaseURL(requestOrigin string) string {
root := normalizeRootURL(s.getString(SettingOIDCRootURL, ""))
if root == "" {
root = normalizeRootURL(requestOrigin)
}
return strings.TrimRight(root, "/")
}
// AbsoluteURL 将相对路径拼成绝对 URL。
// base 为空或非 http(s) 时返回空串,避免邮件等场景出现无法点击的相对路径。
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(strings.TrimSpace(base), "/")
if base == "" || (!strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://")) {
return ""
}
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)
plain = html.UnescapeString(plain)
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, p PermalinkConfig) string {
if boardID == 0 {
return "/"
}
return p.BoardPath(boardID)
}
// LegacyQueryBoardHome 旧版 query 形式(/?board=id),仅用于 301 重定向
func LegacyQueryBoardHome(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:])
}