From 722c14bf3fd1bed4221786de53cac822e5ea22c1 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 15 Oct 2025 16:11:26 +0800 Subject: [PATCH] handle dynamic base url --- next-ui/README.md | 53 ++++++++++++++++++++++++++++++ next-ui/index.html | 10 ++++++ next-ui/src/api/base.ts | 8 +++++ next-ui/src/api/images.ts | 8 +++-- next-ui/src/api/komga-client.ts | 3 +- next-ui/src/mocks/api/httpTyped.ts | 3 +- next-ui/src/router/index.ts | 4 ++- next-ui/vite.config.mts | 11 +++++++ 8 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 next-ui/src/api/base.ts diff --git a/next-ui/README.md b/next-ui/README.md index 987612421..3b630474a 100644 --- a/next-ui/README.md +++ b/next-ui/README.md @@ -60,3 +60,56 @@ Components are automatically imported using [unplugin-vue-components](https://gi ## Icons [UnoCSS Icons preset](https://unocss.dev/presets/icons) is used for icons, with the MDI set from Iconify. + +## Base URL + +The generated bundle is server by Apache Tomcat when running Spring. By default the site is hosted at `/`, but if `server.servlet-context-path` is set, the base URL can be dynamic. + +The base URL needs to be set correctly so the web app works: + +- in the API client +- in the Vue Router, to properly handle the web history +- in the generated bundle, to load other files (js/css/images) + +1. Vite is [configured](./vite.config.mts) with the experimental `renderBuiltUrl`, which will use a dynamic function (`window.buildUrl`) defined in `index.html` to generate the asset path at runtime. This is only supported withing JS files though. +2. To handle the dynamic path in `index.html`, a Gradle task `prepareThymeLeafNext` modifies `index.html` to duplicate `href`, `src` and `content` attributes as Thymeleaf variants. + + For example the following: + + ```html + + + ``` + + will be transformed to: + + ```html + + + ``` + + In Thymeleaf, `@{}` will prepend the path with the context path dynamically when serving `index.html`. + +3. when the `index.html` is served by the `IndexController`, a `baseUrl` attribute is injected, which contains the servlet context path (by default `/`, but could be `/komga` for example) +4. the `index.html` contains a Thymeleaf script block that will be processed when serving the page, effectively injecting the `baseUrl` value into `window.ressourceBaseUrl`. +5. `window.ressourceBaseUrl` is subsequently used in Typescript code to set the base URL for: + - the API client and the images served [by API](./src/api/base.ts) + - the [Vue Router](./src/router/index.ts) diff --git a/next-ui/index.html b/next-ui/index.html index 663685422..60cf0a116 100644 --- a/next-ui/index.html +++ b/next-ui/index.html @@ -11,6 +11,16 @@ content="width=device-width, initial-scale=1.0" /> Komga + +
diff --git a/next-ui/src/api/base.ts b/next-ui/src/api/base.ts new file mode 100644 index 000000000..b9bd31ebf --- /dev/null +++ b/next-ui/src/api/base.ts @@ -0,0 +1,8 @@ +declare global { + interface Window { + resourceBaseUrl: string + } +} + +export const API_BASE_URL = + import.meta.env.VITE_KOMGA_API_URL || window.location.origin + window.resourceBaseUrl diff --git a/next-ui/src/api/images.ts b/next-ui/src/api/images.ts index 4b2b595dd..22431a5cc 100644 --- a/next-ui/src/api/images.ts +++ b/next-ui/src/api/images.ts @@ -1,14 +1,16 @@ +import { API_BASE_URL } from '@/api/base' + export function seriesThumbnailUrl(seriesId?: string): string | undefined { - if (seriesId) return `${import.meta.env.VITE_KOMGA_API_URL}/api/v1/series/${seriesId}/thumbnail` + if (seriesId) return `${API_BASE_URL}/api/v1/series/${seriesId}/thumbnail` return undefined } export function bookThumbnailUrl(bookId?: string): string | undefined { - if (bookId) return `${import.meta.env.VITE_KOMGA_API_URL}/api/v1/books/${bookId}/thumbnail` + if (bookId) return `${API_BASE_URL}/api/v1/books/${bookId}/thumbnail` return undefined } export function pageHashKnownThumbnailUrl(hash?: string): string | undefined { - if (hash) return `${import.meta.env.VITE_KOMGA_API_URL}/api/v1/page-hashes/${hash}/thumbnail` + if (hash) return `${API_BASE_URL}/api/v1/page-hashes/${hash}/thumbnail` return undefined } diff --git a/next-ui/src/api/komga-client.ts b/next-ui/src/api/komga-client.ts index d1dea65ff..eaf61ed44 100644 --- a/next-ui/src/api/komga-client.ts +++ b/next-ui/src/api/komga-client.ts @@ -1,6 +1,7 @@ import type { Middleware } from 'openapi-fetch' import createClient from 'openapi-fetch' import type { paths } from '@/generated/openapi/komga' +import { API_BASE_URL } from '@/api/base' // Middleware that throws on error, so it works with Pinia Colada const coladaMiddleware: Middleware = { @@ -27,7 +28,7 @@ const coladaMiddleware: Middleware = { } const client = createClient({ - baseUrl: import.meta.env.VITE_KOMGA_API_URL, + baseUrl: API_BASE_URL, // required to pass the session cookie on all requests credentials: 'include', // required to avoid browser basic-auth popups diff --git a/next-ui/src/mocks/api/httpTyped.ts b/next-ui/src/mocks/api/httpTyped.ts index 514399b9d..b56a07e98 100644 --- a/next-ui/src/mocks/api/httpTyped.ts +++ b/next-ui/src/mocks/api/httpTyped.ts @@ -1,4 +1,5 @@ import { createOpenApiHttp } from 'openapi-msw' import type { paths } from '@/generated/openapi/komga' +import { API_BASE_URL } from '@/api/base' -export const httpTyped = createOpenApiHttp({ baseUrl: import.meta.env.VITE_KOMGA_API_URL }) +export const httpTyped = createOpenApiHttp({ baseUrl: API_BASE_URL }) diff --git a/next-ui/src/router/index.ts b/next-ui/src/router/index.ts index a32130b4b..de4c9394a 100644 --- a/next-ui/src/router/index.ts +++ b/next-ui/src/router/index.ts @@ -10,7 +10,9 @@ import { setupLayouts } from 'virtual:generated-layouts' import { routes } from 'vue-router/auto-routes' const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), + history: createWebHistory( + import.meta.env.PROD ? window.resourceBaseUrl : import.meta.env.BASE_URL, + ), routes: setupLayouts(routes), }) diff --git a/next-ui/vite.config.mts b/next-ui/vite.config.mts index 9e818ed2c..12c50de87 100644 --- a/next-ui/vite.config.mts +++ b/next-ui/vite.config.mts @@ -81,6 +81,17 @@ export default defineConfig({ server: { port: 3000, }, + base: '/', + // support for the runtime base url (depending on the server.servlet.context-path) + // window.buildUrl is a function defined in index.html that dynamically provides the path + // it only works within js files though + experimental: { + renderBuiltUrl(filename, { hostType }) { + if (hostType === 'js') return { runtime: `window.buildUrl(${JSON.stringify(filename)})` } + // else if (hostType === 'html') return `@{/${filename}}` + else return { relative: true } + }, + }, optimizeDeps: { exclude: ['vuetify'], },