目标

给 6767 主页(6767.chat)新增一个子页面(如 /ai-tools),视觉与主站一致、带图标、走 https。2026-09-24 以「AI 工具集」落地。

关键事实(结构)

  • 主页静态根目录:/home/ubuntu/workspace/static/6767-home/(nginx location / { root ...; index index.html; },见 /etc/nginx/platform/sites/6767.chat.conf)。
  • 子页 = 根目录下建 <name>/index.html 即可,nginx 自动服务所有子路径,无需改 routes.json / deploy.sh / 任何 nginx 配置(除非要独立路由/鉴权)。
  • 子页复用主站设计系统:<link rel="stylesheet" href="/assets/apple-site.css"> + <script src="/assets/apple-site.js">。主题切换(localStorage.apple_theme + data-theme)、滚动进度条、顶部公告条(/api/notice)、在线人数(/api/visits)全部自动生效。
  • 导航栏复制主站 index.html 里的 .apple-nav,页脚复制 .apple-footer;子页锚点链接改成 /#about 形式回跳主站区块。
  • 目录属主 www-data,mkdir / cp 需要 sudo,写完后 sudo chown -R www-data:www-data。
  • 新页记得补进 sitemap.xml(robots.txt 全放行,仅禁 /api/,无需改)。

图标获取(自托管)

  • https://cdn.simpleicons.org/<slug>:anthropic、googlegemini 等可用;openai 已被 simple-icons 下架(返回空),OpenAI/Codex 的标从 openai/codex 仓库内 codex-rs/skills/src/assets/samples/openai-docs/assets/openai-small.svg(fill=currentColor,<img> 下即黑色)拿。
  • 官网 favicon:curl -L https://<site>/favicon.ico(ccswitch.io、bigmodel.cn 可用)。
  • GitHub 品牌资源:opencode 官方 logo 在 sst/opencode 的 packages/console/app/src/asset/brand/(dark/light + square 变体)。
  • 应用图标:从本地资产库克隆的仓库直接 copy(如 rikkahub 的 docs/icon.png)。
  • 统一放到子页 icons/ 下,卡片用圆角磁贴,object-fit: contain。

图标磁贴的明暗主题处理(2026-09-24 踩过一遍)

深色磁贴 + 图标分三类,不能一刀切:

  • 透明底彩色标(cc-switch 橙、claude-code 橙、codex 蓝、gemini 紫):深色磁贴上直接可见,不改。
  • 透明底深色单色标(Anthropic 的 A 字标 claude.svg):深色磁贴上糊掉,需 filter: brightness(0) invert(1) 反白。
  • 自带底色的图标(rikkahub、bigmodel 的 Z、OpenMinis、Antigravity、opencode):本身就是不透明带底的方形/圆形图标,既不反色、也不套磁贴——直接让它充满整个 52px 槽位(background: transparent + img { width:52px; height:52px }),图标自己就是磁贴,和别的卡片磁贴同尺寸对齐。

判断依据:PNG 四角 alpha 是否透明。四角透明 → 分彩色/单色(单色要反白);四角不透明(自带底)→ 去掉磁贴。踩过的坑:给自带底的图标做 invert 会变成纯白块(rikkahub / bigmodel 都中了),别只看文件名猜(opencode 的「dark」变体其实带浅色底)。

