You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6.1 KiB
6.1 KiB
设计:个人主页可选 EJS 整页渲染
日期:2026-04-21
状态:已定稿(与产品对话一致)
1. 背景与目标
当前公开个人根路径 /@:publicSlug 由 Nuxt 页面 app/pages/@[publicSlug]/index.vue 与 layout: public 渲染,视觉与结构受站内组件体系约束。
目标:
- 在 同一 URL
/@:publicSlug(及与之同义的仅带尾部/的根路径)上,允许用户通过设置 切换:- Vue 默认主页(现状),或
- 服务端 EJS 输出的完整 HTML 文档(整页不局限于站内壳层;模板内可链到
/@slug/posts/...、timeline、about等现有 Vue 子路由)。
/@slug根路径以外(如文章、时光机、关于页)行为不变,仍走现有 Vue 路由。- 模板能力(已确认):采用 完整 EJS(含任意
<% %>服务端逻辑);等价于在服务器上以进程身份执行 用户可控代码,信任模型与滥用责任由运营侧承担;本版 不做 沙箱化必选(若日后对公众开放,需另立项「受限模板 / 沙箱进程」)。
2. 产品规则(已确认)
| 维度 | 规则 |
|---|---|
| 切换范围 | 仅 个人主页根路径 /@:publicSlug(路径上 无 下一级 segment,如 /posts 不算)。 |
| 子路由 | /@:publicSlug/posts/...、timeline、about 等 始终 为现有 Vue 页;主页模板中可 链接 至这些路径(产品叙事:首页可完全自定义,子页仍是站内 App)。 |
| 模式 | 每用户一个 homeRenderMode(命名实现时可微调,语义固定):vue | ejs。vue 时行为与现网一致;ejs 时对根路径返回 完整 text/html。 |
| 编辑权限 | 仅 对应 publicSlug 的 已登录所有者 可在控制台(如 me / 设置)编辑模板与切换模式。 |
| 数据真相 | 注入 EJS 的公开数据与 GET /api/public/profile/:publicSlug 语义一致(同一可见性、同一字段含义),通过 共享 server 层 组装,避免两套逻辑漂移。 |
3. 非目标(本版明确不做)
- 不 用 EJS 接管
/@slug/**整棵子树;不 改为独立子域或独立路径作为主入口(除非你方后续改需求)。 - 不 将「完整 EJS」与「沙箱必选」同时作为本版交付条件;沙箱 / Liquid 等作为 后续 产品阶段评估。
- 不 以「仅客户端
v-html注入整站」冒充 整页 SSR HTML 的主方案(可与方案 1 对比说明,但不替代主路径)。
4. 架构(推荐实现)
4.1 方案选定:Nitro 短路与早返回
在 Nitro 侧(server/middleware 或等价、且顺序可控的钩子):
- 仅当请求路径 匹配「公开个人根路径」:
/^\/@(?<slug>[^/]+)\/?$/(具体正则与大小写策略 与路由参数约定一致,需与 Nuxt@[publicSlug]对齐)。 - 解析
publicSlug,查库得到用户与homeRenderMode。 - 若为
ejs:拉取 与公开 API 等价 的 payload,执行ejs.render,设置Content-Type: text/html;charset=utf-8,结束响应(不进入 Nuxt 页面管线)。 - 若为
vue:不写 body,放行 至现有index.vue。
备选(不采用为主):独立路径或子域渲染子站(违背同 URL 切换);或仅用 Vue + v-html(违背整页 HTML / SEO 预期)。
4.2 数据与注入
- 单一来源:抽取
getPublicProfilePayloadForHome(publicSlug)(名称可调整),供GET /api/public/profile/:publicSlug与 EJS 渲染 共用。 ejs.render(template, locals):locals至少包含:profile:与 API 响应体(成功时的业务data)结构一致 的对象;site:站名、站点公开根 URL 等(来源与现有全局配置一致);- 安全向 helper:如
escapeHtml、日期展示格式化等(鼓励 在模板里用 helper,但 不 通过模板消灭 EJS 的任意代码能力——与产品选项 A 一致)。
- 禁止项(工程习惯):不向
locals注入开放require、未约束文件路径等;ejs选项 需审阅官方文档,避免root/filename等导致意外包含路径遍历;模板存储于 DB 文本字段,不设「从磁盘任意路径读模板」。
4.3 存储
- 用户表或等价扩展:保存
homeRenderMode、homeEjsSource(或拆表);最大长度、版本号(可选) 在实现层约束。 - 保存时:可选 先做 语法与编译试跑(仅对所有者预览接口或保存路径),语法错误返回 400,错误信息 不 对匿名访客暴露内部栈。
5. 安全与滥用(在选项 A 下的最低工程线)
以下内容 不能 替代「仅可信用户 / 条款 + 封禁」的产品决策,但作为 最低限度:
- 单请求:渲染 超时(防止死循环 / 极重计算拖死进程)。
- 模板体积:上传 上限。
- 错误响应:访客侧 通用错误页或简短 500 文案;调试信息仅在 已鉴权预览 中展示。
- 可选:运营 一键将
homeRenderMode置回vue(管理端或 CLI 文档化)。
6. 错误与兼容性
- 运行时错误:记录服务端日志;对匿名访客返回 安全 的 HTML 错误页或 500。
- 自动降级(连续失败 N 次自动切回
vue):本版默认不做;若日后做,须在设置中明文提示并可关闭,避免主人困惑。
7. 测试
- 单元测试:固定
locals与短模板片段,断言ejs.render输出与 逃逸 行为。 - 集成或手动清单:同一
publicSlug在vue/ejs下各访问一次根路径;确认/posts等子路径 仍为 Vue。
8. 依赖
- 新增
ejs运行时依赖(版本以 lockfile 与审计为准);类型如有@types/ejs则开发依赖补齐。
下一实现阶段:在用户确认本 spec 无异议后,另见 docs/superpowers/plans/ 下 implementation plan(writing-plans 产出)。