Skip to content

Add support for app deployment under sub-paths #577

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

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
13 changes: 12 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 (
<React.StrictMode>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) => {
Expand All @@ -33,7 +33,7 @@ async function initClientApp() {
);
}

const browserRouter = createBrowserRouter(router);
const browserRouter = createBrowserRouter(routes, options);

hydrateRoot(
document.getElementById("app") as HTMLElement,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);

Expand Down
9 changes: 7 additions & 2 deletions frontend/src/router.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);
Expand All @@ -20,7 +21,11 @@ const Root = () => {
}
};

export const router: RouteObject[] = [
export const options: Parameters<typeof createBrowserRouter>[1] = {
basename: getBasePath(),
};

export const routes: RouteObject[] = [
{
path: "",
element: <Root/>,
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/utilites/basePath.ts
Original file line number Diff line number Diff line change
@@ -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 "/";
}
}
82 changes: 43 additions & 39 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
};
})