Skip to content

Commit d8068b6

Browse files
committed
fix(webapp): prevent SSG errors on error and showcase pages
- Simplify 404/500/_error pages by removing useRouter - Create AppProviders component for client-side only hooks - Dynamically import router-dependent hooks with ssr: false - Add getServerSideProps to all showcase pages
1 parent 4f63e97 commit d8068b6

11 files changed

Lines changed: 136 additions & 131 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Client-side App Providers
3+
* =========================
4+
* Contains router-dependent hooks that can't run during SSG.
5+
* This component is dynamically imported in _app.tsx.
6+
*/
7+
8+
import { useEffect } from 'react'
9+
import { useRouter } from 'next/router'
10+
import { useOnAuthStateChange } from '@hooks/useOnAuthStateChange'
11+
import { useCatchUserPresences } from '@hooks/useCatchUserPresences'
12+
import { useInitialSteps } from '@hooks/useInitialSteps'
13+
import { useBroadcastListner } from '@hooks/useBroadcastListner'
14+
import useServiceWorker from '@hooks/useServiceWorker'
15+
import { useHandleUserStatus } from '@hooks/useHanelUserStatus'
16+
import { eventsHub } from '@services/eventsHub'
17+
import { performMaintenanceCleanup } from '@db/messageComposerDB'
18+
import { useEditorPreferences, applyEditorPreferences } from '@stores'
19+
20+
interface AppProvidersProps {
21+
isMobileInitial: boolean
22+
}
23+
24+
export default function AppProviders({ isMobileInitial }: AppProvidersProps) {
25+
const router = useRouter()
26+
const { preferences, hydrated } = useEditorPreferences()
27+
28+
useServiceWorker()
29+
useOnAuthStateChange()
30+
useCatchUserPresences()
31+
useBroadcastListner()
32+
useHandleUserStatus()
33+
useInitialSteps(isMobileInitial)
34+
35+
// Apply editor preferences on hydration and changes
36+
useEffect(() => {
37+
if (hydrated) applyEditorPreferences(preferences)
38+
}, [hydrated, preferences])
39+
40+
useEffect(() => {
41+
if (!router.isReady) return
42+
eventsHub(router)
43+
44+
// Run DB maintenance cleanup once per session
45+
performMaintenanceCleanup().catch(() => {
46+
// Silently fail - cleanup is best-effort
47+
})
48+
49+
// iOS Safari keyboard viewport fix
50+
const doc = document.documentElement
51+
let lastHeight = window.visualViewport?.height ?? window.innerHeight
52+
let rafId: number | null = null
53+
54+
const vv = window.visualViewport
55+
if (vv) {
56+
doc.style.setProperty('--visual-viewport-height', `${vv.height}px`)
57+
doc.style.setProperty('--vh', `${vv.height * 0.01}px`)
58+
}
59+
60+
function handleViewportResize() {
61+
if (rafId) cancelAnimationFrame(rafId)
62+
63+
rafId = requestAnimationFrame(() => {
64+
const vv = window.visualViewport
65+
if (!vv) return
66+
67+
const height = vv.height
68+
if (Math.abs(height - lastHeight) < 50) return
69+
70+
lastHeight = height
71+
doc.style.setProperty('--visual-viewport-height', `${height}px`)
72+
doc.style.setProperty('--vh', `${height * 0.01}px`)
73+
})
74+
}
75+
76+
let scrollResetTimeout: ReturnType<typeof setTimeout> | null = null
77+
78+
function handleViewportScroll() {
79+
const vv = window.visualViewport
80+
if (!vv || vv.offsetTop === 0) return
81+
82+
if (scrollResetTimeout) clearTimeout(scrollResetTimeout)
83+
84+
scrollResetTimeout = setTimeout(() => {
85+
if (window.visualViewport && window.visualViewport.offsetTop > 0) {
86+
window.scrollTo(0, 0)
87+
}
88+
}, 100)
89+
}
90+
91+
window.visualViewport?.addEventListener('resize', handleViewportResize)
92+
window.visualViewport?.addEventListener('scroll', handleViewportScroll)
93+
94+
return () => {
95+
if (rafId) cancelAnimationFrame(rafId)
96+
if (scrollResetTimeout) clearTimeout(scrollResetTimeout)
97+
window.visualViewport?.removeEventListener('resize', handleViewportResize)
98+
window.visualViewport?.removeEventListener('scroll', handleViewportScroll)
99+
}
100+
}, [router.isReady])
101+
102+
return null // This is a side-effect-only component
103+
}

packages/webapp/src/pages/404.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
1-
import { useRouter } from 'next/router'
1+
import Link from 'next/link'
22

