From a4561cfb7d49d997a60d762e5b7b2b683c1e1c04 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Thu, 16 Jul 2026 19:45:46 +0200 Subject: [PATCH 1/2] feat: add docker container. --- .dockerignore | 20 +++++++++++ .env.example | 2 ++ .github/workflows/docker.yml | 70 ++++++++++++++++++++++++++++++++++++ Dockerfile | 40 +++++++++++++++++++++ app/config/constants.ts | 14 ++++---- app/config/runtime.ts | 66 ++++++++++++++++++++++++++++++++++ app/main.tsx | 15 ++++---- app/utils/code-runner.ts | 3 +- docker/90-app-config.sh | 70 ++++++++++++++++++++++++++++++++++++ docker/default.conf.template | 27 ++++++++++++++ docker/vite-placeholders.env | 4 +++ index.html | 1 + public/config.js | 2 ++ vite.config.mts | 1 + 14 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 app/config/runtime.ts create mode 100644 docker/90-app-config.sh create mode 100644 docker/default.conf.template create mode 100644 docker/vite-placeholders.env create mode 100644 public/config.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9a9a199 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +node_modules +dist +.git +.github +.husky +.env +.env.* +!.env.example +playwright-report +test-results +coverage +*.md +!README.md +.vscode +.idea +.cursor +*.log +temp +tmp +.tmp diff --git a/.env.example b/.env.example index 116d61a..0a89f86 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,8 @@ # ================= VITE_BASE_URL= +VITE_PATH_PREFIX= +VITE_OPENEO_API_URL=https://api.explorer.eopf.copernicus.eu/openeo VITE_APP_TITLE=openEO Studio VITE_APP_DESCRIPTION=Interactive code editor for openEO satellite imagery processing diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..762c57d --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,70 @@ +name: Docker + +on: + pull_request: + paths: + - 'Dockerfile' + - '.dockerignore' + - 'docker/**' + - '.github/workflows/docker.yml' + push: + branches: + - main + paths: + - 'Dockerfile' + - '.dockerignore' + - 'docker/**' + - '.github/workflows/docker.yml' + release: + types: [published] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..92ec1db --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build + +WORKDIR /app + +RUN corepack enable + +COPY package.json pnpm-lock.yaml ./ +ENV HUSKY=0 +RUN pnpm install --frozen-lockfile + +COPY . . + +# File copy (not ENV) so Docker does not expand the $ placeholders. +COPY docker/vite-placeholders.env .env.production.local + +RUN pnpm build + +FROM nginx:stable-alpine + +COPY --from=build /app/dist /usr/share/nginx/html + +COPY docker/default.conf.template /etc/nginx/templates/default.conf.template +COPY docker/90-app-config.sh /docker-entrypoint.d/90-app-config.sh +RUN chmod +x /docker-entrypoint.d/90-app-config.sh \ + && touch /etc/nginx/conf.d/path-prefix.inc + +ENV OPENEO_API_URL=https://api.explorer.eopf.copernicus.eu/openeo \ + BASE_URL= \ + PATH_PREFIX= \ + APP_TITLE="openEO Studio" \ + APP_DESCRIPTION="Interactive code editor for openEO satellite imagery processing" \ + MAPTILER_KEY= \ + AUTH_AUTHORITY= \ + AUTH_CLIENT_ID= \ + AUTH_REDIRECT_URI= \ + ENABLE_NARRATIVE_EXPORT=false + +EXPOSE 80 diff --git a/app/config/constants.ts b/app/config/constants.ts index bf2ccaf..cfe3693 100644 --- a/app/config/constants.ts +++ b/app/config/constants.ts @@ -1,10 +1,8 @@ -/** - * Application-wide constants from environment variables - */ -export const APP_TITLE = import.meta.env.VITE_APP_TITLE; -export const APP_DESCRIPTION = import.meta.env.VITE_APP_DESCRIPTION; +import { appConfig } from './runtime'; -export const MAPTILER_KEY = import.meta.env.VITE_MAPTILER_KEY; +export const APP_TITLE = appConfig.appTitle; +export const APP_DESCRIPTION = appConfig.appDescription; -export const ENABLE_NARRATIVE_EXPORT = - import.meta.env.VITE_ENABLE_NARRATIVE_EXPORT === 'true'; +export const MAPTILER_KEY = appConfig.maptilerKey; + +export const ENABLE_NARRATIVE_EXPORT = appConfig.enableNarrativeExport; diff --git a/app/config/runtime.ts b/app/config/runtime.ts new file mode 100644 index 0000000..39e7811 --- /dev/null +++ b/app/config/runtime.ts @@ -0,0 +1,66 @@ +/** + * Runtime config: window.__APP_CONFIG__ (Docker) with VITE_* fallbacks (local). + */ + +export interface AppConfig { + openeoApiUrl: string; + pathPrefix: string; + appTitle: string; + appDescription: string; + maptilerKey: string; + authAuthority: string; + authClientId: string; + authRedirectUri: string; + enableNarrativeExport: boolean; +} + +declare global { + interface Window { + __APP_CONFIG__?: Partial; + } +} + +function pick( + runtimeValue: string | undefined, + envValue: string | undefined, + fallback = '' +): string { + return runtimeValue || envValue || fallback; +} + +/** Leading slash, no trailing slash; empty when served at root. */ +function normalizePathPrefix(prefix: string): string { + if (!prefix || prefix === '/') return ''; + return `/${prefix.replace(/^\/+|\/+$/g, '')}`; +} + +const runtime = window.__APP_CONFIG__ ?? {}; + +export const appConfig: AppConfig = { + openeoApiUrl: pick( + runtime.openeoApiUrl, + import.meta.env.VITE_OPENEO_API_URL, + 'https://api.explorer.eopf.copernicus.eu/openeo' + ), + pathPrefix: normalizePathPrefix( + pick(runtime.pathPrefix, import.meta.env.VITE_PATH_PREFIX) + ), + appTitle: pick(runtime.appTitle, import.meta.env.VITE_APP_TITLE), + appDescription: pick( + runtime.appDescription, + import.meta.env.VITE_APP_DESCRIPTION + ), + maptilerKey: pick(runtime.maptilerKey, import.meta.env.VITE_MAPTILER_KEY), + authAuthority: pick( + runtime.authAuthority, + import.meta.env.VITE_AUTH_AUTHORITY + ), + authClientId: pick(runtime.authClientId, import.meta.env.VITE_AUTH_CLIENT_ID), + authRedirectUri: pick( + runtime.authRedirectUri, + import.meta.env.VITE_AUTH_REDIRECT_URI + ), + enableNarrativeExport: + runtime.enableNarrativeExport ?? + import.meta.env.VITE_ENABLE_NARRATIVE_EXPORT === 'true' +}; diff --git a/app/main.tsx b/app/main.tsx index c80be1b..621ea6d 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -6,6 +6,7 @@ import { StacApiProvider } from '@developmentseed/stac-react'; import { BrowserRouter } from 'react-router'; import { WebStorageStateStore } from 'oidc-client-ts'; +import { appConfig } from '$config/runtime'; import { PyodideProvider } from '$contexts/pyodide-context'; import { AuthMonitor } from '$utils/auth-monitor'; import { setupReloadDetector } from './utils/reload-detector'; @@ -21,15 +22,11 @@ if (import.meta.env.DEV) { setupReloadDetector(); } -const authAuthority = import.meta.env.VITE_AUTH_AUTHORITY || ''; -const authClientId = import.meta.env.VITE_AUTH_CLIENT_ID || ''; -const authRedirectUri = import.meta.env.VITE_AUTH_REDIRECT_URI || ''; - const oidcConfig: AuthProviderProps = { userStore: new WebStorageStateStore({ store: window.localStorage }), - authority: authAuthority, - client_id: authClientId, - redirect_uri: authRedirectUri, + authority: appConfig.authAuthority, + client_id: appConfig.authClientId, + redirect_uri: appConfig.authRedirectUri, onSigninCallback: (user) => { // eslint-disable-next-line no-console console.log('[AUTH] onSigninCallback triggered', { @@ -91,11 +88,11 @@ function Root() { const authProps = window.__MOCK_AUTH__ ? {} : oidcConfig; return ( - + {!window.__MOCK_AUTH__ && } - + diff --git a/app/utils/code-runner.ts b/app/utils/code-runner.ts index 289148d..dc3cb62 100644 --- a/app/utils/code-runner.ts +++ b/app/utils/code-runner.ts @@ -10,6 +10,7 @@ import { type BackendService } from '$types'; +import { appConfig } from '$config/runtime'; import { getInstanceId } from './instance-id'; // Track active services for cleanup @@ -18,7 +19,7 @@ const activeServices: ServiceInfo[] = []; export const EXAMPLE_CODE = trueColorAlgorithm; // OpenEO API constants -const OPENEO_API_URL = 'https://api.explorer.eopf.copernicus.eu/openeo'; +const OPENEO_API_URL = appConfig.openeoApiUrl; const AUTH_PREFIX = 'Bearer oidc/oidc/'; // Service title conventions for backend-side discovery diff --git a/docker/90-app-config.sh b/docker/90-app-config.sh new file mode 100644 index 0000000..94d116b --- /dev/null +++ b/docker/90-app-config.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# nginx entrypoint hook: write config.js and template index.html from env. +set -eu + +HTML_ROOT=/usr/share/nginx/html +CONFIG_JS="${HTML_ROOT}/config.js" +INDEX_HTML="${HTML_ROOT}/index.html" +PATH_PREFIX_INC=/etc/nginx/conf.d/path-prefix.inc + +# Leading slash, no trailing slash; empty for root. +normalize_prefix() { + prefix="$1" + if [ -z "$prefix" ] || [ "$prefix" = "/" ]; then + printf '' + return + fi + case "$prefix" in + /*) ;; + *) prefix="/${prefix}" ;; + esac + printf '%s' "$prefix" | sed 's|/*$||' +} + +PATH_PREFIX="$(normalize_prefix "$PATH_PREFIX")" + +json_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +if [ "$ENABLE_NARRATIVE_EXPORT" = "true" ]; then + ENABLE_NARRATIVE_EXPORT_JS=true +else + ENABLE_NARRATIVE_EXPORT_JS=false +fi + +cat >"$CONFIG_JS" <"$tmp_index" +mv "$tmp_index" "$INDEX_HTML" + +# so Vite relative assets resolve on deep links under PATH_PREFIX. +sed -i "s|||" "$INDEX_HTML" +chmod 644 "$INDEX_HTML" + +# Empty include when at root (avoids rewrite loops). +if [ -n "$PATH_PREFIX" ]; then + cat >"$PATH_PREFIX_INC" <"$PATH_PREFIX_INC" +fi + +echo "openeo-studio: wrote ${CONFIG_JS} (PATH_PREFIX='${PATH_PREFIX}', OPENEO_API_URL='${OPENEO_API_URL}')" diff --git a/docker/default.conf.template b/docker/default.conf.template new file mode 100644 index 0000000..816374f --- /dev/null +++ b/docker/default.conf.template @@ -0,0 +1,27 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + absolute_redirect off; + + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + + include /etc/nginx/conf.d/path-prefix.inc; + + location = /index.html { + add_header Cache-Control "no-cache" always; + } + + location = /config.js { + add_header Cache-Control "no-cache" always; + } + + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable" always; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker/vite-placeholders.env b/docker/vite-placeholders.env new file mode 100644 index 0000000..865f81c --- /dev/null +++ b/docker/vite-placeholders.env @@ -0,0 +1,4 @@ +# Escaped $ so Vite leaves these for envsubst at container startup. +VITE_BASE_URL=\${BASE_URL} +VITE_APP_TITLE=\${APP_TITLE} +VITE_APP_DESCRIPTION=\${APP_DESCRIPTION} diff --git a/index.html b/index.html index 7010b41..5d43940 100644 --- a/index.html +++ b/index.html @@ -99,6 +99,7 @@ }); + diff --git a/public/config.js b/public/config.js new file mode 100644 index 0000000..0046903 --- /dev/null +++ b/public/config.js @@ -0,0 +1,2 @@ +// Stub; overwritten at container startup. Local dev uses VITE_* fallbacks. +window.__APP_CONFIG__ = {}; diff --git a/vite.config.mts b/vite.config.mts index 39827ee..54e4f90 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -16,6 +16,7 @@ const alias = Object.entries(pkg.alias).reduce((acc, [key, value]) => { // https://vite.dev/config/ export default defineConfig({ + base: './', plugins: [react()], define: { APP_VERSION: JSON.stringify(pkg.version) From aa51644cf7df471b1d573f4fe15939001a66849c Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Wed, 22 Jul 2026 13:04:35 +0100 Subject: [PATCH 2/2] Derive base URL config from a single VITE_BASE_URL var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite's asset `base`, the router's basename, Docker's nginx rewrite/ tag, and the GitHub Pages 404 redirect were each configured through overlapping, duplicated vars (VITE_BASE_URL, VITE_PATH_PREFIX, a hardcoded vite.config.mts base, and a hardcoded 404.html segment count) that had to be kept in sync by hand and didn't actually agree with each other — building with a path prefix and serving the result locally didn't work. VITE_BASE_URL (BASE_URL in Docker) is now the single source of truth; everything else derives from it. Also replaces Docker's separate config.js file with an inline splice directly into index.html. --- .env.example | 6 +++- 404.html | 48 +++++++++++++++++++++++++ Dockerfile | 3 +- README.md | 21 +++++++++-- app/config/baseUrl.ts | 21 +++++++++++ app/config/runtime.ts | 4 ++- docker/90-app-config.sh | 46 ++++++++++++------------ docker/default.conf.template | 4 --- docker/lib.sh | 37 +++++++++++++++++++ index.html | 26 ++++++++------ public/404.html | 26 -------------- public/config.js | 2 -- vite.config.mts | 69 ++++++++++++++++++++++++++---------- 13 files changed, 223 insertions(+), 90 deletions(-) create mode 100644 404.html create mode 100644 app/config/baseUrl.ts create mode 100644 docker/lib.sh delete mode 100644 public/404.html delete mode 100644 public/config.js diff --git a/.env.example b/.env.example index 0a89f86..b9866dd 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,12 @@ # App Configuration # ================= +# Full URL where the app is served, including any path prefix — this is +# the only var needed to configure where the app is mounted. Everything +# else (Vite's asset base, the router, Docker's nginx rewrite, and the +# GitHub Pages 404 redirect) derives from it automatically. +# Examples: http://localhost:9000 | https://example.com/subpath VITE_BASE_URL= -VITE_PATH_PREFIX= VITE_OPENEO_API_URL=https://api.explorer.eopf.copernicus.eu/openeo VITE_APP_TITLE=openEO Studio VITE_APP_DESCRIPTION=Interactive code editor for openEO satellite imagery processing diff --git a/404.html b/404.html new file mode 100644 index 0000000..bb06dd9 --- /dev/null +++ b/404.html @@ -0,0 +1,48 @@ + + + + + Redirecting... + + + +

