diff --git a/frontend/.env.example b/frontend/.env.example index 2694633..0d99be8 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -5,3 +5,8 @@ # the Vite dev server proxies to the backend (see vite.config.ts) and which a # reverse proxy is expected to route in production. VITE_API_URL= + +# Public path the app is served from. Leave empty for the domain root; set to +# e.g. /myapp/ to build for a sub-path (moves the asset URLs, the router and +# the default API base together). +VITE_BASE_PATH= diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6d9e9ce..a30660a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -21,6 +21,11 @@ COPY . . ARG VITE_API_URL= ENV VITE_API_URL=$VITE_API_URL +# Public path the app is served from. Leave empty for the domain root: +# docker build --build-arg VITE_BASE_PATH=/myapp/ . +ARG VITE_BASE_PATH= +ENV VITE_BASE_PATH=$VITE_BASE_PATH + RUN npm run build # ---------- Runtime stage ---------- diff --git a/frontend/README.md b/frontend/README.md index e9ee8a0..b25f3ad 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,6 +33,21 @@ API calls go through the small wrapper in `src/api/client.ts` and hit `/api/...` compose variant `docker/nginx.compose.conf`), or set `VITE_API_URL` at build time (see `.env.example`). +### Deploying under a sub-path + +The app is served from the domain root by default. To put it behind a path +prefix, build with `VITE_BASE_PATH` — one variable moves the asset URLs, the +router's `basename` and the default API base together: + +```bash +VITE_BASE_PATH=/myapp/ npm run build +# or: docker build --build-arg VITE_BASE_PATH=/myapp/ . +``` + +The build then loads `/myapp/assets/...` and calls `/myapp/api/...`, so the +proxy in front of it needs to serve the files and route that API prefix under +`/myapp/`. + The example endpoints live in `src/api/backend.ts` and match `backend/app/api.py`: - `GET /api/` → `{"app-api": "version ..."}` (status/version chip) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fa5fce5..465ece9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,7 +15,9 @@ export default function App() { - + {/* BASE_URL comes from `base` in vite.config.ts: '/' by default, so + this is a no-op until the app is built for a sub-path. */} + }> } /> diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index ea37a70..0786590 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,10 +1,11 @@ // Minimal typed wrapper around fetch for talking to the backend. // -// By default requests go to `/api`, which the Vite dev server proxies to the -// backend (see vite.config.ts) and which a reverse proxy (e.g. nginx) is -// expected to route in production. Set VITE_API_URL at build time to point -// somewhere else. -const API_BASE_URL: string = import.meta.env.VITE_API_URL || '/api' +// By default requests go to `api` under the app's own base path — `/api` at +// the domain root, `/myapp/api` when built with VITE_BASE_PATH=/myapp/. The +// Vite dev server proxies that to the backend (see vite.config.ts) and a +// reverse proxy (e.g. nginx) is expected to route it in production. Set +// VITE_API_URL at build time to point somewhere else entirely. +const API_BASE_URL: string = import.meta.env.VITE_API_URL || `${import.meta.env.BASE_URL}api` export class ApiError extends Error { constructor( diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bba09ce..80fafa5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,26 +1,41 @@ /// import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' // https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - server: { - // Forward API requests to the backend during development so the frontend - // can call `/api/...` without CORS configuration. The backend serves its - // routes without the /api prefix, so it is stripped here (nginx does the - // same in production, see docker/nginx.conf). 7000 matches the dev port - // used by backend/app/api.py. - proxy: { - '/api': { - target: 'http://localhost:7000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), +export default defineConfig(({ mode }) => { + // Read .env files here too, so VITE_BASE_PATH can be set the same ways as + // VITE_API_URL: an .env file, the shell, or a Docker build arg. + const env = loadEnv(mode, process.cwd(), 'VITE_') + // Vite normalises `base` to a trailing slash; do the same so the dev proxy + // and the API client agree on where `api` sits. + const raw = env.VITE_BASE_PATH || '/' + const base = raw.endsWith('/') ? raw : `${raw}/` + + return { + // Public path the app is served from. Leave unset for the domain root; + // set VITE_BASE_PATH=/myapp/ to deploy under a sub-path. Vite exposes the + // final value as import.meta.env.BASE_URL, which the router and the API + // client read, so one variable moves the whole app. + base, + plugins: [react()], + server: { + // Forward API requests to the backend during development so the frontend + // can call `api/...` without CORS configuration. The backend serves + // its routes without the prefix, so it is stripped here (nginx does the + // same in production, see docker/nginx.conf). 7000 matches the dev port + // used by backend/app/api.py. + proxy: { + [`${base}api`]: { + target: 'http://localhost:7000', + changeOrigin: true, + rewrite: (path) => path.slice(`${base}api`.length), + }, }, }, - }, - test: { - environment: 'jsdom', - setupFiles: './src/setupTests.ts', - }, + test: { + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + }, + } })