From 9339383497405050526973860257f56ecb554172 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 28 Jan 2026 17:18:03 -0800 Subject: [PATCH 01/13] remove www. prefix from hostname during comparison. Closes #3256 --- src/lib/prisma.ts | 2 +- src/queries/sql/getValues.ts | 2 +- src/queries/sql/pageviews/getPageviewExpandedMetrics.ts | 2 +- src/queries/sql/pageviews/getPageviewMetrics.ts | 2 +- src/queries/sql/reports/getAttribution.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index bfd007d1..c2f98b57 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -108,7 +108,7 @@ function getFilterQuery(filters: Record, options: QueryOptions = {} if (name === 'referrer') { arr.push( - `and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`, + `and (website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') or website_event.referrer_domain is null)`, ); } } diff --git a/src/queries/sql/getValues.ts b/src/queries/sql/getValues.ts index cc6bb7d2..8573335b 100644 --- a/src/queries/sql/getValues.ts +++ b/src/queries/sql/getValues.ts @@ -23,7 +23,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query let excludeDomain = ''; if (column === 'referrer_domain') { - excludeDomain = `and website_event.referrer_domain != website_event.hostname + excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') and website_event.referrer_domain != ''`; } diff --git a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts index ccb0be53..54164973 100644 --- a/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewExpandedMetrics.ts @@ -50,7 +50,7 @@ async function relationalQuery( let excludeDomain = ''; if (column === 'referrer_domain') { - excludeDomain = `and website_event.referrer_domain != website_event.hostname + excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') and website_event.referrer_domain != ''`; if (type === 'domain') { column = toPostgresGroupedReferrer(GROUPED_DOMAINS); diff --git a/src/queries/sql/pageviews/getPageviewMetrics.ts b/src/queries/sql/pageviews/getPageviewMetrics.ts index 9d4f6278..b41ea058 100644 --- a/src/queries/sql/pageviews/getPageviewMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewMetrics.ts @@ -46,7 +46,7 @@ async function relationalQuery( let excludeDomain = ''; if (column === 'referrer_domain') { - excludeDomain = `and website_event.referrer_domain != website_event.hostname + excludeDomain = `and website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') and website_event.referrer_domain != ''`; } diff --git a/src/queries/sql/reports/getAttribution.ts b/src/queries/sql/reports/getAttribution.ts index 29068f7d..088b5b24 100644 --- a/src/queries/sql/reports/getAttribution.ts +++ b/src/queries/sql/reports/getAttribution.ts @@ -142,7 +142,7 @@ async function relationalQuery( ${ currency ? '' - : `and we.referrer_domain != hostname + : `and we.referrer_domain != regexp_replace(we.hostname, '^www.', '') and we.referrer_domain != ''` } group by 1 From 2df24a78ca16d390efee54bd69c494c5407d3e27 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 28 Jan 2026 18:05:34 -0800 Subject: [PATCH 02/13] bug fix. remove All time filter for websites with no data. --- src/components/input/WebsiteDateFilter.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index a76058ec..58e596f5 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -31,9 +31,11 @@ export function WebsiteDateFilter({ const showCompare = allowCompare && !isAllTime; const websiteDateRange = useDateRangeQuery(websiteId); + const { startDate, endDate } = websiteDateRange; + const hasData = startDate && endDate; const handleChange = (date: string) => { - if (date === 'all') { + if (date === 'all' && hasData) { router.push( updateParams({ date: `${getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate)}:all`, @@ -78,7 +80,7 @@ export function WebsiteDateFilter({ From 018e76b067a8e194c9fe39dbe0421aa8bbd9b4c8 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 28 Jan 2026 19:24:13 -0800 Subject: [PATCH 03/13] Fixed website nav. --- src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx index 7260a7ea..95628dd3 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteLayout.tsx @@ -9,11 +9,10 @@ import { WebsiteNav } from './WebsiteNav'; export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { return ( - + Date: Wed, 28 Jan 2026 23:10:42 -0800 Subject: [PATCH 04/13] Redesigned share page. --- src/app/share/ShareProvider.tsx | 29 +++++++ src/app/share/[...shareId]/ShareNav.tsx | 62 ++++++++++----- src/app/share/[...shareId]/SharePage.tsx | 78 +++++++------------ src/app/share/[...shareId]/page.tsx | 9 ++- src/components/common/SideMenu.tsx | 9 ++- src/components/hooks/context/useShare.ts | 6 ++ src/components/hooks/index.ts | 1 + .../hooks/queries/useShareTokenQuery.ts | 18 ++--- src/store/app.ts | 6 +- 9 files changed, 130 insertions(+), 88 deletions(-) create mode 100644 src/app/share/ShareProvider.tsx create mode 100644 src/components/hooks/context/useShare.ts diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx new file mode 100644 index 00000000..fe4b5b3a --- /dev/null +++ b/src/app/share/ShareProvider.tsx @@ -0,0 +1,29 @@ +'use client'; +import { Loading } from '@umami/react-zen'; +import { createContext, type ReactNode } from 'react'; +import { useShareTokenQuery } from '@/components/hooks'; +import type { WhiteLabel } from '@/lib/types'; + +export interface ShareData { + shareId: string; + websiteId: string; + parameters: any; + token: string; + whiteLabel?: WhiteLabel; +} + +export const ShareContext = createContext(null); + +export function ShareProvider({ shareId, children }: { shareId: string; children: ReactNode }) { + const { share, isLoading, isFetching } = useShareTokenQuery(shareId); + + if (isFetching && isLoading) { + return ; + } + + if (!share) { + return null; + } + + return {children}; +} diff --git a/src/app/share/[...shareId]/ShareNav.tsx b/src/app/share/[...shareId]/ShareNav.tsx index b494046d..171539ac 100644 --- a/src/app/share/[...shareId]/ShareNav.tsx +++ b/src/app/share/[...shareId]/ShareNav.tsx @@ -1,21 +1,20 @@ -'use client'; -import { Column } from '@umami/react-zen'; +import { Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen'; import { SideMenu } from '@/components/common/SideMenu'; -import { useMessages, useNavigation } from '@/components/hooks'; +import { useMessages, useNavigation, useShare } from '@/components/hooks'; import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons'; -import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg'; +import { LanguageButton } from '@/components/input/LanguageButton'; +import { PreferencesButton } from '@/components/input/PreferencesButton'; +import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg'; -export function ShareNav({ - shareId, - parameters, - onItemClick, -}: { - shareId: string; - parameters: Record; - onItemClick?: () => void; -}) { +export function ShareNav({ onItemClick }: { onItemClick?: () => void }) { + const share = useShare(); const { formatMessage, labels } = useMessages(); const { pathname } = useNavigation(); + const { shareId, parameters, whiteLabel } = share; + + const logoUrl = whiteLabel?.url || 'https://umami.is'; + const logoName = whiteLabel?.name || 'umami'; + const logoImage = whiteLabel?.image; const renderPath = (path: string) => `/share/${shareId}${path}`; @@ -131,13 +130,36 @@ export function ShareNav({ .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id; return ( - - + + + + + {logoImage ? ( + {logoName} + ) : ( + + + + )} + {logoName} + + + + + + + + + + + + + ); } diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index 91a8b298..aa0361c6 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -1,6 +1,6 @@ 'use client'; import { Column, Grid, Row, useTheme } from '@umami/react-zen'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useMemo } from 'react'; import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage'; import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage'; @@ -18,10 +18,8 @@ import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader'; import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { PageBody } from '@/components/common/PageBody'; -import { useShareTokenQuery } from '@/components/hooks'; +import { useShare } from '@/components/hooks'; import { MobileMenuButton } from '@/components/input/MobileMenuButton'; -import { ShareFooter } from './ShareFooter'; -import { ShareHeader } from './ShareHeader'; import { ShareNav } from './ShareNav'; const PAGE_COMPONENTS: Record> = { @@ -58,17 +56,20 @@ const ALL_SECTION_IDS = [ 'attribution', ]; -export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) { - const { shareToken, isLoading } = useShareTokenQuery(shareId); +export function SharePage({ shareId }: { shareId: string }) { + const share = useShare(); const { setTheme } = useTheme(); const router = useRouter(); + const pathname = usePathname(); + const path = pathname.split('/')[3]; + const { websiteId, parameters = {} } = share; // Calculate allowed sections const allowedSections = useMemo(() => { - if (!shareToken?.parameters) return []; - const params = shareToken.parameters; + if (!share?.parameters) return []; + const params = share.parameters; return ALL_SECTION_IDS.filter(id => params[id] !== false); - }, [shareToken?.parameters]); + }, [share?.parameters]); useEffect(() => { const url = new URL(window?.location?.href); @@ -90,12 +91,6 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri } }, [allowedSections, shareId, path, router]); - if (isLoading || !shareToken) { - return null; - } - - const { websiteId, parameters = {}, whiteLabel } = shareToken; - // Redirect to only allowed section - return null while redirecting if ( allowedSections.length === 1 && @@ -116,40 +111,25 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage; return ( - - - - - - {({ close }) => { - return ; - }} - - - - - - + + + + {({ close }) => { + return ; + }} + + + + + + + + + + - - - - - - - - - - - - - + + + ); } diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx index 3a21f836..7d080fe5 100644 --- a/src/app/share/[...shareId]/page.tsx +++ b/src/app/share/[...shareId]/page.tsx @@ -1,8 +1,13 @@ +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, ...path] = shareId; + const [slug] = shareId; - return ; + return ( + + + + ); } diff --git a/src/components/common/SideMenu.tsx b/src/components/common/SideMenu.tsx index 92ff798a..dd716d78 100644 --- a/src/components/common/SideMenu.tsx +++ b/src/components/common/SideMenu.tsx @@ -7,6 +7,7 @@ import { NavMenuItem, type NavMenuProps, Row, + Text, } from '@umami/react-zen'; import Link from 'next/link'; @@ -42,9 +43,11 @@ export function SideMenu({ return ( - - {label} - + + + {label} + + ); }); diff --git a/src/components/hooks/context/useShare.ts b/src/components/hooks/context/useShare.ts new file mode 100644 index 00000000..c7493c66 --- /dev/null +++ b/src/components/hooks/context/useShare.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { ShareContext } from '@/app/share/ShareProvider'; + +export function useShare() { + return useContext(ShareContext); +} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index f47f11f0..89cb904b 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -3,6 +3,7 @@ // Context hooks export * from './context/useLink'; export * from './context/usePixel'; +export * from './context/useShare'; export * from './context/useTeam'; export * from './context/useUser'; export * from './context/useWebsite'; diff --git a/src/components/hooks/queries/useShareTokenQuery.ts b/src/components/hooks/queries/useShareTokenQuery.ts index 446e33da..28820be0 100644 --- a/src/components/hooks/queries/useShareTokenQuery.ts +++ b/src/components/hooks/queries/useShareTokenQuery.ts @@ -1,25 +1,21 @@ -import { setShareToken, useApp } from '@/store/app'; +import { setShare, useApp } from '@/store/app'; import { useApi } from '../useApi'; -const selector = (state: { shareToken: string }) => state.shareToken; +const selector = state => state.share; -export function useShareTokenQuery(slug: string): { - shareToken: any; - isLoading?: boolean; - error?: Error; -} { - const shareToken = useApp(selector); +export function useShareTokenQuery(slug: string) { + const share = useApp(selector); const { get, useQuery } = useApi(); - const { isLoading, error } = useQuery({ + const query = useQuery({ queryKey: ['share', slug], queryFn: async () => { const data = await get(`/share/${slug}`); - setShareToken(data); + setShare(data); return data; }, }); - return { shareToken, isLoading, error }; + return { share, ...query }; } diff --git a/src/store/app.ts b/src/store/app.ts index bb54e565..e2a54a80 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -16,7 +16,7 @@ const initialState = { theme: getItem(THEME_CONFIG) || DEFAULT_THEME, timezone: getItem(TIMEZONE_CONFIG) || getTimezone(), dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE, - shareToken: null, + share: null, user: null, config: null, }; @@ -31,8 +31,8 @@ export function setLocale(locale: string) { store.setState({ locale }); } -export function setShareToken(shareToken: string) { - store.setState({ shareToken }); +export function setShare(share: object) { + store.setState({ share }); } export function setUser(user: object) { From d028bfa1f51dcc926db2ebb421fa2fa4a106e286 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 28 Jan 2026 23:18:05 -0800 Subject: [PATCH 05/13] Move share page redirect logic to ShareProvider Centralizes the single-section redirect logic in ShareProvider instead of SharePage, reducing useEffect complexity and preventing children from rendering during redirect. Co-Authored-By: Claude Opus 4.5 --- src/app/share/ShareProvider.tsx | 39 +++++++++++++++++- src/app/share/[...shareId]/SharePage.tsx | 51 ++---------------------- src/app/share/[...shareId]/page.tsx | 2 +- 3 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx index fe4b5b3a..5b0ca12e 100644 --- a/src/app/share/ShareProvider.tsx +++ b/src/app/share/ShareProvider.tsx @@ -1,6 +1,7 @@ 'use client'; import { Loading } from '@umami/react-zen'; -import { createContext, type ReactNode } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { createContext, type ReactNode, useEffect } from 'react'; import { useShareTokenQuery } from '@/components/hooks'; import type { WhiteLabel } from '@/lib/types'; @@ -14,14 +15,48 @@ export interface ShareData { export const ShareContext = createContext(null); +const ALL_SECTION_IDS = [ + 'overview', + 'events', + 'sessions', + 'realtime', + 'compare', + 'breakdown', + 'goals', + 'funnels', + 'journeys', + 'retention', + 'utm', + 'revenue', + 'attribution', +]; + export function ShareProvider({ shareId, children }: { shareId: string; children: ReactNode }) { const { share, isLoading, isFetching } = useShareTokenQuery(shareId); + const router = useRouter(); + const pathname = usePathname(); + const path = pathname.split('/')[3]; + + const allowedSections = share?.parameters + ? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false) + : []; + + const shouldRedirect = + allowedSections.length === 1 && + allowedSections[0] !== 'overview' && + (path === undefined || path === '' || path === 'overview'); + + useEffect(() => { + if (shouldRedirect) { + router.replace(`/share/${shareId}/${allowedSections[0]}`); + } + }, [shouldRedirect, shareId, allowedSections, router]); if (isFetching && isLoading) { return ; } - if (!share) { + if (!share || shouldRedirect) { return null; } diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index aa0361c6..5d93fd96 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -1,7 +1,7 @@ 'use client'; import { Column, Grid, Row, useTheme } from '@umami/react-zen'; -import { usePathname, useRouter } from 'next/navigation'; -import { useEffect, useMemo } from 'react'; +import { usePathname } from 'next/navigation'; +import { useEffect } from 'react'; import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage'; import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage'; import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage'; @@ -39,38 +39,13 @@ const PAGE_COMPONENTS: Record attribution: AttributionPage, }; -// All section IDs that can be enabled/disabled via parameters -const ALL_SECTION_IDS = [ - 'overview', - 'events', - 'sessions', - 'realtime', - 'compare', - 'breakdown', - 'goals', - 'funnels', - 'journeys', - 'retention', - 'utm', - 'revenue', - 'attribution', -]; - -export function SharePage({ shareId }: { shareId: string }) { +export function SharePage() { const share = useShare(); const { setTheme } = useTheme(); - const router = useRouter(); const pathname = usePathname(); const path = pathname.split('/')[3]; const { websiteId, parameters = {} } = share; - // Calculate allowed sections - const allowedSections = useMemo(() => { - if (!share?.parameters) return []; - const params = share.parameters; - return ALL_SECTION_IDS.filter(id => params[id] !== false); - }, [share?.parameters]); - useEffect(() => { const url = new URL(window?.location?.href); const theme = url.searchParams.get('theme'); @@ -80,26 +55,6 @@ export function SharePage({ shareId }: { shareId: string }) { } }, []); - // Redirect to the only allowed section if there's just one and we're on the base path - useEffect(() => { - if ( - allowedSections.length === 1 && - allowedSections[0] !== 'overview' && - (path === '' || path === 'overview') - ) { - router.replace(`/share/${shareId}/${allowedSections[0]}`); - } - }, [allowedSections, shareId, path, router]); - - // Redirect to only allowed section - return null while redirecting - if ( - allowedSections.length === 1 && - allowedSections[0] !== 'overview' && - (path === '' || path === 'overview') - ) { - return null; - } - // Check if the requested path is allowed const pageKey = path || ''; const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false; diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx index 7d080fe5..f21e1979 100644 --- a/src/app/share/[...shareId]/page.tsx +++ b/src/app/share/[...shareId]/page.tsx @@ -7,7 +7,7 @@ export default async function ({ params }: { params: Promise<{ shareId: string[] return ( - + ); } From c9e14f3bce0191a500ba8bb091cf0709f27972f4 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 28 Jan 2026 23:32:51 -0800 Subject: [PATCH 06/13] 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 --- src/app/share/ShareProvider.tsx | 11 ++++--- src/app/share/[...shareId]/ShareFooter.tsx | 23 ------------- src/app/share/[...shareId]/ShareHeader.tsx | 33 ------------------- src/app/share/[...shareId]/page.tsx | 13 -------- .../[[...path]]}/ShareNav.tsx | 4 +-- .../[[...path]]}/SharePage.tsx | 0 src/app/share/[slug]/[[...path]]/page.tsx | 5 +++ src/app/share/[slug]/layout.tsx | 13 ++++++++ 8 files changed, 26 insertions(+), 76 deletions(-) delete mode 100644 src/app/share/[...shareId]/ShareFooter.tsx delete mode 100644 src/app/share/[...shareId]/ShareHeader.tsx delete mode 100644 src/app/share/[...shareId]/page.tsx rename src/app/share/{[...shareId] => [slug]/[[...path]]}/ShareNav.tsx (97%) rename src/app/share/{[...shareId] => [slug]/[[...path]]}/SharePage.tsx (100%) create mode 100644 src/app/share/[slug]/[[...path]]/page.tsx create mode 100644 src/app/share/[slug]/layout.tsx diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx index 5b0ca12e..9862a974 100644 --- a/src/app/share/ShareProvider.tsx +++ b/src/app/share/ShareProvider.tsx @@ -7,6 +7,7 @@ import type { WhiteLabel } from '@/lib/types'; export interface ShareData { shareId: string; + slug: string; websiteId: string; parameters: any; token: string; @@ -31,8 +32,8 @@ const ALL_SECTION_IDS = [ 'attribution', ]; -export function ShareProvider({ shareId, children }: { shareId: string; children: ReactNode }) { - const { share, isLoading, isFetching } = useShareTokenQuery(shareId); +export function ShareProvider({ slug, children }: { slug: string; children: ReactNode }) { + const { share, isLoading, isFetching } = useShareTokenQuery(slug); const router = useRouter(); const pathname = usePathname(); const path = pathname.split('/')[3]; @@ -48,9 +49,9 @@ export function ShareProvider({ shareId, children }: { shareId: string; children useEffect(() => { 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) { return ; @@ -60,5 +61,5 @@ export function ShareProvider({ shareId, children }: { shareId: string; children return null; } - return {children}; + return {children}; } diff --git a/src/app/share/[...shareId]/ShareFooter.tsx b/src/app/share/[...shareId]/ShareFooter.tsx deleted file mode 100644 index 5348ac63..00000000 --- a/src/app/share/[...shareId]/ShareFooter.tsx +++ /dev/null @@ -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 ( - - - {whiteLabel.name} - - - ); - } - - return ( - - - umami {`v${CURRENT_VERSION}`} - - - ); -} diff --git a/src/app/share/[...shareId]/ShareHeader.tsx b/src/app/share/[...shareId]/ShareHeader.tsx deleted file mode 100644 index abd8511d..00000000 --- a/src/app/share/[...shareId]/ShareHeader.tsx +++ /dev/null @@ -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 ( - - - - {logoImage ? ( - {logoName} - ) : ( - - - - )} - {logoName} - - - - - - - - - ); -} diff --git a/src/app/share/[...shareId]/page.tsx b/src/app/share/[...shareId]/page.tsx deleted file mode 100644 index f21e1979..00000000 --- a/src/app/share/[...shareId]/page.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/src/app/share/[...shareId]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx similarity index 97% rename from src/app/share/[...shareId]/ShareNav.tsx rename to src/app/share/[slug]/[[...path]]/ShareNav.tsx index 171539ac..fa100b17 100644 --- a/src/app/share/[...shareId]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -10,13 +10,13 @@ export function ShareNav({ onItemClick }: { onItemClick?: () => void }) { const share = useShare(); const { formatMessage, labels } = useMessages(); const { pathname } = useNavigation(); - const { shareId, parameters, whiteLabel } = share; + const { slug, parameters, whiteLabel } = share; const logoUrl = whiteLabel?.url || 'https://umami.is'; const logoName = whiteLabel?.name || 'umami'; const logoImage = whiteLabel?.image; - const renderPath = (path: string) => `/share/${shareId}${path}`; + const renderPath = (path: string) => `/share/${slug}${path}`; const allItems = [ { diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[slug]/[[...path]]/SharePage.tsx similarity index 100% rename from src/app/share/[...shareId]/SharePage.tsx rename to src/app/share/[slug]/[[...path]]/SharePage.tsx diff --git a/src/app/share/[slug]/[[...path]]/page.tsx b/src/app/share/[slug]/[[...path]]/page.tsx new file mode 100644 index 00000000..872cf267 --- /dev/null +++ b/src/app/share/[slug]/[[...path]]/page.tsx @@ -0,0 +1,5 @@ +import { SharePage } from './SharePage'; + +export default function () { + return ; +} diff --git a/src/app/share/[slug]/layout.tsx b/src/app/share/[slug]/layout.tsx new file mode 100644 index 00000000..7a5f4599 --- /dev/null +++ b/src/app/share/[slug]/layout.tsx @@ -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 {children}; +} From 4e8be724ac44ec64bacc4c6f19820addf4548e24 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 28 Jan 2026 23:38:07 -0800 Subject: [PATCH 07/13] Handle domain name in share URL path Skip domain-like segments (containing dots) when parsing the share path. e.g., /share/slug/aol.com is treated as /share/slug /share/slug/aol.com/events is treated as /share/slug/events Co-Authored-By: Claude Opus 4.5 --- src/app/share/ShareProvider.tsx | 14 +++++++++++++- src/app/share/[slug]/[[...path]]/SharePage.tsx | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx index 9862a974..b83d3794 100644 --- a/src/app/share/ShareProvider.tsx +++ b/src/app/share/ShareProvider.tsx @@ -32,11 +32,23 @@ const ALL_SECTION_IDS = [ 'attribution', ]; +function getSharePath(pathname: string) { + const segments = pathname.split('/'); + const firstSegment = segments[3]; + + // If first segment looks like a domain name, skip it + if (firstSegment?.includes('.')) { + return segments[4]; + } + + return firstSegment; +} + export function ShareProvider({ slug, children }: { slug: string; children: ReactNode }) { const { share, isLoading, isFetching } = useShareTokenQuery(slug); const router = useRouter(); const pathname = usePathname(); - const path = pathname.split('/')[3]; + const path = getSharePath(pathname); const allowedSections = share?.parameters ? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false) diff --git a/src/app/share/[slug]/[[...path]]/SharePage.tsx b/src/app/share/[slug]/[[...path]]/SharePage.tsx index 5d93fd96..d80bc3b1 100644 --- a/src/app/share/[slug]/[[...path]]/SharePage.tsx +++ b/src/app/share/[slug]/[[...path]]/SharePage.tsx @@ -39,11 +39,23 @@ const PAGE_COMPONENTS: Record attribution: AttributionPage, }; +function getSharePath(pathname: string) { + const segments = pathname.split('/'); + const firstSegment = segments[3]; + + // If first segment looks like a domain name, skip it + if (firstSegment?.includes('.')) { + return segments[4]; + } + + return firstSegment; +} + export function SharePage() { const share = useShare(); const { setTheme } = useTheme(); const pathname = usePathname(); - const path = pathname.split('/')[3]; + const path = getSharePath(pathname); const { websiteId, parameters = {} } = share; useEffect(() => { From 482d6c1e4707ee5c5a03eb1d4dbccf930f944ceb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 29 Jan 2026 00:08:53 -0800 Subject: [PATCH 08/13] Add collapsible ShareNav sidebar - Add collapse/expand button in header - When collapsed: hide menu items and logo, show only toggle button - Stack bottom icons vertically when collapsed - Adjust grid layout to match collapsed nav width (60px vs 240px) Co-Authored-By: Claude Opus 4.5 --- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 92 +++++++++++++------ .../share/[slug]/[[...path]]/SharePage.tsx | 7 +- 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index fa100b17..65e97313 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -1,12 +1,29 @@ -import { Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen'; +import { Button, Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen'; import { SideMenu } from '@/components/common/SideMenu'; import { useMessages, useNavigation, useShare } from '@/components/hooks'; -import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons'; +import { + AlignEndHorizontal, + Clock, + Eye, + PanelLeftClose, + PanelLeftOpen, + Sheet, + Tag, + User, +} from '@/components/icons'; import { LanguageButton } from '@/components/input/LanguageButton'; import { PreferencesButton } from '@/components/input/PreferencesButton'; import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg'; -export function ShareNav({ onItemClick }: { onItemClick?: () => void }) { +export function ShareNav({ + collapsed, + onCollapse, + onItemClick, +}: { + collapsed?: boolean; + onCollapse?: (collapsed: boolean) => void; + onItemClick?: () => void; +}) { const share = useShare(); const { formatMessage, labels } = useMessages(); const { pathname } = useNavigation(); @@ -130,35 +147,56 @@ export function ShareNav({ onItemClick }: { onItemClick?: () => void }) { .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id; return ( - - - - - {logoImage ? ( - {logoName} - ) : ( - - - - )} - {logoName} - - + + + {!collapsed && ( + + + {logoImage ? ( + {logoName} + ) : ( + + + + )} + {logoName} + + + )} + - - - + {!collapsed && ( + + + + )} - + - + ); diff --git a/src/app/share/[slug]/[[...path]]/SharePage.tsx b/src/app/share/[slug]/[[...path]]/SharePage.tsx index d80bc3b1..aab32c1f 100644 --- a/src/app/share/[slug]/[[...path]]/SharePage.tsx +++ b/src/app/share/[slug]/[[...path]]/SharePage.tsx @@ -1,7 +1,7 @@ 'use client'; import { Column, Grid, Row, useTheme } from '@umami/react-zen'; import { usePathname } from 'next/navigation'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage'; import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage'; import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage'; @@ -52,6 +52,7 @@ function getSharePath(pathname: string) { } export function SharePage() { + const [navCollapsed, setNavCollapsed] = useState(false); const share = useShare(); const { setTheme } = useTheme(); const pathname = usePathname(); @@ -78,7 +79,7 @@ export function SharePage() { const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage; return ( - + {({ close }) => { @@ -87,7 +88,7 @@ export function SharePage() { - + From 452a385c4e2bebfcc97f3aac37783c7ae167a492 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 29 Jan 2026 00:12:23 -0800 Subject: [PATCH 09/13] Fix ShareNav collapse button and icon layout - Use single PanelLeft icon with muted color - Align collapse button to right of header - Bottom icons: horizontal (Row) when expanded, vertical (Column) when collapsed Co-Authored-By: Claude Opus 4.5 --- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index 65e97313..afad7a79 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -1,16 +1,7 @@ import { Button, Column, Icon, Row, Text, ThemeButton } from '@umami/react-zen'; import { SideMenu } from '@/components/common/SideMenu'; import { useMessages, useNavigation, useShare } from '@/components/hooks'; -import { - AlignEndHorizontal, - Clock, - Eye, - PanelLeftClose, - PanelLeftOpen, - Sheet, - Tag, - User, -} from '@/components/icons'; +import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons'; import { LanguageButton } from '@/components/input/LanguageButton'; import { PreferencesButton } from '@/components/input/PreferencesButton'; import { Funnel, Lightning, Logo, Magnet, Money, Network, Path, Target } from '@/components/svg'; @@ -156,13 +147,7 @@ export function ShareNav({ border="right" borderColor="4" > - + {!collapsed && ( @@ -178,7 +163,9 @@ export function ShareNav({ )} {!collapsed && ( @@ -192,11 +179,19 @@ export function ShareNav({ )} - - - - - + {collapsed ? ( + + + + + + ) : ( + + + + + + )} ); From 6169a58e86ae65124882322531f979b93b1c03bd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 29 Jan 2026 00:13:23 -0800 Subject: [PATCH 10/13] Center bottom icons when sidebar collapsed Co-Authored-By: Claude Opus 4.5 --- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index afad7a79..c17b5814 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -178,9 +178,9 @@ export function ShareNav({ /> )} - + {collapsed ? ( - + From 5880eae4e467eeb749b18b71d841c6adcc7ce7fe Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 29 Jan 2026 00:34:47 -0800 Subject: [PATCH 11/13] Fix NavMenu scrolling on mobile - Add overflowY="auto" and flexGrow to menu container - Menu now scrolls when content exceeds viewport height Co-Authored-By: Claude Opus 4.5 --- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index c17b5814..f4eade5e 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -169,7 +169,7 @@ export function ShareNav({ {!collapsed && ( - + )} - + {collapsed ? ( From b43e7fd3a7f91d5d7efd4a2f65155ac1c8ec2d73 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 29 Jan 2026 00:37:29 -0800 Subject: [PATCH 12/13] Hide sidebar collapse button on mobile onItemClick is only passed on mobile, so use it to detect mobile context Co-Authored-By: Claude Opus 4.5 --- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index f4eade5e..c5989f28 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -162,11 +162,13 @@ export function ShareNav({ )} - + {!onItemClick && ( + + )} {!collapsed && ( From 489c2712d1d43af2519e314da6070b20462be8c4 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 29 Jan 2026 00:44:18 -0800 Subject: [PATCH 13/13] Make ShareNav full width on mobile - Remove fixed width, position, and border on mobile - Use 100% width when onItemClick is provided (mobile context) Co-Authored-By: Claude Opus 4.5 --- src/app/share/[slug]/[[...path]]/ShareNav.tsx | 10 ++++++---- src/components/input/MobileMenuButton.tsx | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/share/[slug]/[[...path]]/ShareNav.tsx b/src/app/share/[slug]/[[...path]]/ShareNav.tsx index c5989f28..e6ca3865 100644 --- a/src/app/share/[slug]/[[...path]]/ShareNav.tsx +++ b/src/app/share/[slug]/[[...path]]/ShareNav.tsx @@ -137,15 +137,17 @@ export function ShareNav({ .flatMap(e => e.items) .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id; + const isMobile = !!onItemClick; + return ( {!collapsed && ( diff --git a/src/components/input/MobileMenuButton.tsx b/src/components/input/MobileMenuButton.tsx index 5e59cbbb..8498b05a 100644 --- a/src/components/input/MobileMenuButton.tsx +++ b/src/components/input/MobileMenuButton.tsx @@ -9,8 +9,8 @@ export function MobileMenuButton(props: DialogProps) { - - + + );