From 166f6d2d980b2dbf0e846186db64e37ee8bdd83f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:37:51 +0100 Subject: [PATCH] path (#4488) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- frontend/index.html | 1 + .../src/components/shared/LandingPage.tsx | 5 +++-- frontend/src/components/shared/Tooltip.tsx | 3 ++- frontend/src/constants/app.ts | 16 ++++++++++++++++ frontend/src/hooks/useCookieConsent.ts | 7 ++++--- frontend/src/hooks/useUrlSync.ts | 4 +++- frontend/src/i18n.ts | 4 +++- frontend/src/index.tsx | 3 ++- frontend/src/tools/SwaggerUI.tsx | 5 +++-- frontend/src/utils/urlRouting.ts | 19 +++++++++++++------ frontend/vite.config.ts | 2 +- 11 files changed, 51 insertions(+), 18 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 31f1b3008..b563bdcd8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,6 +2,7 @@ + diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx index a3ea42fab..787b0e565 100644 --- a/frontend/src/components/shared/LandingPage.tsx +++ b/frontend/src/components/shared/LandingPage.tsx @@ -5,6 +5,7 @@ import LocalIcon from './LocalIcon'; import { useTranslation } from 'react-i18next'; import { useFileHandler } from '../../hooks/useFileHandler'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; +import { BASE_PATH } from '../../constants/app'; const LandingPage = () => { const { addFiles } = useFileHandler(); @@ -72,7 +73,7 @@ const LandingPage = () => { }} > Stirling PDF Logo { {/* Stirling PDF Branding */} Stirling PDF diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index 160857a50..543422609 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -6,6 +6,7 @@ import { useTooltipPosition } from '../../hooks/useTooltipPosition'; import { TooltipTip } from '../../types/tips'; import { TooltipContent } from './tooltip/TooltipContent'; import { useSidebarContext } from '../../contexts/SidebarContext'; +import { BASE_PATH } from '../../constants/app'; import styles from './tooltip/Tooltip.module.css'; export interface TooltipProps { @@ -328,7 +329,7 @@ export const Tooltip: React.FC = ({
{header.logo || ( Stirling PDF diff --git a/frontend/src/constants/app.ts b/frontend/src/constants/app.ts index 3b8b765df..c83cffcfd 100644 --- a/frontend/src/constants/app.ts +++ b/frontend/src/constants/app.ts @@ -5,3 +5,19 @@ export const getBaseUrl = (): string => { const { config } = useAppConfig(); return config?.baseUrl || 'https://stirling.com'; }; + +// Base path from Vite config - build-time constant, normalized (no trailing slash) +// When no subpath, use empty string instead of '.' to avoid relative path issues +export const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '').replace(/^\.$/, ''); + +/** For in-app navigations when you must touch window.location (rare). */ +export const withBasePath = (path: string): string => { + const clean = path.startsWith('/') ? path : `/${path}`; + return `${BASE_PATH}${clean}`; +}; + +/** For OAuth (needs absolute URL with scheme+host) */ +export const absoluteWithBasePath = (path: string): string => { + const clean = path.startsWith('/') ? path : `/${path}`; + return `${window.location.origin}${BASE_PATH}${clean}`; +}; diff --git a/frontend/src/hooks/useCookieConsent.ts b/frontend/src/hooks/useCookieConsent.ts index dd00f4396..1dd324ce7 100644 --- a/frontend/src/hooks/useCookieConsent.ts +++ b/frontend/src/hooks/useCookieConsent.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { BASE_PATH } from '../constants/app'; declare global { interface Window { @@ -37,17 +38,17 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf // Load the cookie consent CSS files first const mainCSS = document.createElement('link'); mainCSS.rel = 'stylesheet'; - mainCSS.href = '/css/cookieconsent.css'; + mainCSS.href = `${BASE_PATH}/css/cookieconsent.css`; document.head.appendChild(mainCSS); const customCSS = document.createElement('link'); customCSS.rel = 'stylesheet'; - customCSS.href = '/css/cookieconsentCustomisation.css'; + customCSS.href = `${BASE_PATH}/css/cookieconsentCustomisation.css`; document.head.appendChild(customCSS); // Load the cookie consent library const script = document.createElement('script'); - script.src = '/js/thirdParty/cookieconsent.umd.js'; + script.src = `${BASE_PATH}/js/thirdParty/cookieconsent.umd.js`; script.onload = () => { // Small delay to ensure DOM is ready setTimeout(() => { diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts index e65e5500f..a90ee4fbe 100644 --- a/frontend/src/hooks/useUrlSync.ts +++ b/frontend/src/hooks/useUrlSync.ts @@ -7,6 +7,7 @@ import { ToolId } from '../types/toolId'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; import { ToolRegistry } from '../data/toolsTaxonomy'; import { firePixel } from '../utils/scarfTracking'; +import { withBasePath } from '../constants/app'; /** * Hook to sync workbench and tool with URL using registry @@ -51,7 +52,8 @@ export function useNavigationUrlSync( } else if (prevSelectedTool.current !== null) { // Only clear URL if we had a tool before (user navigated away) // Don't clear on initial load when both current and previous are null - if (window.location.pathname !== '/') { + const homePath = withBasePath('/'); + if (window.location.pathname !== homePath) { clearToolRoute(false); // Use pushState for user navigation } } diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index b0ce8fdf7..f57d4b1e3 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -74,7 +74,9 @@ i18n loadPath: (lngs: string[], namespaces: string[]) => { // Map 'en' to 'en-GB' for loading translations const lng = lngs[0] === 'en' ? 'en-GB' : lngs[0]; - return `/locales/${lng}/${namespaces[0]}.json`; + const basePath = import.meta.env.BASE_URL || '/'; + const cleanBasePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + return `${cleanBasePath}/locales/${lng}/${namespaces[0]}.json`; }, }, diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 1976eb49d..431aa7bf3 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -10,6 +10,7 @@ import App from './App'; import './i18n'; // Initialize i18next import posthog from 'posthog-js'; import { PostHogProvider } from 'posthog-js/react'; +import { BASE_PATH } from './constants/app'; // Compute initial color scheme function getInitialScheme(): 'light' | 'dark' { @@ -60,7 +61,7 @@ root.render( - + diff --git a/frontend/src/tools/SwaggerUI.tsx b/frontend/src/tools/SwaggerUI.tsx index 20d67f496..32034def3 100644 --- a/frontend/src/tools/SwaggerUI.tsx +++ b/frontend/src/tools/SwaggerUI.tsx @@ -1,10 +1,11 @@ import React, { useEffect } from "react"; import { BaseToolProps } from "../types/tool"; +import { withBasePath } from "../constants/app"; const SwaggerUI: React.FC = () => { useEffect(() => { // Redirect to Swagger UI - window.open("/swagger-ui/5.21.0/index.html", "_blank"); + window.open(withBasePath("/swagger-ui/5.21.0/index.html"), "_blank"); }, []); return ( @@ -12,7 +13,7 @@ const SwaggerUI: React.FC = () => {

Opening Swagger UI in a new tab...

If it didn't open automatically,{" "} - + click here

diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts index 3ca35e9a7..d14ca923a 100644 --- a/frontend/src/utils/urlRouting.ts +++ b/frontend/src/utils/urlRouting.ts @@ -8,12 +8,17 @@ import { getDefaultWorkbench } from '../types/workbench'; import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy'; import { firePixel } from './scarfTracking'; import { URL_TO_TOOL_MAP } from './urlMapping'; +import { BASE_PATH, withBasePath } from '../constants/app'; /** * Parse the current URL to extract tool routing information */ export function parseToolRoute(registry: ToolRegistry): ToolRoute { - const path = window.location.pathname; + const fullPath = window.location.pathname; + // Remove base path to get app-relative path + const path = BASE_PATH && fullPath.startsWith(BASE_PATH) + ? fullPath.slice(BASE_PATH.length) || '/' + : fullPath; const searchParams = new URLSearchParams(window.location.search); // First, check URL mapping for multiple URL aliases @@ -83,7 +88,8 @@ export function updateToolRoute(toolId: ToolId, registry: ToolRegistry, replace: return; } - const newPath = getToolUrlPath(toolId, tool); + const toolPath = getToolUrlPath(toolId, tool); + const newPath = withBasePath(toolPath); const searchParams = new URLSearchParams(window.location.search); // Remove tool query parameter since we're using path-based routing @@ -99,7 +105,7 @@ export function clearToolRoute(replace: boolean = false): void { const searchParams = new URLSearchParams(window.location.search); searchParams.delete('tool'); - updateUrl('/', searchParams, replace); + updateUrl(withBasePath('/'), searchParams, replace); } /** @@ -117,11 +123,12 @@ export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegist const baseUrl = window.location.origin; if (!toolId || !registry[toolId]) { - return baseUrl; + return `${baseUrl}${BASE_PATH || ''}`; } const tool = registry[toolId]; - const path = getToolUrlPath(toolId, tool); - return `${baseUrl}${path}`; + const toolPath = getToolUrlPath(toolId, tool); + const fullPath = withBasePath(toolPath); + return `${baseUrl}${fullPath}`; } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1db9de625..59ebfd663 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,5 +12,5 @@ export default defineConfig({ }, }, }, - base: "./", + base: process.env.RUN_SUBPATH ? `/${process.env.RUN_SUBPATH}` : './', });