|
|
@ -1,7 +1,9 @@ |
|
|
# 设计:个人主页可选 EJS 整页渲染 |
|
|
# 设计:个人主页可选 EJS 整页渲染 |
|
|
|
|
|
|
|
|
**日期**:2026-04-21 |
|
|
**日期**:2026-04-21 |
|
|
**状态**:已定稿(与产品对话一致) |
|
|
**状态**:已定稿(修订:与根路径解耦) |
|
|
|
|
|
|
|
|
|
|
|
**修订说明**:不再在 **`/@:publicSlug`** 上根据设置切换整页;根路径 **始终** 为现有 Vue 主页。**个性化整站(EJS)** 使用 **单独公开路由**,避免与 Nuxt 根路由抢请求、也避免访客/站长对「同一 URL 两种形态」困惑。 |
|
|
|
|
|
|
|
|
## 1. 背景与目标 |
|
|
## 1. 背景与目标 |
|
|
|
|
|
|
|
|
@ -9,40 +11,42 @@ |
|
|
|
|
|
|
|
|
**目标**: |
|
|
**目标**: |
|
|
|
|
|
|
|
|
- 在 **同一 URL** `/@:publicSlug`(及与之同义的仅带尾部 `/` 的根路径)上,允许用户通过设置 **切换**: |
|
|
- **`/@:publicSlug`**:**不覆盖、不劫持**,行为与现网一致,**始终** 渲染 Vue 默认主页。 |
|
|
- **Vue 默认主页**(现状),或 |
|
|
- 新增 **独立公开路由**,在满足设置时由服务端输出 **完整 HTML**(EJS),实现「不限于本站壳」的个性化整页;模板内可链到 `/@slug/posts/...`、`timeline`、`about` 等现有 Vue 子路由。 |
|
|
- **服务端 EJS 输出的完整 HTML 文档**(整页不局限于站内壳层;模板内可链到 `/@slug/posts/...`、`timeline`、`about` 等现有 Vue 子路由)。 |
|
|
- **路由形态(已选)**:在 `@` 命名空间下增加 **固定子路径**,规范路径为: |
|
|
- **`/@slug` 根路径以外**(如文章、时光机、关于页)**行为不变**,仍走现有 Vue 路由。 |
|
|
- **`/@:publicSlug/custom`** |
|
|
|
|
|
- 实现层使用常量(如 `PUBLIC_EJS_HOME_SEGMENT = 'custom'`),若需改名仅改一处;**不得**与已有动态段(如 `posts`、`about`)冲突。 |
|
|
- **模板能力(已确认)**:采用 **完整 EJS**(含任意 `<% %>` 服务端逻辑);等价于在服务器上以进程身份执行 **用户可控代码**,**信任模型与滥用责任由运营侧承担**;本版 **不做** 沙箱化必选(若日后对公众开放,需另立项「受限模板 / 沙箱进程」)。 |
|
|
- **模板能力(已确认)**:采用 **完整 EJS**(含任意 `<% %>` 服务端逻辑);等价于在服务器上以进程身份执行 **用户可控代码**,**信任模型与滥用责任由运营侧承担**;本版 **不做** 沙箱化必选(若日后对公众开放,需另立项「受限模板 / 沙箱进程」)。 |
|
|
|
|
|
|
|
|
## 2. 产品规则(已确认) |
|
|
## 2. 产品规则(已确认) |
|
|
|
|
|
|
|
|
| 维度 | 规则 | |
|
|
| 维度 | 规则 | |
|
|
|------|------| |
|
|
|------|------| |
|
|
| 切换范围 | **仅** 个人主页根路径 `/@:publicSlug`(路径上 **无** 下一级 segment,如 `/posts` 不算)。 | |
|
|
| 根路径 | **`/@:publicSlug`** **仅** Vue;**不**存在「根路径切 EJS」模式。 | |
|
|
| 子路由 | `/@:publicSlug/posts/...`、`timeline`、`about` 等 **始终** 为现有 Vue 页;主页模板中可 **链接** 至这些路径(产品叙事:**首页可完全自定义,子页仍是站内 App**)。 | |
|
|
| 个性化页 | 仅 **`/@:publicSlug/custom`**(及同义尾部 `/`)在**启用**时由 **EJS → `text/html`** 响应;**未启用**时对访客返回 **404**(不推荐 301 到根路径,以免外站链到 `custom` 时期望稳定语义时产生循环或歧义;若产品坚持跳根,可另议)。 | |
|
|
| 模式 | 每用户一个 **`homeRenderMode`**(命名实现时可微调,语义固定):`vue` \| `ejs`。`vue` 时行为与现网一致;`ejs` 时对根路径返回 **完整 `text/html`**。 | |
|
|
| 子路由 | `/@:publicSlug/posts/...`、`timeline`、`about` 等 **始终** Vue;`custom` 与上述 segment **并列**,**不** 接管子树。 | |
|
|
| 编辑权限 | **仅** 对应 `publicSlug` 的 **已登录所有者** 可在控制台(如 `me` / 设置)编辑模板与切换模式。 | |
|
|
| 启用与编辑 | 每用户 **`customHomeEnabled`**(命名实现可微调)与 **`homeEjsSource`**;**仅** 所有者在控制台开启并编辑模板;关闭后 **`/custom` 404**。 | |
|
|
| 数据真相 | 注入 EJS 的公开数据与 **`GET /api/public/profile/:publicSlug`** **语义一致**(同一可见性、同一字段含义),通过 **共享 server 层** 组装,避免两套逻辑漂移。 | |
|
|
| 数据真相 | 注入 EJS 的公开数据与 **`GET /api/public/profile/:publicSlug`** **语义一致**(同一可见性、同一字段含义),通过 **共享 server 层** 组装,避免两套逻辑漂移。 | |
|
|
|
|
|
|
|
|
## 3. 非目标(本版明确不做) |
|
|
## 3. 非目标(本版明确不做) |
|
|
|
|
|
|
|
|
- **不** 用 EJS 接管 `/@slug/**` 整棵子树;**不** 改为独立子域或独立路径作为主入口(除非你方后续改需求)。 |
|
|
- **不** 根据设置 **覆盖** **`/@:publicSlug`** 的响应体或路由解析顺序。 |
|
|
|
|
|
- **不** 用 EJS 接管 `/@slug/**` 除 **`custom`** 之外的整棵子树;**不** 将 **`custom`** 改为通配符子站。 |
|
|
- **不** 将「完整 EJS」与「沙箱必选」同时作为本版交付条件;沙箱 / Liquid 等作为 **后续** 产品阶段评估。 |
|
|
- **不** 将「完整 EJS」与「沙箱必选」同时作为本版交付条件;沙箱 / Liquid 等作为 **后续** 产品阶段评估。 |
|
|
- **不** 以「仅客户端 `v-html` 注入整站」冒充 **整页 SSR HTML** 的主方案(可与方案 1 对比说明,但不替代主路径)。 |
|
|
- **不** 以「仅客户端 `v-html` 注入整站」冒充 **整页 SSR HTML** 的主方案(可与方案对比说明,但不替代主路径)。 |
|
|
|
|
|
|
|
|
## 4. 架构(推荐实现) |
|
|
## 4. 架构(推荐实现) |
|
|
|
|
|
|
|
|
### 4.1 方案选定:Nitro 短路与早返回 |
|
|
### 4.1 方案选定:Nitro 对 **`/@[slug]/custom`** 短路 |
|
|
|
|
|
|
|
|
在 **Nitro** 侧(`server/middleware` 或等价、且顺序可控的钩子): |
|
|
在 **Nitro** 侧(`server/middleware` 或等价、且顺序可控的钩子): |
|
|
|
|
|
|
|
|
1. 仅当请求路径 **匹配**「公开个人根路径」:`/^\/@(?<slug>[^/]+)\/?$/`(具体正则与大小写策略 **与路由参数约定一致**,需与 Nuxt `@[publicSlug]` 对齐)。 |
|
|
1. 仅当请求路径 **匹配**「个性化页」:`/^\/@(?<slug>[^/]+)\/custom\/?$/`(与 **`PUBLIC_EJS_HOME_SEGMENT`** 一致;大小写策略与 **`publicSlug` 查库** 约定对齐)。 |
|
|
2. 解析 `publicSlug`,查库得到用户与 **`homeRenderMode`**。 |
|
|
2. 解析 `publicSlug`,查库:**用户存在** 且 **`customHomeEnabled`** 为真 且 **`homeEjsSource`** 非空(具体条件实现可收紧)。 |
|
|
3. 若为 **`ejs`**:拉取 **与公开 API 等价** 的 payload,执行 **`ejs.render`**,设置 **`Content-Type: text/html;charset=utf-8`**,**结束响应**(不进入 Nuxt 页面管线)。 |
|
|
3. **满足**:拉取 **与公开 API 等价** 的 payload,执行 **`ejs.render`**,**`Content-Type: text/html;charset=utf-8`**,**结束响应**。 |
|
|
4. 若为 **`vue`**:**不写 body**,**放行** 至现有 `index.vue`。 |
|
|
4. **不满足**:**不写 body**、**不** 替代 404——由后续 Nitro/Nuxt 处理;若工程上尚无对应 Vue 占位页,应 **显式** `404`(避免落到无关页面)。推荐:**专用 nitro 路由** 或 **中间件内** 对「匹配 `/custom` 但未启用」直接 **`404`**。 |
|
|
|
|
|
|
|
|
**备选(不采用为主)**:独立路径或子域渲染子站(违背同 URL 切换);或仅用 Vue + `v-html`(违背整页 HTML / SEO 预期)。 |
|
|
**与根路径的关系**:访问 **`/@slug`** **永不进入** 上述 EJS 分支,仅进入现有 `index.vue`。 |
|
|
|
|
|
|
|
|
### 4.2 数据与注入 |
|
|
### 4.2 数据与注入 |
|
|
|
|
|
|
|
|
@ -50,32 +54,40 @@ |
|
|
- **`ejs.render(template, locals)`**:`locals` 至少包含: |
|
|
- **`ejs.render(template, locals)`**:`locals` 至少包含: |
|
|
- **`profile`**:与 API 响应体(成功时的业务 `data`)**结构一致** 的对象; |
|
|
- **`profile`**:与 API 响应体(成功时的业务 `data`)**结构一致** 的对象; |
|
|
- **`site`**:站名、站点公开根 URL 等(来源与现有全局配置一致); |
|
|
- **`site`**:站名、站点公开根 URL 等(来源与现有全局配置一致); |
|
|
|
|
|
- **URL 辅助**:如 **`profileRootPath`** = `/@{slug}`、**`customHomePath`** = `/@{slug}/custom`,便于模板内链到站内子路径; |
|
|
- **安全向 helper**:如 **`escapeHtml`**、日期展示格式化等(**鼓励** 在模板里用 helper,但 **不** 通过模板消灭 EJS 的任意代码能力——与产品选项 A 一致)。 |
|
|
- **安全向 helper**:如 **`escapeHtml`**、日期展示格式化等(**鼓励** 在模板里用 helper,但 **不** 通过模板消灭 EJS 的任意代码能力——与产品选项 A 一致)。 |
|
|
- **禁止项(工程习惯)**:不向 `locals` 注入开放 **`require`**、未约束文件路径等;**`ejs` 选项** 需审阅官方文档,避免 **`root` / `filename`** 等导致意外包含路径遍历;模板存储于 **DB 文本字段**,不设「从磁盘任意路径读模板」。 |
|
|
- **禁止项(工程习惯)**:不向 `locals` 注入开放 **`require`**、未约束文件路径等;**`ejs` 选项** 需审阅官方文档,避免 **`root` / `filename`** 等导致意外包含路径遍历;模板存储于 **DB 文本字段**,不设「从磁盘任意路径读模板」。 |
|
|
|
|
|
|
|
|
### 4.3 存储 |
|
|
### 4.3 存储 |
|
|
|
|
|
|
|
|
- 用户表或等价扩展:保存 **`homeRenderMode`**、`homeEjsSource`(或拆表);**最大长度**、**版本号(可选)** 在实现层约束。 |
|
|
- 用户表或等价扩展:保存 **`customHomeEnabled`**、**`homeEjsSource`**(或拆表);**最大长度**、**版本号(可选)** 在实现层约束。 |
|
|
- 保存时:**可选** 先做 **语法与编译试跑**(仅对所有者预览接口或保存路径),语法错误返回 **400**,错误信息 **不** 对匿名访客暴露内部栈。 |
|
|
- 保存时:**可选** 先做 **语法与编译试跑**(仅对所有者预览接口或保存路径),语法错误返回 **400**,错误信息 **不** 对匿名访客暴露内部栈。 |
|
|
|
|
|
|
|
|
|
|
|
### 4.4 可选:站内发现 |
|
|
|
|
|
|
|
|
|
|
|
- 若产品需要在 Vue 根主页展示「个性化站点」入口:仅在 **`customHomeEnabled`** 时显示链到 **`/@slug/custom`**;**本 spec 不强制**(YAGNI,实现计划可单列为小任务)。 |
|
|
|
|
|
|
|
|
## 5. 安全与滥用(在选项 A 下的最低工程线) |
|
|
## 5. 安全与滥用(在选项 A 下的最低工程线) |
|
|
|
|
|
|
|
|
以下内容 **不能** 替代「仅可信用户 / 条款 + 封禁」的产品决策,但作为 **最低限度**: |
|
|
以下内容 **不能** 替代「仅可信用户 / 条款 + 封禁」的产品决策,但作为 **最低限度**: |
|
|
|
|
|
|
|
|
- **单请求**:渲染 **超时**(防止死循环 / 极重计算拖死进程)。 |
|
|
- **单请求**:渲染 **超时**(防止死循环 / 极重计算拖死进程)。 |
|
|
- **模板体积**:上传 **上限**。 |
|
|
- **模板体积**:**上限**。 |
|
|
- **错误响应**:访客侧 **通用错误页或简短 500 文案**;调试信息仅在 **已鉴权预览** 中展示。 |
|
|
- **错误响应**:访客侧 **通用错误页或简短 500 文案**;调试信息仅在 **已鉴权预览** 中展示。 |
|
|
- **可选**:运营 **一键将 `homeRenderMode` 置回 `vue`**(管理端或 CLI 文档化)。 |
|
|
- **可选**:运营 **关闭 `customHomeEnabled`**(管理端或 CLI 文档化),使 **`/custom` 404**。 |
|
|
|
|
|
|
|
|
## 6. 错误与兼容性 |
|
|
## 6. 错误与兼容性 |
|
|
|
|
|
|
|
|
- **运行时错误**:记录服务端日志;对匿名访客返回 **安全** 的 HTML 错误页或 500。 |
|
|
- **运行时错误**:记录服务端日志;对匿名访客返回 **安全** 的 HTML 错误页或 500。 |
|
|
- **自动降级**(连续失败 N 次自动切回 `vue`):**本版默认不做**;若日后做,须在设置中明文提示并可关闭,避免主人困惑。 |
|
|
- **自动降级**(连续失败自动关闭 `customHomeEnabled`):**本版默认不做**;若日后做,须在设置中明文提示并可关闭。 |
|
|
|
|
|
|
|
|
## 7. 测试 |
|
|
## 7. 测试 |
|
|
|
|
|
|
|
|
- **单元测试**:固定 `locals` 与短模板片段,断言 **`ejs.render`** 输出与 **逃逸** 行为。 |
|
|
- **单元测试**:固定 `locals` 与短模板片段,断言 **`ejs.render`** 输出与 **逃逸** 行为。 |
|
|
- **集成或手动清单**:同一 `publicSlug` 在 `vue` / `ejs` 下各访问一次根路径;确认 **`/posts` 等子路径** 仍为 Vue。 |
|
|
- **集成或手动清单**: |
|
|
|
|
|
- **`/@slug`**:始终为 Vue; |
|
|
|
|
|
- **`/@slug/custom`**:启用 → HTML;关闭 → **404**; |
|
|
|
|
|
- **`/@slug/posts/...`** 等仍为 Vue。 |
|
|
|
|
|
|
|
|
## 8. 依赖 |
|
|
## 8. 依赖 |
|
|
|
|
|
|
|
|
|