From f985b4f9a559ebb8d6e491971b98115931e828a6 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 2 Jun 2026 11:12:48 -0700 Subject: [PATCH 1/2] Use next rewrites for API_URL. --- next.config.ts | 31 ++++++++++++++++++++----------- pnpm-lock.yaml | 2 ++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/next.config.ts b/next.config.ts index 922d128bd..879b2d857 100644 --- a/next.config.ts +++ b/next.config.ts @@ -123,6 +123,7 @@ if (isProd) { } const rewrites = []; +const beforeFilesRewrites = []; if (trackerScriptURL) { rewrites.push({ @@ -157,6 +158,11 @@ if (isRelativeUrl(apiUrl)) { destination: '/api/:path*', }); } +} else if (apiUrl) { + beforeFilesRewrites.push({ + source: '/api/:path((?!auth(?:/|$)|config(?:/|$)).*)', + destination: `${apiUrl.replace(/\/+$/, '')}/:path`, + }); } const redirects = [ @@ -244,17 +250,20 @@ export default withNextIntl({ return headers; }, async rewrites() { - return [ - ...rewrites, - { - source: '/telemetry.js', - destination: '/api/scripts/telemetry', - }, - { - source: '/teams/:teamId/:path*', - destination: '/:path*', - }, - ]; + return { + beforeFiles: beforeFilesRewrites, + afterFiles: [ + ...rewrites, + { + source: '/telemetry.js', + destination: '/api/scripts/telemetry', + }, + { + source: '/teams/:teamId/:path*', + destination: '/:path*', + }, + ], + }; }, async redirects() { return [...redirects]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed0e7f952..5c65e466d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -334,6 +334,8 @@ importers: specifier: ^4.1.6 version: 4.1.6(@types/node@25.9.0)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.9.0)(typescript@6.0.3))(vite@8.0.11(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.1)(tsx@4.22.2)) + dist: {} + packages: '@adobe/css-tools@4.4.4': From 9ced9ac85a64cfcf8ac379c78c9735ec1481a457 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 2 Jun 2026 11:19:33 -0700 Subject: [PATCH 2/2] Fixed API_URL handling. --- README.md | 2 +- src/lib/api-url.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/lib/api-url.ts | 8 ++------ 3 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 src/lib/api-url.test.ts diff --git a/README.md b/README.md index 2d0eafd75..3c315a7ba 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ DATABASE_URL=connection-url ``` Optional: set `API_URL` to change the base URL used by internal UI API calls. -Relative paths are served under `BASE_PATH`; absolute URLs are called directly by the browser. +Relative paths are served under `BASE_PATH`; absolute URLs are proxied through the local `/api` route. For example, `API_URL=/internal-api` or `API_URL=https://api.example.com/api`. The connection URL format: diff --git a/src/lib/api-url.test.ts b/src/lib/api-url.test.ts new file mode 100644 index 000000000..322315b00 --- /dev/null +++ b/src/lib/api-url.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from 'vitest'; +import { getApiUrl } from './api-url'; + +describe('getApiUrl', () => { + test('uses the local api path when API_URL is absolute', () => { + expect( + getApiUrl('/websites', { + apiUrl: 'https://gateway-eu.umami.dev/api', + basePath: '/analytics', + }), + ).toBe('/analytics/api/websites'); + }); + + test('uses a relative API_URL under the base path', () => { + expect( + getApiUrl('/websites', { + apiUrl: '/internal-api', + basePath: '/analytics', + }), + ).toBe('/analytics/internal-api/websites'); + }); + + test('keeps app routes on the local api path', () => { + expect( + getApiUrl('/auth/verify', { + apiUrl: '/internal-api', + basePath: '/analytics', + }), + ).toBe('/analytics/api/auth/verify'); + }); + + test('returns absolute input urls unchanged', () => { + expect(getApiUrl('https://example.com/api/websites')).toBe( + 'https://example.com/api/websites', + ); + }); +}); diff --git a/src/lib/api-url.ts b/src/lib/api-url.ts index 104634fde..8cc790c1b 100644 --- a/src/lib/api-url.ts +++ b/src/lib/api-url.ts @@ -39,12 +39,8 @@ export function getApiUrl(url: string, options: ApiUrlOptions = {}) { } const { apiUrl = process.env.apiUrl || '', basePath = process.env.basePath || '' } = options; - const useApiUrl = apiUrl && !isAppRoute(url); - const baseUrl = useApiUrl - ? isAbsoluteUrl(apiUrl) - ? apiUrl - : joinPath(basePath, apiUrl) - : joinPath(basePath, '/api'); + const useApiUrl = apiUrl && !isAbsoluteUrl(apiUrl) && !isAppRoute(url); + const baseUrl = useApiUrl ? joinPath(basePath, apiUrl) : joinPath(basePath, '/api'); return joinPath(baseUrl, url); }