diff --git a/README.md b/README.md index 013c69d..924a9cd 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,31 @@ NAVI_SSH_PASS='<密码>' bash deploy/zd-host.sh v50 - 配置:`portal/nginx.conf.bak-<时间戳>`;`conf.yml` 的改动另有 `conf.yml.bak-*` - 数据:`conf.yml`、`history.json`、`gateway.yml`、`portal-auth.json` 都在数据卷,容器重建不丢 +### 认证页与门户页文案(可配置,不写死) + +原先登录页与门户页的标题/副标题/徽标/页脚都硬编码在代码与镜像里。现在统一从 `conf.yml` +的 `pageInfo` 读取,**留空即回落内置默认值**(不配置时行为与之前一致): + +| 字段 | 作用 | 默认 | +| --- | --- | --- | +| `authTitle` / `authSubtitle` / `authLogo` | 登录页标题、副标题、徽标文字 | 统一门户 / 登录后访问受保护的内部服务 / JF | +| `portalTitle` | 门户页浏览器标题 | JianF Git - 个人开发环境 | +| `heroTitle` / `heroSubtitle` | 门户页主标题、副标题 | 代码、博客与灵感之站 / … | +| `icp` / `gongan` | 两页页脚备案号(与导航页共用) | — | + +入口:管理后台 →「站点信息」→「认证页与门户页文案」。 + +**整页完全自定义**:把 `index.html` 放进数据卷的 `portal/` 目录即生效(优先于上述字段, +用于改版式而非只改文案);`portal/` 下的 `portal.css`、favicon 等静态资源同样可覆盖。 +门户首页由 admin-server 渲染(`/portal-page`),静态资源仍由 nginx 提供。 + +自检(发布前确认占位符已被替换、页面无残留标记): + +```bash +curl -s https://<域名>/ | grep -c '", html.EscapeString(pageStr(info, "portalTitle", defaultPortalTitle)), + "", html.EscapeString(pageStr(info, "heroTitle", defaultPortalHero)), + "", html.EscapeString(pageStr(info, "heroSubtitle", defaultPortalHeroSub)), + "", beianFooter(mapString(info, "icp"), mapString(info, "gongan")), + ) + out := rep.Replace(string(builtin)) + // 未做占位替换的旧模板(例如自备的 index.html)原样返回 + return []byte(out) +} + +// handlePortalPage 供 nginx 的 kind=portal 路由反代使用。 +func handlePortalPage(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + _, _ = w.Write(renderPortalPage()) +} + +// authPageInfo 返回登录页所需字段(供 auth.go 渲染)。 +func authPageInfo() (title, subtitle, logo, icp, gongan string) { + cfg, err := loadConfig() + info := map[string]any{} + if err == nil && cfg != nil { + info = cfg.PageInfo + } + return pageStr(info, "authTitle", defaultAuthTitle), + pageStr(info, "authSubtitle", defaultAuthSubtitle), + pageStr(info, "authLogo", defaultAuthLogo), + mapString(info, "icp"), mapString(info, "gongan") +} diff --git a/admin-server/pages_test.go b/admin-server/pages_test.go new file mode 100644 index 0000000..2883e31 --- /dev/null +++ b/admin-server/pages_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// 未配置任何文案时应回落到内置默认值(不配置也照旧)。 +func TestPageStrFallbacks(t *testing.T) { + empty := map[string]any{} + if got := pageStr(empty, "authTitle", defaultAuthTitle); got != defaultAuthTitle { + t.Fatalf("空配置应回落默认,得到 %q", got) + } + info := map[string]any{"authTitle": " 内网入口 ", "heroTitle": ""} + if got := pageStr(info, "authTitle", defaultAuthTitle); got != "内网入口" { + t.Fatalf("应取配置值并去空白,得到 %q", got) + } + // 空串视为未配置 + if got := pageStr(info, "heroTitle", defaultPortalHero); got != defaultPortalHero { + t.Fatalf("空串应回落默认,得到 %q", got) + } +} + +// 认证页字段从 pageInfo 读取,缺失时回落。 +func TestAuthPageInfoFallback(t *testing.T) { + dir := t.TempDir() + old := confFile + confFile = filepath.Join(dir, "conf.yml") + defer func() { confFile = old }() + if err := os.WriteFile(confFile, []byte("pageInfo:\n authTitle: 我的门户\n authLogo: AB\n"), 0o644); err != nil { + t.Fatal(err) + } + title, subtitle, logo, _, _ := authPageInfo() + if title != "我的门户" || logo != "AB" { + t.Fatalf("未读到配置值: %q %q", title, logo) + } + if subtitle != defaultAuthSubtitle { + t.Fatalf("未配置的字段应回落默认,得到 %q", subtitle) + } +} + +// 数据卷里的整页覆盖优先于内置模板。 +func TestPortalOverrideWins(t *testing.T) { + dir := t.TempDir() + old := confFile + confFile = filepath.Join(dir, "conf.yml") + defer func() { confFile = old }() + if err := os.MkdirAll(filepath.Join(dir, "portal"), 0o755); err != nil { + t.Fatal(err) + } + custom := "
整页完全自定义:把 index.html 放进数据卷的 portal/ 目录即生效(优先于以上字段)。
JianF Git · 代码托管 · 技术博客 · 私有服务入口
+ + - +