From 3b104be7ccea2763a3f2f5daab54b83c4ad2d37d Mon Sep 17 00:00:00 2001 From: chai-guy-7 <265797038+chai-guy-7@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:45:40 +0000 Subject: [PATCH 1/2] feat(app): support serving the explorer from a configurable base path Allow the frontend to be served under a URL subpath (e.g. /explorer) instead of only at the domain root, for deployments behind prefix-preserving reverse proxies. - vite.config.ts: base from VITE_BASE_PATH (build time); Docker builds use a relative base so one image works at any subpath - index.html: tag templated from BASE_PATH (gomplate at container start or the html-transform plugin locally); config.js and favicon defaults are now base-relative - new src/utils/basePath.ts: getBasePath/basePathPrefix/publicAsset/ appUrl/appRootUrl/currentAppPath helpers reading the live tag - router uses createWebHistory() so vue-router picks up the tag - absolute /images/... references and window.location redirects (401 login redirect, wallet switch reload, OAuth redirect_uri and post_logout_redirect_uri, MetaMask blockExplorerUrls) now resolve through the helpers; root-deployment output is unchanged - nginx.conf.subpath.template: serves under ${BASE_PATH} prefix when the BASE_PATH env var is set; the existing template and rendered config remain untouched when it is unset - Dockerfile: VITE_BASE_PATH build arg; CMD normalizes BASE_PATH and selects the nginx template at container start Verified at root and under /explorer (direct and behind a prefix-preserving proxy) against a real nginx with the rendered configs: assets, deep links, client-side navigation, config.js and images all resolve; default deployment output is functionally identical to before. --- packages/app/Dockerfile | 20 ++- packages/app/README.md | 8 + packages/app/index.html | 7 +- packages/app/nginx.conf.subpath.template | 51 ++++++ packages/app/src/App.vue | 3 +- packages/app/src/components/Account.vue | 5 +- .../src/components/ConnectMetamaskButton.vue | 4 +- packages/app/src/components/Contract.vue | 5 +- packages/app/src/components/NetworkSwitch.vue | 7 +- packages/app/src/components/Token.vue | 3 +- .../app/src/components/TokenIconLabel.vue | 4 +- .../app/src/components/header/TheHeader.vue | 11 +- .../components/prividium/NetworkIndicator.vue | 4 +- .../src/components/prividium/WalletButton.vue | 3 +- .../components/prividium/WalletInfoModal.vue | 4 +- .../app/src/composables/useFetchInstance.ts | 3 +- packages/app/src/composables/useLogin.ts | 3 +- packages/app/src/composables/useWallet.ts | 3 +- packages/app/src/lib/prividium-auth/index.ts | 4 +- packages/app/src/router/index.ts | 4 +- packages/app/src/utils/basePath.ts | 90 ++++++++++ packages/app/src/views/AuthCallbackView.vue | 8 +- packages/app/src/views/LoginView.vue | 8 +- packages/app/src/views/NotAuthorizedView.vue | 8 +- packages/app/tests/utils/basePath.spec.ts | 162 ++++++++++++++++++ packages/app/vite.config.ts | 21 +++ 26 files changed, 424 insertions(+), 29 deletions(-) create mode 100644 packages/app/nginx.conf.subpath.template create mode 100644 packages/app/src/utils/basePath.ts create mode 100644 packages/app/tests/utils/basePath.spec.ts diff --git a/packages/app/Dockerfile b/packages/app/Dockerfile index 7f34a5d2cd..f4b32afa2b 100644 --- a/packages/app/Dockerfile +++ b/packages/app/Dockerfile @@ -23,6 +23,11 @@ ARG VITE_VERSION= ENV VITE_VERSION=$VITE_VERSION ARG VITE_APP_ENVIRONMENT=default ENV VITE_APP_ENVIRONMENT=$VITE_APP_ENVIRONMENT +# Optional explicit base path baked in at build time. When empty (default), +# the build uses a relative base so the image can be served from any subpath +# configured at container start via the BASE_PATH env var. See vite.config.ts. +ARG VITE_BASE_PATH= +ENV VITE_BASE_PATH=$VITE_BASE_PATH ENV DOCKER_BUILD=true RUN npm run build @@ -33,10 +38,23 @@ ENV CSP_REPORT_ONLY="default-src 'self'; child-src 'self' blob:; script-src 'sel RUN apk add --no-cache gomplate COPY --from=build-stage /usr/src/app/packages/app/nginx.conf.template /etc/nginx/conf.d/default.conf.template +COPY --from=build-stage /usr/src/app/packages/app/nginx.conf.subpath.template /etc/nginx/conf.d/subpath.conf.template COPY --from=build-stage /usr/src/app/packages/app/dist /usr/share/nginx/html RUN cp /usr/share/nginx/html/index.html /usr/share/nginx/html/index.html.tpl -CMD envsubst '$PORT $CSP_REPORT_ONLY' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf \ +# BASE_PATH (e.g. "/explorer") serves the app under a subpath for +# prefix-preserving reverse proxies. When unset, behavior is unchanged. +# nginx needs the prefix without a trailing slash, the tag (filled +# by gomplate from BASE_PATH) needs it with one. +CMD BP="${BASE_PATH#/}" && BP="${BP%/}" \ + && if [ -n "$BP" ]; then \ + export BASE_PATH="/$BP" \ + && envsubst '$PORT $CSP_REPORT_ONLY $BASE_PATH' < /etc/nginx/conf.d/subpath.conf.template > /etc/nginx/conf.d/default.conf \ + && export BASE_PATH="/$BP/"; \ + else \ + export BASE_PATH="/" \ + && envsubst '$PORT $CSP_REPORT_ONLY' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf; \ + fi \ && gomplate -f /usr/share/nginx/html/index.html.tpl -o /usr/share/nginx/html/index.html \ && exec nginx -g 'daemon off;' diff --git a/packages/app/README.md b/packages/app/README.md index adf724906a..c2e245af93 100644 --- a/packages/app/README.md +++ b/packages/app/README.md @@ -47,6 +47,14 @@ settlement chains: For a complete example of network configuration including settlement chains, refer to [`production.config.json`](./src/configs/production.config.json). +### Serving the app from a subpath +By default the app is served from the root of the domain (e.g. `https://explorer.example.com/`). To serve it from a subpath (e.g. `https://example.com/explorer/`), there are two options: + +- **Build time**: set the `VITE_BASE_PATH` env variable when building (e.g. `VITE_BASE_PATH=/explorer/ npm run build`). This bakes the base path into the build output. It also works for the dev server: `VITE_BASE_PATH=/explorer/ npm run dev`. +- **Container runtime**: the published Docker image is built with a relative base, so the base path can be configured at container start by setting the `BASE_PATH` env variable (e.g. `docker run -e BASE_PATH=/explorer ...`). This injects a `` tag into `index.html` and configures nginx to serve the app under the prefix. No rebuild is needed. `BASE_PATH` must be a plain path prefix consisting of URL path segments (letters, digits, `-`, `_`, `/`), since it is interpolated into an nginx location and rewrite rule. + +When the app sits behind a reverse proxy that forwards the subpath prefix to the container (prefix-preserving, e.g. `proxy.example.com/explorer/*` -> `container/explorer/*`), set `BASE_PATH` to that prefix. If the proxy strips the prefix before forwarding, leave `BASE_PATH` unset and note that the app would then generate root-relative URLs which will not work under the proxied subpath, so prefix-preserving proxying is the supported setup. + ### Compile and Hot-Reload for Development ```sh diff --git a/packages/app/index.html b/packages/app/index.html index b4a391c0ad..7c3d6d3372 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -1,6 +1,7 @@ + {{ getenv "VITE_BRAND_NAME" | default "ZKsync" }} Block Explorer @@ -13,12 +14,12 @@ - - + + - + diff --git a/packages/app/nginx.conf.subpath.template b/packages/app/nginx.conf.subpath.template new file mode 100644 index 0000000000..dbb9be176b --- /dev/null +++ b/packages/app/nginx.conf.subpath.template @@ -0,0 +1,51 @@ +# Used instead of nginx.conf.template when the BASE_PATH env var is set +# (see the Dockerfile CMD). Serves the app under the ${BASE_PATH} prefix +# for prefix-preserving reverse proxies. BASE_PATH must have a leading +# slash and no trailing slash (e.g. /explorer); the Dockerfile normalizes it. +server { + listen ${PORT}; + listen [::]:${PORT} default; + + root /usr/share/nginx/html; + index index.html; + + server_name _; # all hostnames + + # Emit relative Location headers so redirects survive https-terminating + # reverse proxies in front of this container. + absolute_redirect off; + + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + gzip_min_length 256; + gzip_proxied any; + gzip_vary on; + + # Redirect the bare prefix to its trailing-slash form so relative + # asset URLs resolve correctly. + location = ${BASE_PATH} { + return 301 ${BASE_PATH}/; + } + + location ${BASE_PATH}/ { + # Static files live at the filesystem root, strip the prefix. + rewrite ^${BASE_PATH}/(.*)$ /$1 break; + # "=404" makes "/index.html" a file check (SPA fallback) instead of an + # internal redirect, which would re-match "location /" and return 404. + try_files $uri /index.html =404; + # Add headers to all responses + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Content-Security-Policy-Report-Only "${CSP_REPORT_ONLY}" always; + } + + # Anything outside the prefix is not served. + location / { + return 404; + } + + # TODO: add cache for static files, avoid caching config.js, index.html etc. +} diff --git a/packages/app/src/App.vue b/packages/app/src/App.vue index 808cb02fad..1263db36ce 100644 --- a/packages/app/src/App.vue +++ b/packages/app/src/App.vue @@ -4,7 +4,7 @@ v-if="isPrividiumAuthChecking" class="flex min-h-screen flex-col items-center justify-center bg-gradient-to-br from-slate-50 via-white to-blue-50 px-4 py-12 sm:px-6 lg:px-8" > - Prividium Logo + Prividium Logo

Checking permissions...

@@ -46,6 +46,7 @@ import useLocalization from "@/composables/useLocalization"; import useLogin from "@/composables/useLogin"; import useRouteTitle from "@/composables/useRouteTitle"; +import { publicAsset } from "@/utils/basePath"; import MaintenanceView from "@/views/MaintenanceView.vue"; const { setup } = useLocalization(); diff --git a/packages/app/src/components/Account.vue b/packages/app/src/components/Account.vue index 54f07fa851..20a720be73 100644 --- a/packages/app/src/components/Account.vue +++ b/packages/app/src/components/Account.vue @@ -16,7 +16,7 @@