From f539f105076a4a365acc33242656625359040044 Mon Sep 17 00:00:00 2001 From: pppobear Date: Fri, 20 Mar 2026 08:46:21 +0800 Subject: [PATCH] fix(web): fix push notification click 404 on GitHub Pages (#322) --- web/public/404.html | 12 ++++++ web/src/lib/spaRedirect.test.ts | 69 +++++++++++++++++++++++++++++++++ web/src/lib/spaRedirect.ts | 22 +++++++++++ web/src/main.tsx | 8 ++++ 4 files changed, 111 insertions(+) create mode 100644 web/public/404.html create mode 100644 web/src/lib/spaRedirect.test.ts create mode 100644 web/src/lib/spaRedirect.ts diff --git a/web/public/404.html b/web/public/404.html new file mode 100644 index 00000000..5b2afee3 --- /dev/null +++ b/web/public/404.html @@ -0,0 +1,12 @@ + + + + + HAPI + + + + diff --git a/web/src/lib/spaRedirect.test.ts b/web/src/lib/spaRedirect.test.ts new file mode 100644 index 00000000..0585d51b --- /dev/null +++ b/web/src/lib/spaRedirect.test.ts @@ -0,0 +1,69 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { restoreSpaRedirect, storeSpaRedirect } from './spaRedirect' + +describe('spaRedirect', () => { + beforeEach(() => { + sessionStorage.clear() + vi.spyOn(window.history, 'replaceState') + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('restoreSpaRedirect', () => { + it('restores stored path via replaceState', () => { + sessionStorage.setItem('spaRedirect', '/sessions/abc123') + + restoreSpaRedirect() + + expect(window.history.replaceState).toHaveBeenCalledWith(null, '', '/sessions/abc123') + }) + + it('removes spaRedirect from sessionStorage after restoring', () => { + sessionStorage.setItem('spaRedirect', '/sessions/abc123') + + restoreSpaRedirect() + + expect(sessionStorage.getItem('spaRedirect')).toBeNull() + }) + + it('does nothing when spaRedirect is not set', () => { + restoreSpaRedirect() + + expect(window.history.replaceState).not.toHaveBeenCalled() + }) + + it('preserves query string and hash in restored path', () => { + sessionStorage.setItem('spaRedirect', '/sessions/abc123?foo=bar#section') + + restoreSpaRedirect() + + expect(window.history.replaceState).toHaveBeenCalledWith(null, '', '/sessions/abc123?foo=bar#section') + }) + }) + + describe('storeSpaRedirect', () => { + it('stores the current pathname in sessionStorage', () => { + Object.defineProperty(window, 'location', { + value: { pathname: '/sessions/abc123', search: '', hash: '' }, + configurable: true, + }) + + storeSpaRedirect() + + expect(sessionStorage.getItem('spaRedirect')).toBe('/sessions/abc123') + }) + + it('stores pathname with search and hash', () => { + Object.defineProperty(window, 'location', { + value: { pathname: '/sessions/abc123', search: '?foo=bar', hash: '#section' }, + configurable: true, + }) + + storeSpaRedirect() + + expect(sessionStorage.getItem('spaRedirect')).toBe('/sessions/abc123?foo=bar#section') + }) + }) +}) diff --git a/web/src/lib/spaRedirect.ts b/web/src/lib/spaRedirect.ts new file mode 100644 index 00000000..33f2ce23 --- /dev/null +++ b/web/src/lib/spaRedirect.ts @@ -0,0 +1,22 @@ +const SPA_REDIRECT_KEY = 'spaRedirect' + +/** + * Stores the current path in sessionStorage before GitHub Pages redirects to /. + * Called from public/404.html when GitHub Pages serves a 404 for SPA routes. + */ +export function storeSpaRedirect(): void { + const path = window.location.pathname + window.location.search + window.location.hash + sessionStorage.setItem(SPA_REDIRECT_KEY, path) +} + +/** + * Restores the path stored by storeSpaRedirect() using replaceState, + * so TanStack Router initializes at the correct URL without a server round-trip. + */ +export function restoreSpaRedirect(): void { + const redirect = sessionStorage.getItem(SPA_REDIRECT_KEY) + if (redirect) { + sessionStorage.removeItem(SPA_REDIRECT_KEY) + window.history.replaceState(null, '', redirect) + } +} diff --git a/web/src/main.tsx b/web/src/main.tsx index b88697c0..2efaad1a 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -10,6 +10,7 @@ import { getTelegramWebApp, isTelegramEnvironment, loadTelegramSdk } from './hoo import { queryClient } from './lib/query-client' import { createAppRouter } from './router' import { I18nProvider } from './lib/i18n-context' +import { restoreSpaRedirect } from './lib/spaRedirect' function getStartParam(): string | null { const query = new URLSearchParams(window.location.search) @@ -41,6 +42,13 @@ async function bootstrap() { await loadTelegramSdk() } + // Handle GitHub Pages 404 redirect for SPA routing + // When GitHub Pages can't find a path (e.g. /sessions/xxx), it serves 404.html + // which stores the path in sessionStorage and redirects to / + if (!isTelegram) { + restoreSpaRedirect() + } + const updateSW = registerSW({ onNeedRefresh() { if (confirm('New version available! Reload to update?')) {