handle dynamic base url

This commit is contained in:
Gauthier Roebroeck 2025-10-15 16:11:26 +08:00
parent 2fdc876b34
commit 722c14bf3f
8 changed files with 94 additions and 6 deletions

View file

@ -60,3 +60,56 @@ Components are automatically imported using [unplugin-vue-components](https://gi
## Icons ## Icons
[UnoCSS Icons preset](https://unocss.dev/presets/icons) is used for icons, with the MDI set from Iconify. [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
<script
type="module"
crossorigin
src="/assets/index-xEUJQodq.js"
></script>
<link
rel="stylesheet"
crossorigin
href="/assets/index-CQqFNa2f.css"
/>
```
will be transformed to:
```html
<script
type="module"
crossorigin
src="/assets/index-xEUJQodq.js"
th:src="@{/assets/index-xEUJQodq.js}"
></script>
<link
rel="stylesheet"
crossorigin
href="/assets/index-CQqFNa2f.css"
th:href="@{/assets/index-CQqFNa2f.css}"
/>
```
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)

View file

@ -11,6 +11,16 @@
content="width=device-width, initial-scale=1.0" content="width=device-width, initial-scale=1.0"
/> />
<title>Komga</title> <title>Komga</title>
<script th:inline="javascript">
/*<![CDATA[*/
window.resourceBaseUrl = /*[(${"'" + baseUrl + "'"})]*/ '/'
/*]]>*/
</script>
<script>
window.buildUrl = (filename) => {
return `${window.resourceBaseUrl}${filename}`
}
</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

8
next-ui/src/api/base.ts Normal file
View file

@ -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

View file

@ -1,14 +1,16 @@
import { API_BASE_URL } from '@/api/base'
export function seriesThumbnailUrl(seriesId?: string): string | undefined { 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 return undefined
} }
export function bookThumbnailUrl(bookId?: string): string | 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 return undefined
} }
export function pageHashKnownThumbnailUrl(hash?: string): string | 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 return undefined
} }

View file

@ -1,6 +1,7 @@
import type { Middleware } from 'openapi-fetch' import type { Middleware } from 'openapi-fetch'
import createClient from 'openapi-fetch' import createClient from 'openapi-fetch'
import type { paths } from '@/generated/openapi/komga' 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 // Middleware that throws on error, so it works with Pinia Colada
const coladaMiddleware: Middleware = { const coladaMiddleware: Middleware = {
@ -27,7 +28,7 @@ const coladaMiddleware: Middleware = {
} }
const client = createClient<paths>({ const client = createClient<paths>({
baseUrl: import.meta.env.VITE_KOMGA_API_URL, baseUrl: API_BASE_URL,
// required to pass the session cookie on all requests // required to pass the session cookie on all requests
credentials: 'include', credentials: 'include',
// required to avoid browser basic-auth popups // required to avoid browser basic-auth popups

View file

@ -1,4 +1,5 @@
import { createOpenApiHttp } from 'openapi-msw' import { createOpenApiHttp } from 'openapi-msw'
import type { paths } from '@/generated/openapi/komga' import type { paths } from '@/generated/openapi/komga'
import { API_BASE_URL } from '@/api/base'
export const httpTyped = createOpenApiHttp<paths>({ baseUrl: import.meta.env.VITE_KOMGA_API_URL }) export const httpTyped = createOpenApiHttp<paths>({ baseUrl: API_BASE_URL })

View file

@ -10,7 +10,9 @@ import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes' import { routes } from 'vue-router/auto-routes'
const router = createRouter({ 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), routes: setupLayouts(routes),
}) })

View file

@ -81,6 +81,17 @@ export default defineConfig({
server: { server: {
port: 3000, 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: { optimizeDeps: {
exclude: ['vuetify'], exclude: ['vuetify'],
}, },