11 KiB
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.tsserver/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 进入中枢)
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)
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: 提交
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: 写失败测试(首页口径与中枢一致)
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 中改为调用中枢服务
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: 提交
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: 写失败测试(组件渲染规范)
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: 实现统一卡片组件并替换首页模块区
<PublicHubModuleCard
module-key="posts"
title="文章"
:total="data.modules.posts.total"
cta-text="查看全部文章"
:to="`/@${slug}/posts`"
:preview-items="data.modules.posts.preview"
/>
- Step 4: 手工验证页面结构
Run: bun run dev
Expected: @slug 首页显示三张模块卡,顺序为文章→时光机→阅读,且每卡最多 2 条预览。
- Step 5: 提交
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 与中枢同口径)
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 过滤和排序
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: 提交
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 生成)
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 与标题语义
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: 提交
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: 同步文档(如有偏差)
## 实现偏差记录
- [日期] 将阅读模块 CTA 文案从“查看阅读清单”改为“查看全部阅读条目”,以匹配现有信息架构术语。
- Step 4: 最终提交
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
- Spec coverage: 已覆盖首页中枢化、模块卡片规范、可见性口径一致、SEO/canonical、验收测试与扩展约束。
- Placeholder scan: 计划内无 TBD/TODO/“后续补充”占位项。
- Type consistency: 统一使用
public口径、modules.posts|timeline|reading命名,避免前后不一致。