33
export default function Custom404() {
4-
const router = useRouter()
5-
64
return (
75
<div className="bg-base-200 grid min-h-screen place-items-center p-4">
86
<div className="text-center">
97
<p className="text-base-content/20 text-8xl font-bold">404</p>
108
<h1 className="text-base-content mt-4 text-2xl font-semibold">Page not found</h1>
119
<p className="text-base-content/60 mt-2">The page you're looking for doesn't exist.</p>
1210
<div className="mt-6 flex justify-center gap-3">
13-
<button onClick={() => router.back()} className="btn btn-outline btn-sm">
14-
Go Back
15-
</button>
16-
<button onClick={() => router.push('/')} className="btn btn-primary btn-sm">
11+
<Link href="/" className="btn btn-primary btn-sm">
1712
Home
18-
</button>
13+
</Link>
1914
</div>
2015
</div>
2116
</div>

packages/webapp/src/pages/500.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
1-
import { useRouter } from 'next/router'
1+
import Link from 'next/link'
22

33
export default function Custom500() {
4-
const router = useRouter()
5-
const error = router.query.error as string | undefined
6-
74
return (
85
<div className="bg-base-200 grid min-h-screen place-items-center p-4">
96
<div className="text-center">
107
<p className="text-error text-8xl font-bold">500</p>
118
<h1 className="text-base-content mt-4 text-2xl font-semibold">Something went wrong</h1>
129
<p className="text-base-content/60 mt-2 max-w-md">
13-
{error || 'An unexpected error occurred. Please try again later.'}
10+
An unexpected error occurred. Please try again later.
1411
</p>
1512
<div className="mt-6 flex justify-center gap-3">
16-
<button onClick={() => router.back()} className="btn btn-outline btn-sm">
17-
Go Back
18-
</button>
19-
<button onClick={() => router.push('/')} className="btn btn-primary btn-sm">
13+
<Link href="/" className="btn btn-primary btn-sm">
2014
Home
21-
</button>
15+
</Link>
2216
</div>
2317
</div>
2418
</div>

packages/webapp/src/pages/_app.tsx

Lines changed: 5 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
import Head from 'next/head'
2-
import { useEffect } from 'react'
2+
import dynamic from 'next/dynamic'
33
import { Toaster } from 'react-hot-toast'
44
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5-
import { useOnAuthStateChange } from '@hooks/useOnAuthStateChange'
6-
import { useCatchUserPresences } from '@hooks/useCatchUserPresences'
7-
import { useInitialSteps } from '@hooks/useInitialSteps'
8-
import { useBroadcastListner } from '@hooks/useBroadcastListner'
9-
import useServiceWorker from '@hooks/useServiceWorker'
10-
import { useHandleUserStatus } from '@hooks/useHanelUserStatus'
11-
import { eventsHub } from '@services/eventsHub'
125
import GoogleAnalytics from '@components/GoogleAnalytics'
136
import NotificationPromptCard from '@components/NotificationPromptCard'
14-
import { performMaintenanceCleanup } from '@db/messageComposerDB'
15-
import { useEditorPreferences, applyEditorPreferences } from '@stores'
167

178
import '../styles/globals.scss'
189
import '../styles/styles.scss'
19-
import { useRouter } from 'next/router'
2010
import '@config'
2111

22-
// import { initializeApm } from '@utils/elasticApm'
12+
// Dynamically import router-dependent hooks (client-side only)
13+
// This prevents SSG errors on static pages like 404/500
14+
const AppProviders = dynamic(() => import('@components/AppProviders'), { ssr: false })
2315

2416
// Create a client
2517
const queryClient = new QueryClient()
@@ -47,106 +39,13 @@ const Header = () => {
4739

4840
export default function MyApp({ Component, pageProps }: any) {
4941
const isMobileInitial = pageProps.isMobile || false
50-
const { preferences, hydrated } = useEditorPreferences()
51-
52-
const router = useRouter()
53-
54-
useServiceWorker()
55-
useOnAuthStateChange()
56-
useCatchUserPresences()
57-
// pinnedMessage, typingIndicator broadcaster
58-
useBroadcastListner()
59-
// service worker side
60-
useHandleUserStatus()
61-
useInitialSteps(isMobileInitial)
62-
63-
// Apply editor preferences on hydration and changes
64-
useEffect(() => {
65-
if (hydrated) applyEditorPreferences(preferences)
66-
}, [hydrated, preferences])
67-
68-
useEffect(() => {
69-
eventsHub(router)
70-
// initializeApm()
71-
72-
// Run DB maintenance cleanup once per session (client-side only)
73-
if (typeof window !== 'undefined') {
74-
performMaintenanceCleanup().catch(() => {
75-
// Silently fail - cleanup is best-effort
76-
})
77-
78-
// iOS Safari keyboard viewport fix
79-
// Two key behaviors to handle:
80-
// 1. Height changes when keyboard opens/closes
81-
// 2. iOS auto-scrolls when focusing elements in bottom half of screen
82-
const doc = document.documentElement
83-
let lastHeight = window.visualViewport?.height ?? window.innerHeight
84-
let rafId: number | null = null
85-
86-
// Set initial height
87-
const vv = window.visualViewport
88-
if (vv) {
89-
doc.style.setProperty('--visual-viewport-height', `${vv.height}px`)
90-
doc.style.setProperty('--vh', `${vv.height * 0.01}px`)
91-
}
92-
93-
// Update height immediately using rAF for smooth rendering
94-
function handleViewportResize() {
95-
if (rafId) cancelAnimationFrame(rafId)
96-
97-
rafId = requestAnimationFrame(() => {
98-
const vv = window.visualViewport
99-
if (!vv) return
100-
101-
const height = vv.height
102-
103-
// Skip micro-updates (less than 50px change might be just toolbar hiding)
104-
if (Math.abs(height - lastHeight) < 50) return
105-
106-
lastHeight = height
107-
doc.style.setProperty('--visual-viewport-height', `${height}px`)
108-
doc.style.setProperty('--vh', `${height * 0.01}px`)
109-
})
110-
}
111-
112-
// CRITICAL: When iOS auto-scrolls to show focused element, reset scroll
113-
// This prevents the "off-screen" issue when tapping bottom half
114-
let scrollResetTimeout: ReturnType<typeof setTimeout> | null = null
115-
116-
function handleViewportScroll() {
117-
const vv = window.visualViewport
118-
if (!vv || vv.offsetTop === 0) return
119-
120-
// Clear any pending reset
121-
if (scrollResetTimeout) clearTimeout(scrollResetTimeout)
122-
123-
// Debounce the scroll reset to let iOS finish its animation
124-
scrollResetTimeout = setTimeout(() => {
125-
// Double-check offsetTop is still non-zero
126-
if (window.visualViewport && window.visualViewport.offsetTop > 0) {
127-
// Reset window scroll - our fixed container will realign
128-
window.scrollTo(0, 0)
129-
}
130-
}, 100)
131-
}
132-
133-
window.visualViewport?.addEventListener('resize', handleViewportResize)
134-
window.visualViewport?.addEventListener('scroll', handleViewportScroll)
135-
136-
return () => {
137-
if (rafId) cancelAnimationFrame(rafId)
138-
if (scrollResetTimeout) clearTimeout(scrollResetTimeout)
139-
window.visualViewport?.removeEventListener('resize', handleViewportResize)
140-
window.visualViewport?.removeEventListener('scroll', handleViewportScroll)
141-
}
142-
}
143-
}, [])
14442

14543
return (
14644
<div id="root">
14745
<Header />
14846
<GoogleAnalytics />
14947
<NotificationPromptCard />
48+
<AppProviders isMobileInitial={isMobileInitial} />
15049
<QueryClientProvider client={queryClient}>
15150
<Component {...pageProps} />
15251
</QueryClientProvider>

packages/webapp/src/pages/_error.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { useRouter } from 'next/router'
1+
import Link from 'next/link'
22

33
function Error({ statusCode }: { statusCode?: number }) {
4-
const router = useRouter()
54
const isServer = !!statusCode
65

76
return (
@@ -17,12 +16,9 @@ function Error({ statusCode }: { statusCode?: number }) {
1716
: 'An error occurred on the client.'}
1817
</p>
1918
<div className="mt-6 flex justify-center gap-3">
20-
<button onClick={() => router.back()} className="btn btn-outline btn-sm">
21-
Go Back
22-
</button>
23-
<button onClick={() => router.push('/')} className="btn btn-primary btn-sm">
19+
<Link href="/" className="btn btn-primary btn-sm">
2420
Home
25-
</button>
21+
</Link>
2622
</div>
2723
</div>
2824
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import DocumentsShowcase from '@components/pages/showcase/DocumentsShowcase'
2+
import type { GetServerSideProps } from 'next'
3+
4+
export const getServerSideProps: GetServerSideProps = async () => ({ props: {} })
25

36
export default DocumentsShowcase
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import EditorShowcase from '@components/pages/showcase/EditorShowcase'
2+
import type { GetServerSideProps } from 'next'
3+
4+
export const getServerSideProps: GetServerSideProps = async () => ({ props: {} })
25

36
export default EditorShowcase

packages/webapp/src/pages/showcase/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* Landing page for all showcase pages.
55
*/
66

7+
import type { GetServerSideProps } from 'next'
78
import Head from 'next/head'
9+
10+
export const getServerSideProps: GetServerSideProps = async () => ({ props: {} })
811
import Link from 'next/link'
912
import { useState, useCallback } from 'react'
1013
import {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import NotificationsShowcase from '@components/pages/showcase/NotificationsShowcase'
2+
import type { GetServerSideProps } from 'next'
3+
4+
export const getServerSideProps: GetServerSideProps = async () => ({ props: {} })
25

36
export default NotificationsShowcase
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import ProfileShowcase from '@components/pages/showcase/ProfileShowcase'
2+
import type { GetServerSideProps } from 'next'
3+
4+
export const getServerSideProps: GetServerSideProps = async () => ({ props: {} })
25

36
export default ProfileShowcase

0 commit comments

Comments
 (0)