Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
5 changes: 5 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------
Expand Down
15 changes: 15 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export default function App() {
<ThemeProvider theme={theme}>
<CssBaseline />
<QueryClientProvider client={queryClient}>
<BrowserRouter>
{/* 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. */}
<BrowserRouter basename={import.meta.env.BASE_URL}>
<Routes>
<Route element={<Layout />}>
<Route index element={<HomePage />} />
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
53 changes: 34 additions & 19 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
/// <reference types="vitest/config" />
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 `<base>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',
},
}
})
Loading