另外:拿官方图标优先抓站点 favicon / apple-touch-icon(如 https://opencode.ai/favicon-96x96-v3.png),比从仓库 brand 目录拼 SVG 变体靠谱——仓库里的 square 变体常带浅色底、和官网观感对不上。

步骤

  1. sudo mkdir -p /home/ubuntu/workspace/static/6767-home/<name>/icons
  2. 写 index.html(复用 apple-site 结构),scp 上去后 sudo cp + sudo chown www-data:www-data
  3. 图标抓取/上传同目录,同样 chown
  4. 主站 index.html 导航加 <li><a href="/<name>">…</a></li>(若尚未有链接)
  5. sitemap.xml 加 <url>

验证

curl -s -k -o /dev/null -w "%{http_code}\n" https://6767.chat/<name>/        # 期望 200
curl -s -k -o /dev/null -w "%{http_code}\n" https://6767.chat/<name>         # 期望 301 → /<name>/
curl -s -k https://6767.chat/<name>/ | grep -c "<卡片类名"                     # 内容数量
# 逐个图标
for f in icons/*; do curl -s -k -o /dev/null -w "$f %{http_code}\n" https://6767.chat/<name>/$f; done

常见坑

  • 直接用 mkdir 不带 sudo → Permission denied(目录归 www-data)。
  • /assets/... 与 /config.js 是绝对路径,任何子页都能复用,别在子页里写相对路径引用主站资源。
  • 图标放进主站 /assets/ 也行,但子页自带的资源放子页 icons/,隔离更干净、升级主站不牵连。
  • openai-mark.svg 用 fill="currentColor",直接 <img src> 渲染为黑色,浅色磁贴下可见,别期望它是彩色。

独立访问统计实例(visit API)

给子页做「与主站隔离」的访问统计(2026-09-24 给 /ai-tools 落地):

  • home-api 的访问统计已重构为工厂:lib/site-visits.js(createVisitTracker(dataFile))与 lib/visit-regions.js(createRegionTracker(dataFile))都导出工厂,server.js 里 createVisitTracker(path.join(__dirname, "data", "aitools-visits.json")) 即拿到独立实例,数据落独立 JSON(aitools-visits.json / aitools-regions.json)。
  • 新实例 = 4 个端点(镜像 /api/visits 系列):GET /api/ai-tools/visits(打点,返回 total/today/online/location)、/stats(含 30 天 trend + countries/provinces 分布)、POST /heartbeat、POST /leave。
  • 前端切换:apple-site.js 的访问打点 URL 现在由 window.SITE_CONFIG.visitsBase 控制(默认 '/api/visits',主站行为不变)。子页在 apple-site.js 加载前设 window.SITE_CONFIG.visitsBase = '/api/ai-tools/visits' 即打点走新实例、不污染主站计数。
  • nginx 410 坑:公网 /api/* 默认命中 location ^~ /api/ { return 410; }(历史接口下线)。新 API 路径必须去 routes.json 加 proxy 规则(无斜杠版 → location = ...,带斜杠版 → location ^~ .../)再 ./deploy.sh,否则公网 410(本地 127.0.0.1 直连正常,极易误判成代码没生效)。
  • 改完 pm2 restart home-api;routes.json 改动按 AGENTS.md 提交 platform git 仓库(git add nginx/routes.json nginx/sites/6767.chat.conf)。

卡片网格换行修复(auto-fill → auto-fit)

  • 主页 .projects-grid、.connections-grid、ai-tools 的 .ai-grid 原用 repeat(auto-fill, minmax(Npx, 1fr)),最后一行卡片不满格时右侧留空洞、两边不对齐。
  • auto-fit 折叠空轨道、该行卡片自动向左右拉伸占满整行,两边对齐。这是「最外层卡片自适应往左右扩」的标准解法。
  • 主站静态资源改后要 bump 版本号:index.html 里 /assets/apple-site.css?v=20260917 这类查询串改成新日期,否则浏览器命中缓存不刷新。

特例:hero 内 AI Launcher 卡片(面板宽度固定,auto-fit 不够用)

  • .ai-launcher-items 在 hero 的 macOS 窗口mockup 里,面板宽度由 .macos-content(300px 1fr)决定,最宽也只有 ~630px;两列时每张卡仅 ~260px,容不下 图标 + 标题 + URL(约需 265px),于是标题换行。
  • 这种「容器宽度受限、内容有硬性最小需求」的网格,minmax 的阈值不好掐(正好卡在临界会来回翻)。改用显式断点更稳:默认 1fr 1fr,@media (max-width: 1040px) 降为单列(单列全宽,和下方 CLI 命令列表的竖排也统一)。
  • 再加双保险:标题 white-space: nowrap(永不换行),URL white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0(空间不够时截断次要的 URL 而不是标题)。

带鉴权的子页 + 动态后端(2026-09-24 以 /stars 落地)

/stars(GitHub 星标收藏索引)比 /ai-tools 复杂一档:内容来自后端接口,另有一个只有站长能进的设置页。四个新知识点:

1. 静态子页本身仍然不用改 nginx

/stars/ 由主页静态根的 location / 直接服务,照旧 sudo mkdir + sudo cp 即可。只有需要鉴权的那部分才动 routes.json。

2. 给子页的某条路径加鉴权(auth-api)

/stars/settings/ 要登录才可见,就在 routes.json 里加静态 + auth_request 条目(形状照抄 /agy/):

{
  "serverName": "6767.chat www.6767.chat",
  "path": "/stars/settings/",
  "kind": "static",
  "target": "/home/ubuntu/workspace/static/6767-home/stars/settings/",
  "owner": "stars",
  "auth": "auth-api via Nginx auth_request",
  "authRequest": true,
  "authRedirect": "api",
  "status": "active",
  "retiredCode": null
}
  • authRequest: true + authRedirect: "api" → nginx 发 auth_request /_auth_verify(打 auth-api 的 /internal/auth/verify),401 时 error_page 401 = @api_login 跳登录页并带 ?next=。
  • 代理型管理接口(/api/star-lists/admin*)用 authRequest: true + authUserHeader: true,不加 authRedirect(让 401 原样返回给前端 fetch 处理)。authUserHeader 把 X-Auth-User 塞给上游,后端只认这个头:有 = 已登录,没 = 401,不用自己写登录逻辑。
  • 无斜杠版另加一条 kind: "redirect" → 301 到带斜杠版,省得依赖 nginx 对目录的隐式 301。

3. 动态数据后端:抓上游 + 只读缓存

仓库数据来自 home-api 的 lib/github-star-lists.js(照已下线的 zhihu-immersive-reader 同步模块骨架写):

  • 定时(6h)经 curl -x http://127.0.0.1:7890 打 GitHub GraphQL,结果原子写 data/github-star-lists.json。
  • 对外接口只读缓存,请求路径上绝不打上游——否则有人刷页面就撞限流。
  • 服务器上没装 undici,Node 内置 fetch 不认 HTTPS_PROXY,所以走代理只能 spawn curl(零依赖;这类同步几小时才跑一次,开销无所谓)。

4. GitHub Lists 的权限真相(踩过)

想从 GitHub 星标里读「分组(Lists)」,只有一条路:

  • REST 没有:GET /users/{user}/lists → 404。只有 GraphQL。
  • 读公开 list 不需要任何 scope:一张只有 gist/read:org/repo 的票就能读 user(login:"X"){lists{...}}。写才要 user scope —— createUserList / updateUserListsForItem 会报 INSUFFICIENT_SCOPES。
  • 补 user scope 只能 gh auth refresh -h github.com -s user,且必须在浏览器点同意;重新 gh auth login 只会拿回默认 scopes,补不上。
  • 组名 URL slug 规则:转小写、去掉 &、空格转 -、中文原样保留。

5. 列表类接口的首屏性能(2026-09-24 优化 /stars)

253 条装修齐字段时是 268 KB(gzip 114 KB),且响应头是 no-store —— 每次访问全量重下。四条改完降到 728 B:

  • 索引 / 明细分两级:GET /api/star-lists 只发分组元数据(组名、slug、数量、置顶),GET /api/star-lists/items?list=<slug> 发某一组明细,不带 list 发全部。前端首屏只拉索引,明细按组懒加载 + 内存缓存。
  • 别用 no-store:数据几小时才变一次,public, max-age=300 足够;Express 的 ETag 配合 revalidate 还能直接 304。
  • 砍掉前端不渲染的字段:owner / name(由 repo 拆)、homepage / forks / updatedAt 一律不发;描述截到 320 字,topics 截到 6 个。
  • nginx 已开 gzip,所以后端不用再加 compression 中间件。

6. 卡片被畸形描述撑变形的三件套

网格卡片渲染外部数据(仓库描述、标题)时,三个一起加才稳:

.grid > * { min-width: 0; }                    /* 网格项默认 min-width:auto,会被内容撑破轨道 */
.grid .name { overflow-wrap: anywhere; }       /* 超长无空格串(URL、连续英文)允许断 */
.grid .desc {                                  /* 固定行数截断 */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  overflow-wrap: anywhere;
}

只加 -webkit-line-clamp 不够——没有 min-width: 0,长串仍会把整列撑宽。

7. 列表切换不要 scrollTo(0)

列表页的内容区常在页面中部(上面有 hero),切分组时无条件 scrollTo({top: 0}) 会把人甩回 hero,很晕。只在「用户已经滚到内容区下方」时才拉回内容区顶部:

function settleScroll() {
  var top = layout.getBoundingClientRect().top + window.pageYOffset - 76;
  if (window.pageYOffset > top + 24) window.scrollTo({ top, behavior: "smooth" });
}

配合 .stars-layout { scroll-margin-top: 84px } 让锚点/滚动定位避开吸顶导航。

适用边界

  • 纯展示性子页:只做第 1 条即可,无需 routes.json。
  • 需要独立 API / 鉴权 / 服务进程:按上面 2–3 条走 routes.json + deploy.sh,数据侧照「抓上游 + 只读缓存」写模块。
  • 视觉一致性依赖 apple-site.css 的设计令牌(--text-primary 等),子页自定义样式优先用 var(--...),主题自动适配。
  • 另见 tx-nginx-routes-json(routes.json 是唯一真源)与 platform 仓库规范。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web