From dd209fa68aa9a2c9edfac72d0d53116e7bdb1492 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 24 Apr 2026 21:46:23 +0800 Subject: [PATCH] feat(public-home): implement Reader and Showcase layouts for public profile Add ReaderLayout and ShowcaseLayout components to enhance the public profile page, allowing for modular display of posts, timeline, and reading sections. This change establishes a unified navigation hub, improving user experience with structured content previews and consistent API handling. Made-with: Cursor --- app/components/public-home/ReaderLayout.vue | 352 +++++++++++++++++++++ app/components/public-home/ShowcaseLayout.vue | 260 +++++++++++++++ app/pages/@[publicSlug]/index.vue | 263 ++------------- ...-24-public-home-subpages-implementation-plan.md | 317 +++++++++++++++++++ .../2026-04-24-public-home-subpages-design.md | 147 +++++++++ packages/drizzle-pkg/db.sqlite | Bin 163840 -> 163840 bytes 6 files changed, 1110 insertions(+), 229 deletions(-) create mode 100644 app/components/public-home/ReaderLayout.vue create mode 100644 app/components/public-home/ShowcaseLayout.vue create mode 100644 docs/superpowers/plans/2026-04-24-public-home-subpages-implementation-plan.md create mode 100644 docs/superpowers/specs/2026-04-24-public-home-subpages-design.md diff --git a/app/components/public-home/ReaderLayout.vue b/app/components/public-home/ReaderLayout.vue new file mode 100644 index 0000000..7162548 --- /dev/null +++ b/app/components/public-home/ReaderLayout.vue @@ -0,0 +1,352 @@ + + + diff --git a/app/components/public-home/ShowcaseLayout.vue b/app/components/public-home/ShowcaseLayout.vue new file mode 100644 index 0000000..821dabb --- /dev/null +++ b/app/components/public-home/ShowcaseLayout.vue @@ -0,0 +1,260 @@ + + + diff --git a/app/pages/@[publicSlug]/index.vue b/app/pages/@[publicSlug]/index.vue index 45c9deb..0af51ac 100644 --- a/app/pages/@[publicSlug]/index.vue +++ b/app/pages/@[publicSlug]/index.vue @@ -3,7 +3,9 @@ import { unwrapApiBody, type ApiResponse } from '../../utils/http/factory' import { extractFrontMatterDesc, stripFrontMatter } from '../../utils/markdown-front-matter' import { buildPublicCanonicalUrl } from '../../utils/public-canonical-url' import { safeExternalHref } from '../../utils/safe-external-href' -import { formatOccurredOnDisplay, formatPublishedDateOnly, occurredOnToIsoAttr } from '../../utils/timeline-datetime' +import { usePublicProfileLayoutMode } from '../../composables/usePublicHomeLayout' +import ShowcaseLayout from '../../components/public-home/ShowcaseLayout.vue' +import ReaderLayout from '../../components/public-home/ReaderLayout.vue' definePageMeta({ layout: 'public', @@ -12,20 +14,17 @@ definePageMeta({ const route = useRoute() const runtimeConfig = useRuntimeConfig() const slug = computed(() => route.params.publicSlug as string) +const { mode } = usePublicProfileLayoutMode() type PublicPostListItem = { title?: string | null excerpt?: string | null slug?: string | null publishedAt?: Date | string | null - coverUrl?: string | null } type PublicTimelineItem = { - id?: number title?: string | null - occurredOn?: Date | string | null - linkUrl?: string | null bodyMarkdown?: string | null } @@ -67,62 +66,11 @@ function normalizeModule(primary?: ModulePayload, fallback?: ModulePayload } } -function rssPublicHref(it: PublicRssListItem): string | undefined { - const u = it.canonicalUrl ?? it.canonical_url - return safeExternalHref(u) -} - -function rssPublicTitle(it: PublicRssListItem): string { - const t = it.title - return typeof t === 'string' && t.trim().length ? t : '未命名' -} - -function postPublicTitle(it: PublicPostListItem): string { - const t = it.title - return typeof t === 'string' && t.trim().length ? t : '未命名文章' -} - -function postPublicExcerpt(it: PublicPostListItem): string { - const e = it.excerpt - return typeof e === 'string' ? e : '' -} - -function postPublicSlug(it: PublicPostListItem): string { - const s = it.slug - return typeof s === 'string' ? s : '' -} - -function timelinePublicTitle(it: PublicTimelineItem): string { - const t = it.title - return typeof t === 'string' && t.trim().length ? t : '未命名动态' -} - -function timelinePublicBody(it: PublicTimelineItem): string { - const b = it.bodyMarkdown - return typeof b === 'string' ? b : '' -} - -function timelineItemKey(e: PublicTimelineItem, i: number): string | number { - return e.id ?? i -} - function socialHref(link: { url?: string; href?: string }): string | undefined { const value = link.url ?? link.href return safeExternalHref(value, { allowMailto: true }) } -function rssHostname(href: string | undefined): string { - if (!href) { - return '' - } - try { - return new URL(href).hostname - } - catch { - return href - } -} - const { data, pending, error } = await useAsyncData( () => `public-profile-${slug.value}`, async () => { @@ -162,21 +110,16 @@ const bioPreviewText = computed(() => { }) const hasBioPreview = computed(() => bioPreviewText.value.length > 0) +const isDetailedMode = computed(() => mode.value === 'detailed') const socialLinks = computed(() => (data.value?.links ?? []) .map(link => ({ ...link, safeHref: socialHref(link) })) - .filter(link => typeof link.safeHref === 'string' && link.safeHref.length > 0), -) - -const readingPreviewItems = computed(() => - readingModule.value.items.slice(0, 2).map(item => ({ - item, - href: rssPublicHref(item), - })), -) -const hasModuleContent = computed(() => - postsModule.value.total > 0 || timelineModule.value.total > 0 || readingModule.value.total > 0, + .filter(link => typeof link.safeHref === 'string' && link.safeHref.length > 0) as Array<{ + label: string + icon?: string + safeHref: string + }>, ) const canonicalUrl = computed(() => @@ -209,168 +152,30 @@ usePageTitle(() => { -
-
-
-
- -
-
-

- {{ data.user.nickname || data.user.publicSlug || slug }} -

-

- @{{ data.user.publicSlug }} -

-
- -
-
- -
- -

- {{ bioPreviewText }} -

-

- 查看完整介绍 -

-
-
- - - -
+ +
diff --git a/docs/superpowers/plans/2026-04-24-public-home-subpages-implementation-plan.md b/docs/superpowers/plans/2026-04-24-public-home-subpages-implementation-plan.md new file mode 100644 index 0000000..68e4d9f --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-public-home-subpages-implementation-plan.md @@ -0,0 +1,317 @@ +# 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` 命名,避免前后不一致。 diff --git a/docs/superpowers/specs/2026-04-24-public-home-subpages-design.md b/docs/superpowers/specs/2026-04-24-public-home-subpages-design.md new file mode 100644 index 0000000..e1d7239 --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-public-home-subpages-design.md @@ -0,0 +1,147 @@ +# 个人主页子页面化能力评审设计(导航中枢模式) + +## 1. 背景与目标 + +当前项目已经具备公开主页与多个公开子页面能力,用户可以从个人主页进入文章、时光机、阅读(RSS)等模块。 +本次评审目标是将 `@slug` 主页明确定位为“导航中枢”,避免主页继续演化为内容聚合长页。 + +目标如下: + +- `@slug` 仅承担入口分发职责,不承担长内容阅读。 +- 文章、时光机、阅读均为一等子页面,具备独立列表与分页能力。 +- 公开、仅链接、私密三种可见性在公开侧严格边界清晰。 +- 新增模块可按统一规范接入,保持长期可扩展性。 + +## 2. 方案结论 + +本次评审采用 **方案 1:导航中枢**。 + +- 主页只展示身份信息、模块入口卡片、每模块 1-2 条预览和“查看全部”。 +- 子页面承载完整列表、详情跳转与后续模块特有能力。 +- 首页不引入模块专属复杂交互(例如筛选、排序、阅读模式)。 + +## 3. 信息架构 + +### 3.1 页面定位 + +- `@slug`:个人站首页(目录页 / 导航中枢) +- `@slug/posts`:文章列表页 +- `@slug/timeline`:时光机列表页 +- `@slug/reading`:阅读(RSS)列表页 + +### 3.2 入口层级 + +- 一级入口:文章 / 时光机 / 阅读模块卡片 +- 二级入口:卡片内最近 1-2 条预览 +- 三级入口:统一“查看全部”按钮进入子页 + +### 3.3 内容边界 + +- 首页每模块最多展示 2 条预览(建议可配置,默认 2)。 +- 首页不展示完整正文,不承载深度浏览流程。 +- 详情阅读、历史翻页、重度交互全部下沉到子页面。 + +### 3.4 可扩展约束 + +新增公开模块必须满足: + +- 提供 `@slug/` 独立子页 +- 在 `@slug` 提供模块卡片入口 +- 提供统一“查看全部”跳转 + +## 4. 入口组件规范 + +### 4.1 卡片结构(统一骨架) + +每个模块卡片应包含: + +- 标题行:模块名 + 公开总数(如“文章 · 24”) +- 描述行:模块价值说明(10-18 字) +- 预览区:最近 1-2 条(标题 + 日期/来源) +- 操作区:主按钮“查看全部”,次按钮按需配置 + +### 4.2 文案规范 + +- 文章:查看全部文章 +- 时光机:查看全部动态 +- 阅读:查看阅读清单 + +文案采用“动作 + 对象”,避免“更多”等弱语义措辞。 + +### 4.3 状态规范 + +- 正常态:展示预览条目 +- 空态:显示“暂无公开内容”并给出轻提示 +- 异常态:显示“暂时不可用”,不暴露技术细节 + +### 4.4 一致性规则 + +- 固定顺序:文章 → 时光机 → 阅读 +- 预览排序:统一按时间倒序 +- 交互一致:卡片整体可点、预览可点、CTA 始终可见 + +## 5. 可见性与数据口径 + +### 5.1 公开侧展示规则 + +- `@slug` 主页仅展示 `public` 内容。 +- `@slug/posts`、`@slug/timeline`、`@slug/reading` 公开访问仅返回 `public` 内容。 +- 首页卡片计数与子页总数均以 `public` 口径计算。 + +### 5.2 仅链接(unlisted)规则 + +- 不进入 `@slug` 首页预览与计数。 +- 不进入公开子页列表。 +- 仅允许通过 token 链接访问(例如 `/p/:publicSlug/t/:shareToken`)。 + +### 5.3 私密(private)规则 + +- 仅后台站主可见。 +- 不进入任何公开 API 返回。 +- 不进入 sitemap 与公开聚合统计。 + +### 5.4 口径一致性要求 + +同一模块在以下位置的“数量与内容范围”必须一致: + +- 首页卡片总数 +- 子页分页总数 +- 公开 SEO/结构化数据中的条目数 + +## 6. 风险评审结论 + +### 6.1 高风险 + +1. 统计口径不一致导致用户信任受损 +2. `unlisted` 意外进入公开列表导致泄漏 + +### 6.2 中风险 + +1. 主页功能膨胀回归为内容聚合页 +2. 主页与子页 SEO 信号冲突 +3. 新模块接入方式不统一导致维护复杂度上升 + +### 6.3 低风险 + +1. 首页聚合请求过重影响首屏性能 + +## 7. 验收标准(最小集) + +1. 访客访问 `@slug` 仅看到公开模块与公开预览。 +2. `unlisted` 内容无法通过首页与公开子页列表发现,仅 token 可达。 +3. 子页分页总数与首页模块计数口径一致。 +4. 模块空态与异常态表现统一,不影响其他模块。 +5. 主页与子页 canonical 各自独立,`unlisted` 不进入 sitemap。 +6. 新模块可按“卡片入口 + 独立子页 + 查看全部”模板接入。 + +## 8. 实施边界(本次仅设计评审) + +本文件仅确认信息架构与评审结论,不直接包含代码改造。 +下一步应进入实现规划阶段,将本设计拆分为可执行任务(路由、数据聚合、组件抽象、SEO 与测试)。 + +## 9. 规格自检(已完成) + +- 已检查:无 TBD/TODO 占位符。 +- 已检查:信息架构、可见性规则、验收标准之间无明显冲突。 +- 已检查:范围聚焦于“导航中枢化评审”,未引入无关重构。 +- 已检查:关键术语(公开/仅链接/私密)定义明确,口径一致。 diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 7a725649ab58a0056e9af30090446ab55b41f961..2870691248a97106267608d80b31ad93d5f6446f 100644 GIT binary patch delta 471 zcmZo@;A&{#njp>SH&Mo!(QjkIviAb4O#IjQ*YG#&WKnlw_KB=>ysa*V`I1yRpf$t!d_37W$H@EyR7GPf2qq+STKO;{7*J2h{KgPv8 zi#R(rD+)|v6l!nRU=m>9#(Z{uv^WMGw?o|ww0yZu2TV-m9*%#G}G!aB?tg+S^W zwHpQ765A4iDz^hwO0#ey6a{W?&t#Ng7K~sP1S{oDY}IbnhMW1wG9~rStTbcUTEE3_=K^RTOMl9!kvK|fxg=i zFol=<1_LwaPX?Y49#+nuK>e>dxBvXfc%PAlfq_AH@}1k7e7-Dv44ksE+L9o%7$!fv YtuF)%&vo~jxj^O^ecT@MhjC3a0FK(3RsaA1 delta 366 zcmZo@;A&{#njp>SJWJIEK4j&1&N%qg^(JoAf=)RrTZO<(^86&R385X zHsO;73!2KsFNzaUR37*aQhDIJ`sSAZ#RAMl+iq$NA#T!Z#u!s