-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: landing page ssr + seo optimization #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
79c6461
74e4036
84c3fae
c6943be
33356ab
bfd13cf
345e700
b7416cf
8218a5d
7b3cbe9
adc51ef
2ab721c
b52bcc3
5e7ce5c
b597da1
2f0b887
9f2cb7a
070fbd0
c20de07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| ```mermaid | ||
| sequenceDiagram | ||
| autonumber | ||
| actor U as User | ||
| participant B as Browser | ||
| participant CDN as CDN/Edge Cache | ||
| participant Next as Next.js Build (CI) | ||
| participant I18N as next-intl (generateStaticParams) | ||
| participant RC as Firebase Remote Config (build-time) | ||
| participant JS as Client JS (Header component) | ||
|
|
||
| rect rgb(235, 245, 255) | ||
| note over Next,RC: BUILD TIME (SSG) | ||
| Next->>I18N: generateStaticParams() → all locales | ||
| I18N-->>Next: locale list | ||
| Next->>I18N: Build locale message bundles (baked) | ||
| Next->>RC: Fetch Remote Config snapshot (baked) | ||
| RC-->>Next: Remote Config values | ||
| Next->>Next: Render pages to static HTML | ||
| Next->>Next: Extract critical MUI styles (Emotion) | ||
| note over Next: <style data-emotion="mui-*"> inlined in HTML | ||
| Next->>Next: Emit JS/CSS assets (Header client bundle) | ||
| Next->>CDN: Deploy static HTML + assets | ||
| end | ||
|
|
||
| rect rgb(245, 245, 245) | ||
| note over U,CDN: RUNTIME (User request) | ||
| U->>B: Navigate to /{locale}/page | ||
| B->>CDN: GET /{locale}/page | ||
| CDN-->>B: 200 Static HTML (SSG + inline MUI styles) | ||
|
|
||
| note over B: ✅ Styled content is painted\n(LCP occurs here) | ||
|
|
||
| B->>CDN: GET external CSS (non-critical) | ||
| CDN-->>B: CSS files | ||
| B->>CDN: GET JS bundle(s) | ||
| CDN-->>B: JS | ||
|
|
||
| note over B: React hydration begins | ||
| B->>JS: Hydrate client components | ||
| note over JS: ✅ Header hydration completes\n(interactivity enabled) | ||
| end | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,20 +2,44 @@ | |
|
|
||
| import './App.css'; | ||
| import AppRouter from './router/Router'; | ||
| import { BrowserRouter } from 'react-router-dom'; | ||
| import { MemoryRouter } from 'react-router-dom'; | ||
| import { useDispatch } from 'react-redux'; | ||
| import { anonymousLogin } from './store/profile-reducer'; | ||
| import { app } from '../firebase'; | ||
| import { Suspense, useEffect, useState } from 'react'; | ||
| import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; | ||
| import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; | ||
| import AppContainer from './AppContainer'; | ||
| import { Helmet, HelmetProvider } from 'react-helmet-async'; | ||
| import { usePathname, useSearchParams } from 'next/navigation'; | ||
|
|
||
| function App(): React.ReactElement { | ||
| interface AppProps { | ||
| locale?: string; | ||
| } | ||
|
|
||
| // Helper function to construct the full path from Next.js routing | ||
| function buildPathFromNextRouter( | ||
| pathname: string, | ||
| searchParams: URLSearchParams, | ||
| locale?: string, | ||
| ): string { | ||
| const cleanPath = | ||
| locale != null && locale !== 'en' | ||
| ? (pathname.replace(`/${locale}`, '') ?? '/') | ||
| : pathname; | ||
|
|
||
| const searchString = searchParams.toString(); | ||
| return searchString !== '' ? `${cleanPath}?${searchString}` : cleanPath; | ||
| } | ||
|
|
||
| function App({ locale }: AppProps): React.ReactElement { | ||
| const dispatch = useDispatch(); | ||
| const [isAppReady, setIsAppReady] = useState(false); | ||
|
|
||
| const pathname = usePathname(); | ||
| const searchParams = useSearchParams(); | ||
|
|
||
| const initialPath = buildPathFromNextRouter(pathname, searchParams, locale); | ||
|
Comment on lines
+38
to
+41
|
||
|
|
||
| useEffect(() => { | ||
| app.auth().onAuthStateChanged((user) => { | ||
| if (user != null) { | ||
|
|
@@ -29,24 +53,15 @@ function App(): React.ReactElement { | |
| }, [dispatch]); | ||
|
|
||
| return ( | ||
| <HelmetProvider> | ||
| <Helmet> | ||
| <meta | ||
| name='description' | ||
| content={ | ||
| "Access GTFS, GTFS Realtime, GBFS transit data with over 4,000 feeds from 70+ countries on the web's leading transit data platform." | ||
| } | ||
| /> | ||
| </Helmet> | ||
| <Suspense> | ||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | ||
| {/* BrowserRouter will be deprecated in favor of Next AppRouter */} | ||
| <BrowserRouter> | ||
| <AppContainer>{isAppReady ? <AppRouter /> : null}</AppContainer> | ||
| </BrowserRouter> | ||
| </LocalizationProvider> | ||
| </Suspense> | ||
| </HelmetProvider> | ||
| <Suspense> | ||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | ||
| {/* MemoryRouter will be deprecated in favor of Next AppRouter */} | ||
| {/* MemoryRouter synced with Next.js routing via RouterSync component */} | ||
| <MemoryRouter initialEntries={[initialPath]}> | ||
| <AppContainer>{isAppReady ? <AppRouter /> : null}</AppContainer> | ||
| </MemoryRouter> | ||
| </LocalizationProvider> | ||
| </Suspense> | ||
| ); | ||
| } | ||
|
|
||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| 'use client'; | ||
|
|
||
| // This page is temporary to ease the migration to Next.js App Router | ||
| // It will be deprecated once the migration is fully complete | ||
| import { type ReactNode, use, useEffect } from 'react'; | ||
| import dynamic from 'next/dynamic'; | ||
| import { PersistGate } from 'redux-persist/integration/react'; | ||
| import { persistStore } from 'redux-persist'; | ||
| import { store } from '../../store/store'; | ||
| import { useAppDispatch } from '../../hooks'; | ||
| import { resetProfileErrors } from '../../store/profile-reducer'; | ||
|
|
||
| const App = dynamic(async () => await import('../../App'), { ssr: false }); | ||
|
|
||
| const persistor = persistStore(store); | ||
|
|
||
| interface PageProps { | ||
| params: Promise<{ | ||
| locale: string; | ||
| slug: string[]; | ||
| }>; | ||
| } | ||
|
|
||
| export default function Page({ params }: PageProps): ReactNode { | ||
| const { locale } = use(params); | ||
| const pathKey = use(params).slug?.join('/') ?? '/'; | ||
|
Comment on lines
+24
to
+26
|
||
| const dispatch = useAppDispatch(); | ||
|
|
||
| useEffect(() => { | ||
| // Clean errors from previous session | ||
| dispatch(resetProfileErrors()); | ||
| }, [dispatch]); | ||
|
|
||
| // Pass locale to App so BrowserRouter can use correct basename | ||
| return ( | ||
| <PersistGate loading={null} persistor={persistor}> | ||
| <App locale={locale} key={pathKey} />; | ||
| </PersistGate> | ||
| ); | ||
| } | ||
|
Comment on lines
+35
to
+40
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useSearchParams()fromnext/navigationreturnsReadonlyURLSearchParams, notURLSearchParams, so this call is likely a TypeScript error. Also,pathname.replace(\/${locale}`, '')will replace the first occurrence anywhere in the string rather than only stripping a leading locale segment; using a prefix check (e.g.,pathname.startsWith(...)`) avoids accidental replacements.