Redirecting...

+ + diff --git a/Dockerfile b/Dockerfile index 92ec1db..e428b37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,13 +22,12 @@ FROM nginx:stable-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY docker/default.conf.template /etc/nginx/templates/default.conf.template -COPY docker/90-app-config.sh /docker-entrypoint.d/90-app-config.sh +COPY docker/90-app-config.sh docker/lib.sh /docker-entrypoint.d/ RUN chmod +x /docker-entrypoint.d/90-app-config.sh \ && touch /etc/nginx/conf.d/path-prefix.inc ENV OPENEO_API_URL=https://api.explorer.eopf.copernicus.eu/openeo \ BASE_URL= \ - PATH_PREFIX= \ APP_TITLE="openEO Studio" \ APP_DESCRIPTION="Interactive code editor for openEO satellite imagery processing" \ MAPTILER_KEY= \ diff --git a/README.md b/README.md index d8666a3..1a67870 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,15 @@ To configure the application: 2. Modify the `.env` file with your specific configuration values 3. Never modify `.env.example` directly as it serves as documentation +#### Where the app is mounted: `VITE_BASE_URL` + +`VITE_BASE_URL` is the one variable that controls where the app is served from — set it once to the full URL (including any path prefix) and everything else follows automatically: Vite's built asset paths, the client-side router's mount point, Docker's nginx rewrite rules, and the GitHub Pages 404 redirect. + +- **Local dev** (`pnpm dev`): leave it as the dev server's own URL, e.g. `http://localhost:9000` (no path prefix). +- **`pnpm build` + serving locally under a prefix**: set it to the full URL you'll serve from, e.g. `http://localhost:8888/subpath`, then serve `dist/` so it's reachable at that path (e.g. behind a reverse proxy). The build's asset URLs and router will match automatically. +- **GitHub Pages**: set the `VITE_BASE_URL` repository variable to the site's full deployed URL, e.g. `https://developmentseed.github.io/openeo-studio` for a project page, or your custom domain root if one is configured. +- **Docker**: set the `BASE_URL` environment variable at `docker run` time (see below) — no rebuild needed to change it. + ### Starting the app ```sh @@ -78,7 +87,13 @@ or pnpm stage ``` -This will package the app and place all the contents in the `dist` directory. -The app can then be run by any web server. +This will package the app and place all the contents in the `dist` directory, with asset URLs already baked to match whatever `VITE_BASE_URL` was set to at build time. The app can then be run by any web server capable of serving a single-page app (falling back to `index.html` for unknown paths). + +### Docker + +```sh +docker build -t openeo-studio . +docker run -p 8888:80 -e BASE_URL=http://localhost:8888/subpath openeo-studio +``` -**When building the site for deployment provide the base url trough the `VITE_BASE_URL` environment variable. Omit the leading slash. (E.g. )** +The image is built once with a relative asset base and configured per-container via environment variables (`BASE_URL`, `OPENEO_API_URL`, `APP_TITLE`, etc. — see `Dockerfile` for the full list). The entrypoint script derives the mount path from `BASE_URL` and writes it into nginx's rewrite rules, the page's `` tag, and `window.__APP_CONFIG__` at container start — so the same image can be redeployed under a different `BASE_URL` without rebuilding. diff --git a/app/config/baseUrl.ts b/app/config/baseUrl.ts new file mode 100644 index 0000000..169a84b --- /dev/null +++ b/app/config/baseUrl.ts @@ -0,0 +1,21 @@ +/** + * Parses VITE_BASE_URL / BASE_URL — the single source of truth for where + * the app is mounted — into a URL, or undefined if it isn't a real, + * parseable absolute URL yet. During Docker's image build, VITE_BASE_URL is + * deliberately left as the literal escaped string `${BASE_URL}` (see + * docker/vite-placeholders.env) to be resolved by envsubst at container + * start; callers must treat that case the same as "not set yet". + */ +export function parseBaseUrl(rawUrl: string | undefined): URL | undefined { + if (!rawUrl) return undefined; + try { + return new URL(rawUrl); + } catch { + return undefined; + } +} + +/** The URL's path only, no trailing slash; '' if there is no path or no valid URL. */ +export function pathPrefixFromUrl(rawUrl: string | undefined): string { + return parseBaseUrl(rawUrl)?.pathname.replace(/\/+$/, '') ?? ''; +} diff --git a/app/config/runtime.ts b/app/config/runtime.ts index 39e7811..4c75229 100644 --- a/app/config/runtime.ts +++ b/app/config/runtime.ts @@ -2,6 +2,8 @@ * Runtime config: window.__APP_CONFIG__ (Docker) with VITE_* fallbacks (local). */ +import { pathPrefixFromUrl } from '$config/baseUrl'; + export interface AppConfig { openeoApiUrl: string; pathPrefix: string; @@ -43,7 +45,7 @@ export const appConfig: AppConfig = { 'https://api.explorer.eopf.copernicus.eu/openeo' ), pathPrefix: normalizePathPrefix( - pick(runtime.pathPrefix, import.meta.env.VITE_PATH_PREFIX) + pick(runtime.pathPrefix, pathPrefixFromUrl(import.meta.env.VITE_BASE_URL)) ), appTitle: pick(runtime.appTitle, import.meta.env.VITE_APP_TITLE), appDescription: pick( diff --git a/docker/90-app-config.sh b/docker/90-app-config.sh index 94d116b..9fe2aa1 100644 --- a/docker/90-app-config.sh +++ b/docker/90-app-config.sh @@ -1,27 +1,16 @@ #!/bin/sh -# nginx entrypoint hook: write config.js and template index.html from env. +# nginx entrypoint hook: template index.html from env. set -eu HTML_ROOT=/usr/share/nginx/html -CONFIG_JS="${HTML_ROOT}/config.js" INDEX_HTML="${HTML_ROOT}/index.html" PATH_PREFIX_INC=/etc/nginx/conf.d/path-prefix.inc -# Leading slash, no trailing slash; empty for root. -normalize_prefix() { - prefix="$1" - if [ -z "$prefix" ] || [ "$prefix" = "/" ]; then - printf '' - return - fi - case "$prefix" in - /*) ;; - *) prefix="/${prefix}" ;; - esac - printf '%s' "$prefix" | sed 's|/*$||' -} +. "$(dirname "$0")/lib.sh" -PATH_PREFIX="$(normalize_prefix "$PATH_PREFIX")" +# BASE_URL is the single source of truth for where the app is mounted — +# derive the path prefix from it instead of a separate env var. +PATH_PREFIX="$(normalize_prefix "$(derive_prefix "$BASE_URL")")" json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' @@ -33,7 +22,18 @@ else ENABLE_NARRATIVE_EXPORT_JS=false fi -cat >"$CONFIG_JS" <"$tmp_index" +mv "$tmp_index" "$INDEX_HTML" + +# Splice the runtime app config in as an inline script where index.html has +# . Using sed's `r` (read file) instead of `s|...|...|` +# avoids fighting sed's replacement-text escaping rules for values that may +# contain slashes or quotes (URLs, titles, etc.). +config_script="$(mktemp)" +cat >"$config_script" < window.__APP_CONFIG__ = { openeoApiUrl: "$(json_escape "$OPENEO_API_URL")", pathPrefix: "$(json_escape "$PATH_PREFIX")", @@ -45,16 +45,18 @@ window.__APP_CONFIG__ = { authRedirectUri: "$(json_escape "$AUTH_REDIRECT_URI")", enableNarrativeExport: ${ENABLE_NARRATIVE_EXPORT_JS} }; + EOF -chmod 644 "$CONFIG_JS" tmp_index="$(mktemp)" -# shellcheck disable=SC2016 -envsubst '${BASE_URL} ${APP_TITLE} ${APP_DESCRIPTION}' <"$INDEX_HTML" >"$tmp_index" +sed -e "//r ${config_script}" -e "//d" "$INDEX_HTML" >"$tmp_index" mv "$tmp_index" "$INDEX_HTML" +rm -f "$config_script" # so Vite relative assets resolve on deep links under PATH_PREFIX. -sed -i "s|||" "$INDEX_HTML" +tmp_index="$(mktemp)" +sed "s|||" "$INDEX_HTML" >"$tmp_index" +mv "$tmp_index" "$INDEX_HTML" chmod 644 "$INDEX_HTML" # Empty include when at root (avoids rewrite loops). @@ -67,4 +69,4 @@ else : >"$PATH_PREFIX_INC" fi -echo "openeo-studio: wrote ${CONFIG_JS} (PATH_PREFIX='${PATH_PREFIX}', OPENEO_API_URL='${OPENEO_API_URL}')" +echo "openeo-studio: templated ${INDEX_HTML} (PATH_PREFIX='${PATH_PREFIX}', OPENEO_API_URL='${OPENEO_API_URL}')" diff --git a/docker/default.conf.template b/docker/default.conf.template index 816374f..2d6e0e8 100644 --- a/docker/default.conf.template +++ b/docker/default.conf.template @@ -13,10 +13,6 @@ server { add_header Cache-Control "no-cache" always; } - location = /config.js { - add_header Cache-Control "no-cache" always; - } - location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable" always; } diff --git a/docker/lib.sh b/docker/lib.sh new file mode 100644 index 0000000..9803a56 --- /dev/null +++ b/docker/lib.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Pure string/path helpers shared by 90-app-config.sh. No side effects — +# safe to source directly in tests. + +# Leading slash, no trailing slash; empty for root. +normalize_prefix() { + prefix="$1" + if [ -z "$prefix" ] || [ "$prefix" = "/" ]; then + printf '' + return + fi + case "$prefix" in + /*) ;; + *) prefix="/${prefix}" ;; + esac + printf '%s' "$prefix" | sed 's|/*$||' +} + +# Extracts the path from a BASE_URL of the form scheme://host[/path]. +# Empty string if there's no scheme (e.g. unset, or an unresolved +# `${BASE_URL}` build-time placeholder that hasn't been envsubst'd yet). +derive_prefix() { + case "$1" in + http://*|https://*) + rest=${1#*://} + path=${rest#*/} + if [ "$path" = "$rest" ]; then + printf '' + else + printf '/%s' "$path" + fi + ;; + *) + printf '' + ;; + esac +} diff --git a/index.html b/index.html index 5d43940..756fff9 100644 --- a/index.html +++ b/index.html @@ -25,16 +25,22 @@