Restructure share routes to fix client-side navigation

- Change from [...shareId] catch-all to [slug]/[[...path]] structure
- Layout with ShareProvider now persists across sub-route navigation
- Add slug to ShareData context (separate from shareId UUID)
- Links now use slug instead of UUID for proper routing
- Remove unused ShareFooter and ShareHeader files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mike Cao 2026-01-28 23:32:51 -08:00
parent d028bfa1f5
commit c9e14f3bce
8 changed files with 26 additions and 76 deletions

View file

@ -7,6 +7,7 @@ import type { WhiteLabel } from '@/lib/types';
export interface ShareData { export interface ShareData {
shareId: string; shareId: string;
slug: string;
websiteId: string; websiteId: string;
parameters: any; parameters: any;
token: string; token: string;
@ -31,8 +32,8 @@ const ALL_SECTION_IDS = [
'attribution', 'attribution',
]; ];
export function ShareProvider({ shareId, children }: { shareId: string; children: ReactNode }) { export function ShareProvider({ slug, children }: { slug: string; children: ReactNode }) {
const { share, isLoading, isFetching } = useShareTokenQuery(shareId); const { share, isLoading, isFetching } = useShareTokenQuery(slug);
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const path = pathname.split('/')[3]; const path = pathname.split('/')[3];
@ -48,9 +49,9 @@ export function ShareProvider({ shareId, children }: { shareId: string; children
useEffect(() => { useEffect(() => {
if (shouldRedirect) { if (shouldRedirect) {
router.replace(`/share/${shareId}/${allowedSections[0]}`); router.replace(`/share/${slug}/${allowedSections[0]}`);
} }
}, [shouldRedirect, shareId, allowedSections, router]); }, [shouldRedirect, slug, allowedSections, router]);
if (isFetching && isLoading) { if (isFetching && isLoading) {
return <Loading placement="absolute" />; return <Loading placement="absolute" />;
@ -60,5 +61,5 @@ export function ShareProvider({ shareId, children }: { shareId: string; children
return null; return null;
} }
return <ShareContext.Provider value={share}>{children}</ShareContext.Provider>; return <ShareContext.Provider value={{ ...share, slug }}>{children}</ShareContext.Provider>;
} }

View file

@ -1,23 +0,0 @@
import { Row, Text } from '@umami/react-zen';
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
import type { WhiteLabel } from '@/lib/types';
export function ShareFooter({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
if (whiteLabel) {
return (
<Row as="footer" paddingY="6" justifyContent="flex-end">
<a href={whiteLabel.url} target="_blank">
<Text weight="bold">{whiteLabel.name}</Text>
</a>
</Row>
);
}
return (
<Row as="footer" paddingY="6" justifyContent="flex-end">
<a href={HOMEPAGE_URL} target="_blank">
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
</a>
</Row>
);
}

View file

@ -1,33 +0,0 @@
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { Logo } from '@/components/svg';
import type { WhiteLabel } from '@/lib/types';
export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
const logoUrl = whiteLabel?.url || 'https://umami.is';
const logoName = whiteLabel?.name || 'umami';
const logoImage = whiteLabel?.image;
return (
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
<a href={logoUrl} target="_blank" rel="noopener">
<Row alignItems="center" gap>
{logoImage ? (
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
) : (
<Icon>
<Logo />
</Icon>
)}
<Text weight="bold">{logoName}</Text>
</Row>
</a>
<Row alignItems="center" gap>
<ThemeButton />
<LanguageButton />
<PreferencesButton />
</Row>
</Row>
);
}

View file

@ -1,13 +0,0 @@
import { ShareProvider } from '@/app/share/ShareProvider';
import { SharePage } from './SharePage';
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
const { shareId } = await params;
const [slug] = shareId;
return (
<ShareProvider shareId={slug}>
<SharePage />
</ShareProvider>
);
}

View file

@ -10,13 +10,13 @@ export function ShareNav({ onItemClick }: { onItemClick?: () => void }) {
const share = useShare(); const share = useShare();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation(); const { pathname } = useNavigation();
const { shareId, parameters, whiteLabel } = share; const { slug, parameters, whiteLabel } = share;
const logoUrl = whiteLabel?.url || 'https://umami.is'; const logoUrl = whiteLabel?.url || 'https://umami.is';
const logoName = whiteLabel?.name || 'umami'; const logoName = whiteLabel?.name || 'umami';
const logoImage = whiteLabel?.image; const logoImage = whiteLabel?.image;
const renderPath = (path: string) => `/share/${shareId}${path}`; const renderPath = (path: string) => `/share/${slug}${path}`;
const allItems = [ const allItems = [
{ {

View file

@ -0,0 +1,5 @@
import { SharePage } from './SharePage';
export default function () {
return <SharePage />;
}

View file

@ -0,0 +1,13 @@
import { ShareProvider } from '@/app/share/ShareProvider';
export default async function ({
params,
children,
}: {
params: Promise<{ slug: string }>;
children: React.ReactNode;
}) {
const { slug } = await params;
return <ShareProvider slug={slug}>{children}</ShareProvider>;
}