后台:站点设置新增社交链接与自定义 JS 字段

social_links 以 JSON 数组存进 settings KV([{label,url}]),解码容错:
空串 / 坏 JSON / 缺 label 或 url 的条目一律丢弃,前台拿到空数组时区块
自动隐藏。custom_js 原样存取——它是站主自己的代码(统计脚本等),
做转义只会把脚本洗坏。后台表单与前台展示在下一笔前端提交里。
This commit is contained in:
Sakurasan
2026-09-27 03:34:43 +08:00
parent 75737034d9
commit 5dfa1550fb
3 changed files with 54 additions and 2 deletions
+15
View File
@@ -120,6 +120,13 @@ type MonthBucket struct {
Count int `json:"count"`
}
// SocialLink 是站主在后台填写的社交 / 源码入口(label + url),
// 前台渲染成图标或文字链接。icon 不入库 —— 由前端按 url 域名推导。
type SocialLink struct {
Label string `json:"label"`
URL string `json:"url"`
}
type Settings struct {
SiteTitle string `json:"site_title"`
SiteDesc string `json:"site_desc"`
@@ -128,6 +135,9 @@ type Settings struct {
FooterNote string `json:"footer_note"`
ICPLicense string `json:"icp"`
PostsPerPage int `json:"posts_per_page"`
// SocialLinks 以 JSON 数组形式存在 settings KV 里(key: social_links),
// 解析失败/为空时前台拿到空数组,区块自动隐藏。
SocialLinks []SocialLink `json:"social_links"`
// LightSkinID is the front-end skin used when the client (or system)
// prefers light. Valid values: paper / sage / rose.
// Dark side is fixed to ink for now — kept implicit so we can add
@@ -147,4 +157,9 @@ type Settings struct {
// Only injected for the vivid UI, and never on /admin. Always non-nil so
// the JSON response is {} rather than null.
CustomCSS map[string]string `json:"custom_css"`
// CustomJS holds owner-authored JavaScript (analytics snippets like
// Google Analytics / Plausible / Umami). Injected on every public page
// of either UI, never on /admin. Stored raw — it's the owner's own code,
// sanitizing it would only break the snippet.
CustomJS string `json:"custom_js"`
}
+39 -1
View File
@@ -193,12 +193,46 @@ func settingsFromMap(m map[string]string) model.Settings {
st.ThemeID = st.LightSkinID
st.UIID = sanitizeUI(m["ui_id"])
st.CustomCSS = decodeCSSMap(m["custom_css"])
st.CustomJS = m["custom_js"]
st.SocialLinks = decodeSocialLinks(m["social_links"])
if n := atoi(m["posts_per_page"]); n > 0 {
st.PostsPerPage = n
}
return st
}
// decodeSocialLinks 把 KV 里的 JSON 数组还原成社交链接。容错:空串、坏 JSON、
// 缺 label/url 的条目一律丢弃,返回空切片(前台区块自动隐藏)。
func decodeSocialLinks(s string) []model.SocialLink {
out := []model.SocialLink{}
if strings.TrimSpace(s) == "" {
return out
}
if err := json.Unmarshal([]byte(s), &out); err != nil {
return []model.SocialLink{}
}
clean := make([]model.SocialLink, 0, len(out))
for _, l := range out {
l.Label = strings.TrimSpace(l.Label)
l.URL = strings.TrimSpace(l.URL)
if l.Label != "" && l.URL != "" {
clean = append(clean, l)
}
}
return clean
}
func encodeSocialLinks(ls []model.SocialLink) string {
if len(ls) == 0 {
return ""
}
b, err := json.Marshal(ls)
if err != nil {
return ""
}
return string(b)
}
func atoi(v string) int {
n := 0
for _, r := range v {
@@ -341,6 +375,10 @@ func (s *Store) UpdateSettings(st model.Settings) error {
"ui_id": st.UIID,
// Full replace: an omitted/empty map clears every section's CSS.
"custom_css": encodeCSSMap(st.CustomCSS),
// 原样存:站主自己的代码,不做任何转义/清洗。
"custom_js": st.CustomJS,
// 空数组存空串:KV 里不留 "null"。
"social_links": encodeSocialLinks(st.SocialLinks),
}
for k, v := range sets {
if s.db.Dialect == db.Postgres {
@@ -522,7 +560,7 @@ func (s *Store) Get(id int64) (model.Post, error) {
// 返回顺序与时间线一致(published_at DESC):最旧在最前、最新在最后,
// 目标文章夹在中间,前后各取最多 2 篇。
func (s *Store) Neighbors(slug string) ([]model.Post, error) {
rows, err := s.db.Query(s.db.Q(`SELECT ` + listCols + ` FROM posts
rows, err := s.db.Query(s.db.Q(`SELECT `+listCols+` FROM posts
WHERE status = ? ORDER BY published_at DESC`), model.StatusPublished)
if err != nil {
return nil, err
-1
View File
@@ -490,4 +490,3 @@ func TestProjectCRUD(t *testing.T) {
t.Errorf("after delete: got %v, want ErrNotFound", err)
}
}