- internal/linkmeta:取正文第一个 http(s) 链接,解析 og: → twitter: → <title>/ description 逐级兜底,解 HTML 实体、相对图片补成绝对地址。按对外抓取设防: 只放行 http(s)、读满 512KB 即停、最多 3 次跳转、整体限时;SSRF 防护做在拨号 那一刻——解析出 IP 后筛掉回环/私网/链路本地/组播/CGNAT 再直连该 IP,堵住 DNS 重绑定窗口 - 字符集转换:按「HTTP 头 charset → <meta charset> → UTF-8」解码,gb2312 归一 成 gbk(超集,按声明解会漏字);认不出的字符名退回原始字节不报错。为此引入 golang.org/x/text(官方包,约 +1MB 二进制)。tidy 顺带把 aws-sdk 三个包从 indirect 修正为直接依赖——storage/r2.go 本来就直接用它们 - posts 加 link_card 列(JSON,沿用 images 的编解码);admin 保存短文时重算: 同链接沿用旧卡片不重复打远端,删链接或抓取失败则清空,长文不参与 - 前端 LinkCard 组件接两套 UI 的时间线与详情页,配色写成 vivid 变量优先、 classic 变量兜底,一份样式两边通用;外链图挂了自动隐藏不留空框 - 卡片用 div[role=link] 而非 <a>(vivid 整条短文包在 RouterLink 里,嵌套 <a> 非法),点击必须 preventDefault——只 stop 挡不住祖先 <a> 的默认激活行为
46 lines
1.4 KiB
Go
46 lines
1.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"oneblog/internal/linkmeta"
|
|
"oneblog/internal/model"
|
|
)
|
|
|
|
// linkFetchBudget 是给外链抓取的时间预算:发布不该被一个慢站点拖住,
|
|
// 超时就当作「这篇没有卡片」。
|
|
const linkFetchBudget = 2500 * time.Millisecond
|
|
|
|
// linkFetch 是要不要抓远端的注入点:默认走带 SSRF 防护的真实抓取,
|
|
// 测试里换成假实现(真实抓取在测试环境会被自己的防护拦掉)。
|
|
var linkFetch = linkmeta.Fetch
|
|
|
|
// attachLinkCard 按正文重算短文链接卡片,结果写进 in.LinkCard。
|
|
//
|
|
// 只处理短文(长文在时间线已有封面媒体位,再加卡片会抢位置);
|
|
// 链接没变就沿用旧卡片,不重复抓远端;正文里删掉链接、或抓取失败,
|
|
// 一律传空卡片把旧的清掉——宁可没有卡片,也不要挂着别的地址抓来的旧数据。
|
|
func attachLinkCard(ctx context.Context, in *model.PostInput, cur *model.LinkCard) {
|
|
if in.Kind != model.KindShort || in.ContentMd == "" {
|
|
return
|
|
}
|
|
u := linkmeta.FirstURL(in.ContentMd)
|
|
if u == "" {
|
|
in.LinkCard = &model.LinkCard{}
|
|
return
|
|
}
|
|
if cur != nil && cur.URL == u {
|
|
in.LinkCard = cur
|
|
return
|
|
}
|
|
cctx, cancel := context.WithTimeout(ctx, linkFetchBudget)
|
|
defer cancel()
|
|
card, err := linkFetch(cctx, u)
|
|
if err != nil {
|
|
in.LinkCard = &model.LinkCard{}
|
|
return
|
|
}
|
|
in.LinkCard = card
|
|
}
|