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.
 
 
 
 
 

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.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 进入中枢)

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

  1. Spec coverage: 已覆盖首页中枢化、模块卡片规范、可见性口径一致、SEO/canonical、验收测试与扩展约束。
  2. Placeholder scan: 计划内无 TBD/TODO/“后续补充”占位项。
  3. Type consistency: 统一使用 public 口径、modules.posts|timeline|reading 命名,避免前后不一致。