Skip to content

Commit 0e7d463

Browse files
sirjamesgrayclaude
andcommitted
feat: Migrate user profiles from /user/[id] to /u/[username]
- Add new /u/[username] route for username-based profile URLs - Convert /user/[id] to redirect to /u/[username] for backwards compatibility - Update all internal navigation links to use /u/ format - Update SEO files (robots.txt, sitemap generator) for new route - Update email templates with new /u/ URLs - Update link editor and link nodes to use /u/ for user links - Support both /u/ and /user/ patterns in detection hooks This makes usernames more valuable by showing them in the URL, while maintaining full backwards compatibility for existing links. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ca1305a commit 0e7d463

29 files changed

+403
-302
lines changed

app/[id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ export default function ContentPage({ params }: { params: Promise<{ id: string }
234234
method: 'HEAD', // Use HEAD for faster response
235235
});
236236
if (userCheckResponse.ok) {
237-
router.replace(`/user/${cleanId}`);
237+
router.replace(`/u/${cleanId}`);
238238
return;
239239
}
240240
} catch (userError) {
@@ -267,7 +267,7 @@ export default function ContentPage({ params }: { params: Promise<{ id: string }
267267
const userRef = ref(rtdb, `users/${cleanId}`);
268268
const userSnapshot = await get(userRef);
269269
if (userSnapshot.exists()) {
270-
router.replace(`/user/${cleanId}`);
270+
router.replace(`/u/${cleanId}`);
271271
return;
272272
}
273273
} catch (error) {

app/admin/notifications/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,7 @@ export default function AdminEmailsPage() {
10671067
<div className="flex items-center gap-2">
10681068
{log.recipientUsername ? (
10691069
<Link
1070-
href={`/user/${log.recipientUsername}`}
1070+
href={`/u/${log.recipientUsername}`}
10711071
className="font-medium text-sm text-primary hover:underline truncate"
10721072
>
10731073
@{log.recipientUsername}

app/admin/users/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ export default function AdminUsersPage() {
772772
if (notificationType === 'link' && activity.sourceUsername && activity.targetPageTitle && activity.sourcePageTitle) {
773773
return (
774774
<span className="text-xs text-muted-foreground flex flex-wrap items-center gap-1">
775-
<PillLink href={`/user/${activity.sourceUsername}`} type="user">
775+
<PillLink href={`/u/${activity.sourceUsername}`} type="user">
776776
{activity.sourceUsername}
777777
</PillLink>
778778
<span>linked to</span>
@@ -799,7 +799,7 @@ export default function AdminUsersPage() {
799799
if (notificationType === 'user_mention' && activity.sourceUsername && activity.sourcePageTitle) {
800800
return (
801801
<span className="text-xs text-muted-foreground flex flex-wrap items-center gap-1">
802-
<PillLink href={`/user/${activity.sourceUsername}`} type="user">
802+
<PillLink href={`/u/${activity.sourceUsername}`} type="user">
803803
{activity.sourceUsername}
804804
</PillLink>
805805
<span>mentioned this user in</span>
@@ -823,7 +823,7 @@ export default function AdminUsersPage() {
823823
if (followerUsername) {
824824
return (
825825
<span className="text-xs text-muted-foreground flex flex-wrap items-center gap-1">
826-
<PillLink href={`/user/${followerUsername}`} type="user">
826+
<PillLink href={`/u/${followerUsername}`} type="user">
827827
{followerUsername}
828828
</PillLink>
829829
<span>started following this user</span>

app/components/editor/LinkEditorModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ export default function LinkEditorModal({
394394
isEditing,
395395
element: editingLink?.element,
396396
isNew: page.isNew,
397-
url: isUserLink && pageId ? `/user/${pageId}` : (pageId ? `/${pageId}` : undefined),
397+
url: isUserLink && pageId ? `/u/${pageId}` : (pageId ? `/${pageId}` : undefined),
398398
isUser: isUserLink
399399
};
400400

app/components/editor/LinkNode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ const LinkNode: React.FC<LinkNodeProps> = ({ node, canEdit = false, isEditing =
388388
</PillLink>
389389
<span className="text-muted-foreground mx-1">by</span>
390390
<PillLink
391-
href={`/user/${validatedNode.authorUserId}`}
391+
href={`/u/${validatedNode.authorUserId}`}
392392
isPublic={true}
393393
className="user-link"
394394
isEditing={isEditing}

app/components/layout/MobileBottomNav.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,9 @@ export default function MobileBottomNav() {
243243

244244
// Hide on user and group pages (these are ContentPages)
245245
// EXCEPT when viewing your own profile page
246-
if (pathname.startsWith('/user/') || pathname.startsWith('/group/')) {
246+
if (pathname.startsWith('/u/') || pathname.startsWith('/group/')) {
247247
// Show mobile toolbar on your own profile page (since no pledge bar)
248-
if (user?.uid && pathname === `/user/${user.uid}`) {
248+
if (user?.uid && pathname === `/u/${user.uid}`) {
249249
return false; // Show mobile nav on own profile
250250
}
251251
return true; // Hide on other user profiles
@@ -353,7 +353,7 @@ export default function MobileBottomNav() {
353353
const handleProfileClick = () => {
354354
setIsExpanded(false); // Close expanded state
355355
if (user?.uid) {
356-
handleButtonPress('profile', `/user/${user.uid}`);
356+
handleButtonPress('profile', `/u/${user.uid}`);
357357
}
358358
};
359359

@@ -372,7 +372,7 @@ export default function MobileBottomNav() {
372372

373373
// Determine active states for navigation buttons
374374
const isHomeActive = pathname === '/' && !isExpanded;
375-
const isProfileActive = pathname === `/user/${user?.uid}` && !isExpanded;
375+
const isProfileActive = pathname === `/u/${user?.uid}` && !isExpanded;
376376
const isNotificationsActive = pathname === '/notifications' && !isExpanded;
377377
const isMoreActive = isExpanded;
378378

@@ -417,7 +417,7 @@ export default function MobileBottomNav() {
417417
'recents': { icon: Clock, label: 'Recents', href: '/recents' },
418418
'following': { icon: Heart, label: 'Following', href: '/following' },
419419
'notifications': { icon: Bell, label: 'Notifications', href: '/notifications' },
420-
'profile': { icon: User, label: 'Profile', href: user ? `/user/${user.uid}` : '/auth/login' },
420+
'profile': { icon: User, label: 'Profile', href: user ? `/u/${user.uid}` : '/auth/login' },
421421
'settings': { icon: Settings, label: 'Settings', href: '/settings' },
422422
'admin': { icon: Shield, label: 'Admin', href: '/admin' }, // Only shows for admin users
423423
};
@@ -475,8 +475,8 @@ export default function MobileBottomNav() {
475475
onClick: handleProfileClick,
476476
onHover: () => {
477477
if (user?.uid) {
478-
handleButtonHover(`/user/${user.uid}`);
479-
handleNavigationFocus(`/user/${user.uid}`); // 🚀 Preload user profile
478+
handleButtonHover(`/u/${user.uid}`);
479+
handleNavigationFocus(`/u/${user.uid}`); // 🚀 Preload user profile
480480
}
481481
},
482482
isActive: isProfileActive,

app/components/layout/MobileBottomNavUnified.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ export default function MobileBottomNavUnified() {
157157
if (navPageRoutes.includes(pathname)) return false;
158158

159159
// Hide on other user/group pages (show on own profile)
160-
if (pathname.startsWith('/user/') || pathname.startsWith('/group/')) {
161-
if (user?.uid && pathname === `/user/${user.uid}`) return false;
160+
if (pathname.startsWith('/u/') || pathname.startsWith('/group/')) {
161+
if (user?.uid && pathname === `/u/${user.uid}`) return false;
162162
return true;
163163
}
164164

@@ -220,8 +220,8 @@ export default function MobileBottomNavUnified() {
220220
},
221221
profile: {
222222
icon: User,
223-
onClick: () => { setIsExpanded(false); if (user?.uid) router.push(`/user/${user.uid}`); },
224-
isActive: pathname === `/user/${user?.uid}` && !isExpanded,
223+
onClick: () => { setIsExpanded(false); if (user?.uid) router.push(`/u/${user.uid}`); },
224+
isActive: pathname === `/u/${user?.uid}` && !isExpanded,
225225
ariaLabel: 'Profile',
226226
label: 'Profile',
227227
},

app/components/layout/UnifiedSidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ function UnifiedSidebarContent({
298298
'trending-pages': { icon: TrendingUp, label: 'Trending', href: '/trending-pages' },
299299
'following': { icon: Heart, label: 'Following', href: '/following' },
300300
'recents': { icon: Clock, label: 'Recents', href: '/recents' },
301-
'profile': { icon: User, label: 'Profile', href: user ? `/user/${user.uid}` : '/auth/login' },
301+
'profile': { icon: User, label: 'Profile', href: user ? `/u/${user.uid}` : '/auth/login' },
302302
'settings': { icon: Settings, label: 'Settings', href: '/settings' },
303303
// Admin Dashboard - only for admin users
304304
...(isUserAdmin ? { 'admin': { icon: Shield, label: 'Admin', href: '/admin' } } : {}),
@@ -339,7 +339,7 @@ function UnifiedSidebarContent({
339339
if (pathname === item.href) return true;
340340

341341
// Special case for profile - match account profile pages
342-
if (item.label === 'Profile' && user && pathname.startsWith(`/user/${user.uid}`)) {
342+
if (item.label === 'Profile' && user && pathname.startsWith(`/u/${user.uid}`)) {
343343
return true;
344344
}
345345

app/components/payments/EarningsSourceBreakdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ export default function EarningsSourceBreakdown() {
357357
<div key={sponsor.userId} className="flex items-center justify-between py-2 px-3">
358358
<div className="flex items-center gap-2">
359359
<PillLink
360-
href={`/user/${sponsor.userId}`}
360+
href={`/u/${sponsor.userId}`}
361361
isPublic={true}
362362
>
363363
{sponsor.username}
@@ -395,7 +395,7 @@ export default function EarningsSourceBreakdown() {
395395
<div className="flex items-center gap-2 mb-2">
396396
<span className="text-xs text-muted-foreground font-mono">#{index + 1}</span>
397397
<PillLink
398-
href={`/user/${sponsor.userId}`}
398+
href={`/u/${sponsor.userId}`}
399399
isPublic={true}
400400
customOnClick={(e) => e.stopPropagation()}
401401
>

app/components/ui/UsernameBadge.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export function UsernameBadge({
182182
const isInactive = !subscriptionStatus || subscriptionStatus !== 'active';
183183

184184
// Check if we're on a user page
185-
const isOnUserPage = pathname?.startsWith('/user/');
185+
const isOnUserPage = pathname?.startsWith('/u/');
186186

187187
// Generate tooltip text for subscription status
188188
const getTooltipText = () => {
@@ -254,7 +254,7 @@ export function UsernameBadge({
254254
return (
255255
<>
256256
<PillLink
257-
href={`/user/${userId}`}
257+
href={`/u/${userId}`}
258258
variant={pillVariant}
259259
onClick={handleClick}
260260
className={className}
@@ -274,7 +274,7 @@ export function UsernameBadge({
274274
return (
275275
<>
276276
<Link
277-
href={`/user/${userId}`}
277+
href={`/u/${userId}`}
278278
onClick={handleClick}
279279
className={cn(
280280
"username-badge-link inline-flex items-center gap-1 px-2 py-1 rounded-md transition-colors w-fit no-underline",

0 commit comments

Comments
 (0)