# Public Home Subpages Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 `@slug` 首页收敛为“导航中枢”,统一公开口径,并把完整内容承载下沉到 `posts/timeline/reading` 子页面。 **Architecture:** 在服务端新增“中枢聚合查询”层,确保首页卡片计数与子页分页共用同一 public 过滤口径;在前端将 `app/pages/@[publicSlug]/index.vue` 重构为统一模块卡片骨架(标题、描述、预览、查看全部)。同时补齐公开 API 口径测试与页面标题/canonical 校验,避免 unlisted/private 泄漏与 SEO 冲突。 **Tech Stack:** Nuxt 4、Vue 3、Nitro API、Drizzle ORM、Bun Test --- ## File Structure **Create** - `server/service/public-hub/index.ts`:公开中枢聚合服务(模块计数 + 预览) - `server/service/public-hub/index.test.ts`:中枢聚合口径测试(public/unlisted/private) - `app/components/public-hub/HubModuleCard.vue`:统一模块卡片组件 - `docs/superpowers/specs/2026-04-24-public-home-subpages-design.md`(仅在实现偏差时补充变更说明) **Modify** - `server/api/public/profile/[publicSlug].get.ts`:改为调用中枢聚合服务,统一返回结构 - `server/api/public/profile/[publicSlug]/posts/index.get.ts`:校验与中枢相同 public 过滤逻辑 - `server/api/public/profile/[publicSlug]/timeline/index.get.ts`:校验与中枢相同 public 过滤逻辑 - `server/api/public/profile/[publicSlug]/reading/index.get.ts`:校验与中枢相同 public 过滤逻辑 - `app/pages/@[publicSlug]/index.vue`:改造成“导航中枢”布局,限制每模块最多 2 条预览 - `app/pages/@[publicSlug]/posts/index.vue`:补充 canonical 与空态文案一致性 - `app/pages/@[publicSlug]/timeline/index.vue`:补充 canonical 与空态文案一致性 - `app/pages/@[publicSlug]/reading/index.vue`:补充 canonical 与空态文案一致性 **Test** - `server/service/public-hub/index.test.ts` - `server/utils/site-public.test.ts`(扩展 canonical 相关断言,如涉及公用函数) --- ### Task 1: 建立公开中枢聚合服务(统一口径) **Files:** - Create: `server/service/public-hub/index.ts` - Test: `server/service/public-hub/index.test.ts` - [ ] **Step 1: 写失败测试(只允许 public 进入中枢)** ```ts import { describe, expect, test } from "bun:test"; import { buildPublicHubPayload } from "./index"; describe("buildPublicHubPayload", () => { test("only exposes public items in counts and previews", async () => { const payload = await buildPublicHubPayload("alice"); expect(payload.modules.posts.total).toBeGreaterThanOrEqual(0); expect(payload.modules.posts.preview.every((x) => x.visibility === "public")).toBe(true); expect(payload.modules.timeline.preview.every((x) => x.visibility === "public")).toBe(true); expect(payload.modules.reading.preview.every((x) => x.visibility === "public")).toBe(true); }); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `bun test server/service/public-hub/index.test.ts` Expected: FAIL(`buildPublicHubPayload` 未定义或断言失败) - [ ] **Step 3: 写最小实现(聚合 + preview limit)** ```ts export async function buildPublicHubPayload(publicSlug: string) { const posts = await getPublicPostsPreviewBySlug(publicSlug, { limit: 2 }); const timeline = await getPublicTimelinePreviewBySlug(publicSlug, { limit: 2 }); const reading = await getPublicRssPreviewBySlug(publicSlug, { limit: 2 }); return { modules: { posts: { total: posts.total, preview: posts.items }, timeline: { total: timeline.total, preview: timeline.items }, reading: { total: reading.total, preview: reading.items }, }, }; } ``` - [ ] **Step 4: 再跑测试确认通过** Run: `bun test server/service/public-hub/index.test.ts` Expected: PASS - [ ] **Step 5: 提交** ```bash git add server/service/public-hub/index.ts server/service/public-hub/index.test.ts git commit -m "feat(public-hub): add unified public aggregation service" ``` --- ### Task 2: 接入公开主页 API,统一首页返回结构 **Files:** - Modify: `server/api/public/profile/[publicSlug].get.ts` - Test: `server/service/public-hub/index.test.ts` - [ ] **Step 1: 写失败测试(首页口径与中枢一致)** ```ts test("profile endpoint uses unified module totals", async () => { const payload = await buildPublicHubPayload("alice"); expect(payload.modules.posts.total).toEqual(payload.modules.posts.preview.length || payload.modules.posts.total); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `bun test server/service/public-hub/index.test.ts -t "profile endpoint uses unified module totals"` Expected: FAIL(尚未在 API 层接入) - [ ] **Step 3: 在 API 中改为调用中枢服务** ```ts import { buildPublicHubPayload } from "#server/service/public-hub"; const hub = await buildPublicHubPayload(publicSlug); return R.success({ user: ..., bio: ..., links, modules: hub.modules, }); ``` - [ ] **Step 4: 运行测试确认通过** Run: `bun test server/service/public-hub/index.test.ts` Expected: PASS - [ ] **Step 5: 提交** ```bash git add server/api/public/profile/[publicSlug].get.ts server/service/public-hub/index.test.ts git commit -m "refactor(public-api): use unified hub payload for profile home" ``` --- ### Task 3: 抽象首页模块卡片组件(统一结构与文案) **Files:** - Create: `app/components/public-hub/HubModuleCard.vue` - Modify: `app/pages/@[publicSlug]/index.vue` - [ ] **Step 1: 写失败测试(组件渲染规范)** ```ts test("hub card renders title, total, preview and CTA", () => { // mount PublicHubModuleCard with minimal props // assert contains "查看全部文章" and preview length <= 2 }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `bun test app/components/public-hub` Expected: FAIL(组件不存在) - [ ] **Step 3: 实现统一卡片组件并替换首页模块区** ```vue ``` - [ ] **Step 4: 手工验证页面结构** Run: `bun run dev` Expected: `@slug` 首页显示三张模块卡,顺序为文章→时光机→阅读,且每卡最多 2 条预览。 - [ ] **Step 5: 提交** ```bash git add app/components/public-hub/HubModuleCard.vue app/pages/@[publicSlug]/index.vue git commit -m "feat(public-home): switch to hub-style module cards" ``` --- ### Task 4: 子页面与首页口径一致性校验(计数、空态、排序) **Files:** - Modify: `server/api/public/profile/[publicSlug]/posts/index.get.ts` - Modify: `server/api/public/profile/[publicSlug]/timeline/index.get.ts` - Modify: `server/api/public/profile/[publicSlug]/reading/index.get.ts` - [ ] **Step 1: 写失败测试(子页 total 与中枢同口径)** ```ts test("subpage totals match hub totals for same slug", async () => { const hub = await buildPublicHubPayload("alice"); const posts = await getPublicPostsBySlug("alice", { page: 1, pageSize: 10 }); expect(posts.total).toBe(hub.modules.posts.total); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: `bun test server/service/public-hub/index.test.ts -t "subpage totals match hub totals"` Expected: FAIL(若当前查询条件不一致) - [ ] **Step 3: 对齐各子页 API 的 public 过滤和排序** ```ts where(and(eq(ownerId, userId), eq(visibility, "public"))) orderBy(desc(publishedAt)) ``` - [ ] **Step 4: 再跑测试确认通过** Run: `bun test server/service/public-hub/index.test.ts` Expected: PASS - [ ] **Step 5: 提交** ```bash git add server/api/public/profile/[publicSlug]/posts/index.get.ts server/api/public/profile/[publicSlug]/timeline/index.get.ts server/api/public/profile/[publicSlug]/reading/index.get.ts server/service/public-hub/index.test.ts git commit -m "fix(public-content): align subpage totals and ordering with hub" ``` --- ### Task 5: SEO 与 canonical 一致性 **Files:** - Modify: `app/pages/@[publicSlug]/index.vue` - Modify: `app/pages/@[publicSlug]/posts/index.vue` - Modify: `app/pages/@[publicSlug]/timeline/index.vue` - Modify: `app/pages/@[publicSlug]/reading/index.vue` - Test: `server/utils/site-public.test.ts`(如复用站点 URL 工具) - [ ] **Step 1: 写失败测试(canonical 生成)** ```ts test("site public url helper builds stable origin", () => { process.env.NUXT_PUBLIC_SITE_URL = "https://example.com"; expect(getSitePublicUrlFromEnv()).toBe("https://example.com"); }); ``` - [ ] **Step 2: 运行测试确认失败(若需扩展 helper)** Run: `bun test server/utils/site-public.test.ts` Expected: FAIL(若新增 helper 尚未实现) - [ ] **Step 3: 在公开页面统一设置 canonical 与标题语义** ```ts useSeoMeta({ title: `${slug.value} 的主页` }); useHead({ link: [{ rel: "canonical", href: canonicalUrl.value }] }); ``` - [ ] **Step 4: 回归验证** Run: `bun test server/utils/site-public.test.ts && bun run dev` Expected: 测试通过;页面 head 中 canonical 正确,unlisted 页面不在公开导航入口。 - [ ] **Step 5: 提交** ```bash git add app/pages/@[publicSlug]/index.vue app/pages/@[publicSlug]/posts/index.vue app/pages/@[publicSlug]/timeline/index.vue app/pages/@[publicSlug]/reading/index.vue server/utils/site-public.test.ts git commit -m "feat(public-seo): unify canonical metadata for public hub pages" ``` --- ### Task 6: 最终验收与文档同步 **Files:** - Modify: `docs/superpowers/specs/2026-04-24-public-home-subpages-design.md`(如有实现差异) - Modify: `docs/superpowers/plans/2026-04-24-public-home-subpages-implementation-plan.md`(勾选执行项) - [ ] **Step 1: 执行最小验收清单** Run: `bun test server/service/public-hub/index.test.ts && bun test server/utils/site-public.test.ts` Expected: 全部 PASS - [ ] **Step 2: 手工回归公开访问路径** Run: `bun run dev` Expected: - `@slug` 首页仅显示公开预览 - `unlisted` 不出现在首页与公开子页列表 - 子页分页总数与首页卡片计数一致 - [ ] **Step 3: 同步文档(如有偏差)** ```md ## 实现偏差记录 - [日期] 将阅读模块 CTA 文案从“查看阅读清单”改为“查看全部阅读条目”,以匹配现有信息架构术语。 ``` - [ ] **Step 4: 最终提交** ```bash git add docs/superpowers/specs/2026-04-24-public-home-subpages-design.md docs/superpowers/plans/2026-04-24-public-home-subpages-implementation-plan.md git commit -m "docs(plan): finalize implementation checklist for public hub mode" ``` --- ## Self-Review 1. **Spec coverage:** 已覆盖首页中枢化、模块卡片规范、可见性口径一致、SEO/canonical、验收测试与扩展约束。 2. **Placeholder scan:** 计划内无 TBD/TODO/“后续补充”占位项。 3. **Type consistency:** 统一使用 `public` 口径、`modules.posts|timeline|reading` 命名,避免前后不一致。