diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fa93d7b6..e449dc91 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ import "./styles/global.scss"; import {isSsr} from "./utilites/helpers.ts"; import {StartupChecks} from "./StartupChecks.tsx"; import {ThirdPartyScripts} from "./components/common/ThirdPartyScripts"; +import { getBasePath } from "./utilites/basePath.ts"; declare global { interface Window { @@ -32,10 +33,20 @@ export const App: FC< }> > = (props) => { const [isLoadedOnBrowser, setIsLoadedOnBrowser] = React.useState(false); + const basePath = getBasePath(); useEffect(() => { setIsLoadedOnBrowser(!isSsr()); - }, []); + + // Ensure that the client is always accessing via the base path + // This is to ensure that the app is always served from the correct base path + if (!window.location.pathname.startsWith(basePath)) { + window.location.replace(basePath); + } + }, [] ); + + + return ( diff --git a/frontend/src/entry.client.tsx b/frontend/src/entry.client.tsx index 911560db..bfeb6e69 100644 --- a/frontend/src/entry.client.tsx +++ b/frontend/src/entry.client.tsx @@ -2,7 +2,7 @@ import {hydrateRoot} from "react-dom/client"; import {createBrowserRouter, matchRoutes, RouterProvider} from "react-router-dom"; import {hydrate} from "@tanstack/react-query"; -import {router} from "./router"; +import {options, routes} from "./router"; import {App} from "./App"; import {queryClient} from "./utilites/queryClient"; import {dynamicActivateLocale, getClientLocale, getSupportedLocale,} from "./locales.ts"; @@ -23,7 +23,7 @@ async function initClientApp() { await dynamicActivateLocale(locale); // Resolve lazy-loaded routes before hydration - const matches = matchRoutes(router, window.location)?.filter((m) => m.route.lazy); + const matches = matchRoutes(routes, window.location)?.filter((m) => m.route.lazy); if (matches && matches.length > 0) { await Promise.all( matches.map(async (m) => { @@ -33,7 +33,7 @@ async function initClientApp() { ); } - const browserRouter = createBrowserRouter(router); + const browserRouter = createBrowserRouter(routes, options); hydrateRoot( document.getElementById("app") as HTMLElement, diff --git a/frontend/src/entry.server.tsx b/frontend/src/entry.server.tsx index 0cc94cfa..654d6c57 100644 --- a/frontend/src/entry.server.tsx +++ b/frontend/src/entry.server.tsx @@ -2,7 +2,7 @@ import type * as express from "express"; import ReactDOMServer from "react-dom/server"; import {dehydrate} from "@tanstack/react-query"; -import {router} from "./router"; +import {routes} from "./router"; import {App} from "./App"; import {queryClient} from "./utilites/queryClient"; import {setAuthToken} from "./utilites/apiClient.ts"; @@ -26,7 +26,7 @@ export async function render(params: { }) { setAuthToken(params.req.cookies.token); - const {query, dataRoutes} = createStaticHandler(router); + const {query, dataRoutes} = createStaticHandler(routes); const remixRequest = createFetchRequest(params.req, params.res); const context = await query(remixRequest); diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index f13f9064..4c74b1ea 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,9 +1,10 @@ -import {Navigate, RouteObject} from "react-router"; +import { createBrowserRouter, Navigate, RouteObject} from "react-router"; import ErrorPage from "./error-page.tsx"; import {eventsClientPublic} from "./api/event.client.ts"; import {promoCodeClientPublic} from "./api/promo-code.client.ts"; import {useEffect, useState} from "react"; import {useGetMe} from "./queries/useGetMe.ts"; +import { getBasePath } from "./utilites/basePath.ts"; const Root = () => { const [redirectPath, setRedirectPath] = useState(null); @@ -20,7 +21,11 @@ const Root = () => { } }; -export const router: RouteObject[] = [ +export const options: Parameters[1] = { + basename: getBasePath(), +}; + +export const routes: RouteObject[] = [ { path: "", element: , diff --git a/frontend/src/utilites/basePath.ts b/frontend/src/utilites/basePath.ts new file mode 100644 index 00000000..f5846e33 --- /dev/null +++ b/frontend/src/utilites/basePath.ts @@ -0,0 +1,24 @@ +import { getConfig } from "./config"; + +export function getBasePath() { + const frontendUrl: string = getConfig( "VITE_FRONTEND_URL" ) as string || import.meta.env.VITE_FRONTEND_URL as string; + + try { + const url = new URL(frontendUrl); + let basePath: string = url.pathname; + + // Make sure it always ends without trailing slash (except root) + if (basePath !== "/" && basePath.endsWith("/")) { + basePath = basePath.slice(0, -1); + } + + return basePath || "/"; + } catch ( e ) { + // If URL parsing fails, fallback to root + console.warn( + `Invalid frontend URL: ${frontendUrl}. This might be due to an incorrect environment variable 'VITE_FRONTEND_URL'.`, e + ); + + return "/"; + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 722202ad..9c0ecd24 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,41 +1,45 @@ -import {defineConfig} from "vite"; -import {lingui} from "@lingui/vite-plugin"; +import { lingui } from "@lingui/vite-plugin"; import react from "@vitejs/plugin-react"; -import {copy} from "vite-plugin-copy"; +import { defineConfig, loadEnv } from "vite"; +import { copy } from "vite-plugin-copy"; -export default defineConfig({ - optimizeDeps: { - include: ["react-router"] - }, - server: { - hmr: { - port: 24678, - protocol: "ws", - }, - }, - plugins: [ - react({ - babel: { - plugins: ["macros"], - }, - }), - lingui(), - copy({ - targets: [{src: "src/embed/widget.js", dest: "public"}], - hook: "writeBundle", - }), - ], - define: { - "process.env": process.env, - }, - ssr: { - noExternal: ["react-helmet-async"], - }, - css: { - preprocessorOptions: { - scss: { - api: "modern-compiler", - } - } - } -}); +export default defineConfig( ( { mode } ) => { + const env = loadEnv(mode, process.cwd(), ""); + return { + base: new URL(env.VITE_FRONTEND_URL).pathname || "/", + optimizeDeps: { + include: ["react-router"], + }, + server: { + hmr: { + port: 24678, + protocol: "ws", + }, + }, + plugins: [ + react({ + babel: { + plugins: ["macros"], + }, + }), + lingui(), + copy({ + targets: [{ src: "src/embed/widget.js", dest: "public" }], + hook: "writeBundle", + }), + ], + define: { + "process.env": process.env, + }, + ssr: { + noExternal: ["react-helmet-async"], + }, + css: { + preprocessorOptions: { + scss: { + api: "modern-compiler", + }, + }, + }, + }; +})