From d84abde202302e29edd0e5c43ad530f3aef9105d Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Thu, 5 Mar 2026 11:11:57 +0100 Subject: [PATCH] feat(routing): migrate to AT Protocol-style URLs (#177) * feat(routing): migrate to AT Protocol-style URLs Change topic URLs from /t/{slug}/{rkey} to /{handle}/{rkey} and profile URLs from /u/{handle} to /profile/{handle}. This aligns with AT Protocol conventions used by Bluesky, Frontpage, and other ecosystem projects, and fixes a latent collision bug where rkey-only lookups could return the wrong topic when two users share an rkey. - Add getTopicUrl (author-scoped) and getReplyUrl helpers in format.ts - Add getTopicByAuthorAndRkey and getReplyByAuthorAndRkey API client methods - Restructure app router: [handle]/[rkey] replaces t/[slug]/[rkey], profile/[handle] replaces u/[handle] - Add reply permalink stub at [handle]/[rkey]/[replyAuthor]/[replyRkey] - Update all components to use new URL patterns - Add authorHandle to SearchResult and SearchSuggestion types - Add subjectAuthorDid/Handle to Notification type - Update mock data, MSW handlers, and all tests - Fix JSON-LD URL bug (was using encodeURIComponent(title)) Depends on: barazo-forum/barazo-api#139 * style(formatting): fix Prettier issues in 4 files * fix(a11y): update pa11y-ci and mobile audit URLs for new route structure --- .pa11yci.js | 7 ++-- e2e/accessibility.spec.ts | 6 ++-- e2e/mobile-audit.spec.ts | 4 +-- .../[rkey]/[replyAuthor]/[replyRkey]/page.tsx | 21 ++++++++++++ .../[rkey]/edit/page.test.tsx | 12 +++---- .../[slug] => [handle]}/[rkey]/edit/page.tsx | 25 +++++++++------ .../[slug] => [handle]}/[rkey]/error.test.tsx | 2 +- .../{t/[slug] => [handle]}/[rkey]/error.tsx | 0 .../{t/[slug] => [handle]}/[rkey]/layout.tsx | 0 .../[rkey]/loading.test.tsx | 0 .../{t/[slug] => [handle]}/[rkey]/loading.tsx | 0 .../[slug] => [handle]}/[rkey]/page.test.tsx | 7 ++-- .../{t/[slug] => [handle]}/[rkey]/page.tsx | 26 ++++++++------- src/app/new/page.tsx | 2 +- src/app/notifications/page.test.tsx | 4 +++ src/app/notifications/page.tsx | 4 +-- .../[handle]/edit/page.test.tsx | 4 +-- src/app/{u => profile}/[handle]/edit/page.tsx | 6 ++-- .../{u => profile}/[handle]/error.test.tsx | 2 +- src/app/{u => profile}/[handle]/error.tsx | 0 src/app/{u => profile}/[handle]/page.test.tsx | 0 src/app/{u => profile}/[handle]/page.tsx | 2 +- src/app/search/page.test.tsx | 2 ++ src/app/sitemap.test.ts | 16 +++++----- src/app/sitemap.ts | 4 +-- src/components/auth/user-menu.tsx | 5 ++- src/components/breadcrumbs.test.tsx | 2 +- .../profile/profile-header.test.tsx | 2 +- src/components/profile/profile-header.tsx | 2 +- src/components/reply-card.tsx | 2 +- src/components/search-input.test.tsx | 14 ++++---- src/components/search-input.tsx | 9 ++++-- src/components/search-result-card.tsx | 3 +- src/components/search-suggestion-list.tsx | 4 +-- src/components/topic-card.tsx | 7 ++-- src/components/topic-detail-client.tsx | 7 +++- src/components/topic-view.test.tsx | 2 +- src/components/topic-view.tsx | 2 +- src/components/user-profile-card.test.tsx | 2 +- src/components/user-profile-card.tsx | 2 +- src/lib/api/client.ts | 23 +++++++++++++ src/lib/api/types.ts | 4 +++ src/lib/format.test.ts | 32 +++++++++++++++---- src/lib/format.ts | 18 +++++++++-- src/mocks/data.ts | 11 +++++++ src/mocks/handlers.ts | 13 ++++++++ 46 files changed, 231 insertions(+), 91 deletions(-) create mode 100644 src/app/[handle]/[rkey]/[replyAuthor]/[replyRkey]/page.tsx rename src/app/{t/[slug] => [handle]}/[rkey]/edit/page.test.tsx (88%) rename src/app/{t/[slug] => [handle]}/[rkey]/edit/page.tsx (85%) rename src/app/{t/[slug] => [handle]}/[rkey]/error.test.tsx (97%) rename src/app/{t/[slug] => [handle]}/[rkey]/error.tsx (100%) rename src/app/{t/[slug] => [handle]}/[rkey]/layout.tsx (100%) rename src/app/{t/[slug] => [handle]}/[rkey]/loading.test.tsx (100%) rename src/app/{t/[slug] => [handle]}/[rkey]/loading.tsx (100%) rename src/app/{t/[slug] => [handle]}/[rkey]/page.test.tsx (94%) rename src/app/{t/[slug] => [handle]}/[rkey]/page.tsx (90%) rename src/app/{u => profile}/[handle]/edit/page.test.tsx (98%) rename src/app/{u => profile}/[handle]/edit/page.tsx (98%) rename src/app/{u => profile}/[handle]/error.test.tsx (97%) rename src/app/{u => profile}/[handle]/error.tsx (100%) rename src/app/{u => profile}/[handle]/page.test.tsx (100%) rename src/app/{u => profile}/[handle]/page.tsx (99%) diff --git a/.pa11yci.js b/.pa11yci.js index d4b1366..02a57d5 100644 --- a/.pa11yci.js +++ b/.pa11yci.js @@ -27,13 +27,16 @@ module.exports = { ignore: ['WCAG2AA.Principle2.Guideline2_4.2_4_2.H25.1.NoTitleEl'], }, { - url: 'http://localhost:3000/t/test-topic/abc123/', + url: 'http://localhost:3000/jay.bsky.team/abc123/', ignore: ['WCAG2AA.Principle2.Guideline2_4.2_4_2.H25.1.NoTitleEl'], }, 'http://localhost:3000/search/', 'http://localhost:3000/admin/', 'http://localhost:3000/settings/', - 'http://localhost:3000/u/jay/', + { + url: 'http://localhost:3000/profile/jay/', + ignore: ['WCAG2AA.Principle2.Guideline2_4.2_4_2.H25.1.NoTitleEl'], + }, 'http://localhost:3000/accessibility/', ], } diff --git a/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts index e144784..643e6b2 100644 --- a/e2e/accessibility.spec.ts +++ b/e2e/accessibility.spec.ts @@ -5,7 +5,7 @@ import AxeBuilder from '@axe-core/playwright' * Accessibility tests for all Barazo page types. * Tests against WCAG 2.0 A, WCAG 2.0 AA, and WCAG 2.2 AA criteria. * - * Dynamic pages (/c/[slug], /t/[slug]/[rkey], /u/[handle]) may render + * Dynamic pages (/c/[slug], /[handle]/[rkey], /profile/[handle]) may render * error/fallback pages when no API is running. This is acceptable -- * we test the rendered HTML for a11y violations regardless. */ @@ -15,11 +15,11 @@ const WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag22aa'] as const const pages = [ { name: 'Homepage', path: '/' }, { name: 'Category page', path: '/c/general/' }, - { name: 'Topic page', path: '/t/test-topic/abc123/' }, + { name: 'Topic page', path: '/jay.bsky.team/abc123/' }, { name: 'Search page', path: '/search/' }, { name: 'Admin dashboard', path: '/admin/' }, { name: 'Settings page', path: '/settings/' }, - { name: 'Profile page', path: '/u/jay/' }, + { name: 'Profile page', path: '/profile/jay/' }, { name: 'Accessibility statement', path: '/accessibility/' }, ] diff --git a/e2e/mobile-audit.spec.ts b/e2e/mobile-audit.spec.ts index 4c4f034..2ccae7a 100644 --- a/e2e/mobile-audit.spec.ts +++ b/e2e/mobile-audit.spec.ts @@ -11,7 +11,7 @@ import { join } from 'node:path' const pages = [ { name: 'Homepage', path: '/' }, { name: 'Category page', path: '/c/general/' }, - { name: 'Topic page', path: '/t/test-topic/abc123/' }, + { name: 'Topic page', path: '/jay.bsky.team/abc123/' }, { name: 'Search page', path: '/search/' }, { name: 'Admin dashboard', path: '/admin/' }, { name: 'Admin categories', path: '/admin/categories/' }, @@ -19,7 +19,7 @@ const pages = [ { name: 'Admin settings', path: '/admin/settings/' }, { name: 'Admin users', path: '/admin/users/' }, { name: 'Settings page', path: '/settings/' }, - { name: 'Profile page', path: '/u/jay/' }, + { name: 'Profile page', path: '/profile/jay/' }, { name: 'Accessibility statement', path: '/accessibility/' }, { name: 'Login page', path: '/login/' }, { name: 'Legal - Privacy', path: '/legal/privacy/' }, diff --git a/src/app/[handle]/[rkey]/[replyAuthor]/[replyRkey]/page.tsx b/src/app/[handle]/[rkey]/[replyAuthor]/[replyRkey]/page.tsx new file mode 100644 index 0000000..21ed9d3 --- /dev/null +++ b/src/app/[handle]/[rkey]/[replyAuthor]/[replyRkey]/page.tsx @@ -0,0 +1,21 @@ +/** + * Reply permalink page stub. + * URL: /{handle}/{rkey}/{replyAuthor}/{replyRkey} + * Full implementation deferred -- redirects to the topic page for now. + */ + +import { redirect } from 'next/navigation' + +interface ReplyPermalinkPageProps { + params: Promise<{ + handle: string + rkey: string + replyAuthor: string + replyRkey: string + }> +} + +export default async function ReplyPermalinkPage({ params }: ReplyPermalinkPageProps) { + const { handle, rkey } = await params + redirect(`/${handle}/${rkey}`) +} diff --git a/src/app/t/[slug]/[rkey]/edit/page.test.tsx b/src/app/[handle]/[rkey]/edit/page.test.tsx similarity index 88% rename from src/app/t/[slug]/[rkey]/edit/page.test.tsx rename to src/app/[handle]/[rkey]/edit/page.test.tsx index 0273d11..6b2c476 100644 --- a/src/app/t/[slug]/[rkey]/edit/page.test.tsx +++ b/src/app/[handle]/[rkey]/edit/page.test.tsx @@ -80,17 +80,17 @@ beforeEach(() => { describe('EditTopicPage', () => { it('renders edit topic heading', async () => { - render() + render() expect(await screen.findByRole('heading', { name: 'Edit topic' })).toBeInTheDocument() }) it('pre-populates form with topic data', async () => { - render() + render() expect(await screen.findByDisplayValue('Welcome to Barazo Forums')).toBeInTheDocument() }) it('shows save button', async () => { - render() + render() expect(await screen.findByRole('button', { name: 'Save Changes' })).toBeInTheDocument() }) @@ -111,7 +111,7 @@ describe('EditTopicPage', () => { authFetch: vi.fn(), }) - render() + render() expect(await screen.findByRole('heading', { name: 'Edit topic' })).toBeInTheDocument() }) @@ -132,7 +132,7 @@ describe('EditTopicPage', () => { authFetch: vi.fn(), }) - render() + render() expect(await screen.findByText('You can only edit your own posts.')).toBeInTheDocument() }) @@ -148,7 +148,7 @@ describe('EditTopicPage', () => { authFetch: vi.fn(), }) - render() + render() expect(await screen.findByText('You can only edit your own posts.')).toBeInTheDocument() }) }) diff --git a/src/app/t/[slug]/[rkey]/edit/page.tsx b/src/app/[handle]/[rkey]/edit/page.tsx similarity index 85% rename from src/app/t/[slug]/[rkey]/edit/page.tsx rename to src/app/[handle]/[rkey]/edit/page.tsx index f4a4e3e..98a04d9 100644 --- a/src/app/t/[slug]/[rkey]/edit/page.tsx +++ b/src/app/[handle]/[rkey]/edit/page.tsx @@ -1,6 +1,6 @@ /** * Edit topic page - Edit an existing forum topic. - * URL: /t/{slug}/{rkey}/edit + * URL: /{handle}/{rkey}/edit * Client component (requires auth context + form state). * @see specs/prd-web.md Section 3.2 */ @@ -11,7 +11,7 @@ import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import type { CreateTopicInput, PublicSettings, Topic } from '@/lib/api/types' -import { getTopicByRkey, updateTopic, getPublicSettings } from '@/lib/api/client' +import { getTopicByAuthorAndRkey, updateTopic, getPublicSettings } from '@/lib/api/client' import { getTopicUrl } from '@/lib/format' import { useAuth } from '@/hooks/use-auth' import { ForumLayout } from '@/components/layout/forum-layout' @@ -19,12 +19,13 @@ import { Breadcrumbs } from '@/components/breadcrumbs' import { TopicForm } from '@/components/topic-form' interface EditTopicPageProps { - params: Promise<{ slug: string; rkey: string }> | { slug: string; rkey: string } + params: Promise<{ handle: string; rkey: string }> | { handle: string; rkey: string } } export default function EditTopicPage({ params }: EditTopicPageProps) { const router = useRouter() const { user, isLoading: authLoading, getAccessToken } = useAuth() + const [handle, setHandle] = useState(null) const [rkey, setRkey] = useState(null) const [topic, setTopic] = useState(null) const [loading, setLoading] = useState(true) @@ -42,19 +43,20 @@ export default function EditTopicPage({ params }: EditTopicPageProps) { useEffect(() => { async function resolveParams() { const resolved = params instanceof Promise ? await params : params + setHandle(resolved.handle) setRkey(resolved.rkey) } void resolveParams() }, [params]) - // Load topic once rkey is available + // Load topic once handle and rkey are available useEffect(() => { - if (!rkey) return + if (!handle || !rkey) return let cancelled = false async function loadTopic() { try { - const loaded = await getTopicByRkey(rkey!) + const loaded = await getTopicByAuthorAndRkey(handle!, rkey!) if (!cancelled) { setTopic(loaded) setLoading(false) @@ -70,7 +72,10 @@ export default function EditTopicPage({ params }: EditTopicPageProps) { return () => { cancelled = true } - }, [rkey]) + }, [handle, rkey]) + + const buildTopicUrl = (t: Topic) => + getTopicUrl({ authorHandle: t.author?.handle ?? t.authorDid, rkey: t.rkey }) const handleSubmit = async (values: CreateTopicInput) => { if (!rkey) return @@ -89,7 +94,7 @@ export default function EditTopicPage({ params }: EditTopicPageProps) { }, accessToken ) - router.push(getTopicUrl(updated)) + router.push(buildTopicUrl(updated)) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update topic') setSubmitting(false) @@ -129,7 +134,7 @@ export default function EditTopicPage({ params }: EditTopicPageProps) {

You can only edit your own posts.

- + Back to topic
@@ -145,7 +150,7 @@ export default function EditTopicPage({ params }: EditTopicPageProps) { items={[ { label: 'Home', href: '/' }, { label: topic.category, href: `/c/${topic.category}` }, - { label: topic.title, href: getTopicUrl(topic) }, + { label: topic.title, href: buildTopicUrl(topic) }, { label: 'Edit' }, ]} /> diff --git a/src/app/t/[slug]/[rkey]/error.test.tsx b/src/app/[handle]/[rkey]/error.test.tsx similarity index 97% rename from src/app/t/[slug]/[rkey]/error.test.tsx rename to src/app/[handle]/[rkey]/error.test.tsx index cd8882c..3291d75 100644 --- a/src/app/t/[slug]/[rkey]/error.test.tsx +++ b/src/app/[handle]/[rkey]/error.test.tsx @@ -12,7 +12,7 @@ vi.mock('next/link', () => ({ })) vi.mock('next/navigation', () => ({ - usePathname: () => '/t/test-topic/abc123', + usePathname: () => '/jay.bsky.team/abc123', })) describe('ThreadError', () => { diff --git a/src/app/t/[slug]/[rkey]/error.tsx b/src/app/[handle]/[rkey]/error.tsx similarity index 100% rename from src/app/t/[slug]/[rkey]/error.tsx rename to src/app/[handle]/[rkey]/error.tsx diff --git a/src/app/t/[slug]/[rkey]/layout.tsx b/src/app/[handle]/[rkey]/layout.tsx similarity index 100% rename from src/app/t/[slug]/[rkey]/layout.tsx rename to src/app/[handle]/[rkey]/layout.tsx diff --git a/src/app/t/[slug]/[rkey]/loading.test.tsx b/src/app/[handle]/[rkey]/loading.test.tsx similarity index 100% rename from src/app/t/[slug]/[rkey]/loading.test.tsx rename to src/app/[handle]/[rkey]/loading.test.tsx diff --git a/src/app/t/[slug]/[rkey]/loading.tsx b/src/app/[handle]/[rkey]/loading.tsx similarity index 100% rename from src/app/t/[slug]/[rkey]/loading.tsx rename to src/app/[handle]/[rkey]/loading.tsx diff --git a/src/app/t/[slug]/[rkey]/page.test.tsx b/src/app/[handle]/[rkey]/page.test.tsx similarity index 94% rename from src/app/t/[slug]/[rkey]/page.test.tsx rename to src/app/[handle]/[rkey]/page.test.tsx index 330ebdd..5cbcecc 100644 --- a/src/app/t/[slug]/[rkey]/page.test.tsx +++ b/src/app/[handle]/[rkey]/page.test.tsx @@ -55,7 +55,10 @@ vi.mock('next/navigation', () => ({ const topic = mockTopics[0]! describe('TopicPage', () => { - const defaultParams = Promise.resolve({ slug: 'welcome-to-barazo-forums', rkey: topic.rkey }) + const defaultParams = Promise.resolve({ + handle: topic.author?.handle ?? topic.authorDid, + rkey: topic.rkey, + }) const defaultSearchParams = Promise.resolve({}) it('renders topic title as h2', async () => { @@ -100,7 +103,7 @@ describe('TopicPage', () => { }) it('handles topic not found', async () => { - const params = Promise.resolve({ slug: 'nonexistent', rkey: 'notreal' }) + const params = Promise.resolve({ handle: 'unknown.user', rkey: 'notreal' }) await expect(TopicPage({ params, searchParams: defaultSearchParams })).rejects.toThrow( 'NEXT_NOT_FOUND' ) diff --git a/src/app/t/[slug]/[rkey]/page.tsx b/src/app/[handle]/[rkey]/page.tsx similarity index 90% rename from src/app/t/[slug]/[rkey]/page.tsx rename to src/app/[handle]/[rkey]/page.tsx index 0492c75..2d80ea5 100644 --- a/src/app/t/[slug]/[rkey]/page.tsx +++ b/src/app/[handle]/[rkey]/page.tsx @@ -1,6 +1,6 @@ /** * Topic detail page - Shows topic post and threaded replies. - * URL: /t/{slug}/{rkey} + * URL: /{handle}/{rkey} * Server-side rendered with JSON-LD DiscussionForumPosting. * Maturity-aware: Adult topics are noindex'd, Mature topics get rating meta. * @see specs/prd-web.md Section 3.1, Section 5 @@ -9,13 +9,13 @@ import type { Metadata } from 'next' import { notFound } from 'next/navigation' import { - getTopicByRkey, + getTopicByAuthorAndRkey, getCategories, getReplies, getPublicSettings, ApiError, } from '@/lib/api/client' -import { slugify } from '@/lib/format' +import { getTopicUrl } from '@/lib/format' import { getEffectiveMaturity, getMaturityMeta, @@ -32,15 +32,15 @@ import type { CategoriesResponse, RepliesResponse } from '@/lib/api/types' export const dynamic = 'force-dynamic' interface TopicPageProps { - params: Promise<{ slug: string; rkey: string }> + params: Promise<{ handle: string; rkey: string }> searchParams: Promise<{ page?: string }> } export async function generateMetadata({ params }: TopicPageProps): Promise { - const { rkey } = await params + const { handle, rkey } = await params try { const [topic, publicSettings] = await Promise.all([ - getTopicByRkey(rkey), + getTopicByAuthorAndRkey(handle, rkey), getPublicSettings().catch(() => null), ]) @@ -55,6 +55,7 @@ export async function generateMetadata({ params }: TopicPageProps): Promise 160 ? topic.content.slice(0, 157) + '...' : topic.content + const authorHandle = topic.author?.handle ?? topic.authorDid const communityRating = publicSettings?.maturityRating ?? 'safe' const effectiveMaturity = getEffectiveMaturity(communityRating, topic.categoryMaturityRating) const maturityMeta = getMaturityMeta(effectiveMaturity) @@ -64,7 +65,7 @@ export async function generateMetadata({ params }: TopicPageProps): Promise

{notification.message}

- {notification.subjectTitle && ( + {notification.subjectTitle && notification.subjectAuthorHandle && ( {notification.subjectTitle} diff --git a/src/app/u/[handle]/edit/page.test.tsx b/src/app/profile/[handle]/edit/page.test.tsx similarity index 98% rename from src/app/u/[handle]/edit/page.test.tsx rename to src/app/profile/[handle]/edit/page.test.tsx index e2f8e10..ded1491 100644 --- a/src/app/u/[handle]/edit/page.test.tsx +++ b/src/app/profile/[handle]/edit/page.test.tsx @@ -140,7 +140,7 @@ describe('EditProfilePage', () => { render() await waitFor(() => { - expect(mockReplace).toHaveBeenCalledWith('/u/alex.bsky.team') + expect(mockReplace).toHaveBeenCalledWith('/profile/alex.bsky.team') }) }) }) @@ -164,7 +164,7 @@ describe('EditProfilePage', () => { it('renders cancel link', () => { render() const cancel = screen.getByRole('link', { name: /cancel/i }) - expect(cancel).toHaveAttribute('href', '/u/jay.bsky.team') + expect(cancel).toHaveAttribute('href', '/profile/jay.bsky.team') }) }) diff --git a/src/app/u/[handle]/edit/page.tsx b/src/app/profile/[handle]/edit/page.tsx similarity index 98% rename from src/app/u/[handle]/edit/page.tsx rename to src/app/profile/[handle]/edit/page.tsx index 396d2e7..1b2cf44 100644 --- a/src/app/u/[handle]/edit/page.tsx +++ b/src/app/profile/[handle]/edit/page.tsx @@ -72,7 +72,7 @@ export function EditProfilePage({ params }: EditProfilePageProps) { useEffect(() => { if (!handle || !user) return if (user.handle !== handle) { - router.replace(`/u/${handle}`) + router.replace(`/profile/${handle}`) } }, [handle, user, router]) @@ -125,7 +125,7 @@ export function EditProfilePage({ params }: EditProfilePageProps) { @@ -211,7 +211,7 @@ export function EditProfilePage({ params }: EditProfilePageProps) { {saving ? 'Saving...' : 'Save changes'} Cancel diff --git a/src/app/u/[handle]/error.test.tsx b/src/app/profile/[handle]/error.test.tsx similarity index 97% rename from src/app/u/[handle]/error.test.tsx rename to src/app/profile/[handle]/error.test.tsx index a528c4d..e427302 100644 --- a/src/app/u/[handle]/error.test.tsx +++ b/src/app/profile/[handle]/error.test.tsx @@ -12,7 +12,7 @@ vi.mock('next/link', () => ({ })) vi.mock('next/navigation', () => ({ - usePathname: () => '/u/jay.bsky.team', + usePathname: () => '/profile/jay.bsky.team', })) describe('ProfileError', () => { diff --git a/src/app/u/[handle]/error.tsx b/src/app/profile/[handle]/error.tsx similarity index 100% rename from src/app/u/[handle]/error.tsx rename to src/app/profile/[handle]/error.tsx diff --git a/src/app/u/[handle]/page.test.tsx b/src/app/profile/[handle]/page.test.tsx similarity index 100% rename from src/app/u/[handle]/page.test.tsx rename to src/app/profile/[handle]/page.test.tsx diff --git a/src/app/u/[handle]/page.tsx b/src/app/profile/[handle]/page.tsx similarity index 99% rename from src/app/u/[handle]/page.tsx rename to src/app/profile/[handle]/page.tsx index 571631b..cbd0e4e 100644 --- a/src/app/u/[handle]/page.tsx +++ b/src/app/profile/[handle]/page.tsx @@ -1,6 +1,6 @@ /** * User profile page. - * URL: /u/[handle] + * URL: /profile/[handle] * Displays user info, reputation, recent posts. * Client component (needs param resolution + dynamic data). * @see specs/prd-web.md Section M8 diff --git a/src/app/search/page.test.tsx b/src/app/search/page.test.tsx index aeec0d9..fef7445 100644 --- a/src/app/search/page.test.tsx +++ b/src/app/search/page.test.tsx @@ -106,6 +106,7 @@ describe('SearchPage', () => { uri: 'at://did:plc:user/forum.barazo.topic.post/abc', rkey: 'abc', authorDid: 'did:plc:user', + authorHandle: 'jay.bsky.team', title: 'Welcome to Barazo', content: 'First topic on barazo forums.', category: 'general', @@ -160,6 +161,7 @@ describe('SearchPage', () => { uri: 'at://did:plc:user/forum.barazo.reply.post/xyz', rkey: 'xyz', authorDid: 'did:plc:user', + authorHandle: 'jay.bsky.team', title: null, content: 'This is a reply about the topic.', category: null, diff --git a/src/app/sitemap.test.ts b/src/app/sitemap.test.ts index 5373ead..949c0ab 100644 --- a/src/app/sitemap.test.ts +++ b/src/app/sitemap.test.ts @@ -113,16 +113,16 @@ describe('sitemap', () => { expect(urls).toContain('https://barazo.forum/c/introductions') }) - it('includes topic pages with slug and rkey', async () => { + it('includes topic pages with author handle and rkey', async () => { const result = await sitemap() const urls = result.map((entry) => entry.url) - expect(urls).toContain('https://barazo.forum/t/hello-world/abc123') - expect(urls).toContain('https://barazo.forum/t/second-topic/def456') + expect(urls).toContain('https://barazo.forum/did:plc:author1/abc123') + expect(urls).toContain('https://barazo.forum/did:plc:author2/def456') }) it('sets lastModified for topics', async () => { const result = await sitemap() - const topicEntry = result.find((entry) => entry.url.includes('/t/hello-world/abc123')) + const topicEntry = result.find((entry) => entry.url.includes('/did:plc:author1/abc123')) expect(topicEntry?.lastModified).toBeDefined() }) @@ -134,7 +134,7 @@ describe('sitemap', () => { const categoryEntry = result.find((entry) => entry.url.includes('/c/general')) expect(categoryEntry?.changeFrequency).toBe('daily') - const topicEntry = result.find((entry) => entry.url.includes('/t/hello-world/abc123')) + const topicEntry = result.find((entry) => entry.url.includes('/did:plc:author1/abc123')) expect(topicEntry?.changeFrequency).toBe('weekly') }) @@ -146,7 +146,7 @@ describe('sitemap', () => { const categoryEntry = result.find((entry) => entry.url.includes('/c/general')) expect(categoryEntry?.priority).toBe(0.8) - const topicEntry = result.find((entry) => entry.url.includes('/t/hello-world/abc123')) + const topicEntry = result.find((entry) => entry.url.includes('/did:plc:author1/abc123')) expect(topicEntry?.priority).toBe(0.6) }) @@ -255,8 +255,8 @@ describe('sitemap', () => { const result = await sitemap() const urls = result.map((entry) => entry.url) - expect(urls).toContain('https://barazo.forum/t/safe-topic/safe1') - expect(urls).not.toContain('https://barazo.forum/t/adult-topic/adult1') + expect(urls).toContain('https://barazo.forum/did:plc:author1/safe1') + expect(urls).not.toContain('https://barazo.forum/did:plc:author2/adult1') }) it('includes mature-rated categories in sitemap', async () => { diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 05cb431..ca44962 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -7,7 +7,7 @@ import type { MetadataRoute } from 'next' import { getCategories, getTopics } from '@/lib/api/client' -import { slugify } from '@/lib/format' +import { getTopicUrl } from '@/lib/format' import type { CategoryTreeNode } from '@/lib/api/types' const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://barazo.forum' @@ -58,7 +58,7 @@ export default async function sitemap(): Promise { for (const topic of topicsResult.topics) { if (topic.categoryMaturityRating === 'adult') continue entries.push({ - url: `${SITE_URL}/t/${slugify(topic.title)}/${topic.rkey}`, + url: `${SITE_URL}${getTopicUrl({ authorHandle: topic.author?.handle ?? topic.authorDid, rkey: topic.rkey })}`, lastModified: new Date(topic.lastActivityAt), changeFrequency: 'weekly', priority: 0.6, diff --git a/src/components/auth/user-menu.tsx b/src/components/auth/user-menu.tsx index 306d7ba..5e38510 100644 --- a/src/components/auth/user-menu.tsx +++ b/src/components/auth/user-menu.tsx @@ -71,7 +71,10 @@ export function UserMenu() { - +