# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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.
This commit is contained in:
Anthony Stirling 2025-09-24 20:37:51 +01:00 committed by GitHub
parent 6441dc1d6f
commit 166f6d2d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 51 additions and 18 deletions

View File

@ -2,6 +2,7 @@
<html lang="en-GB"> <html lang="en-GB">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<base href="%BASE_URL%" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />

View File

@ -5,6 +5,7 @@ import LocalIcon from './LocalIcon';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useFileHandler } from '../../hooks/useFileHandler'; import { useFileHandler } from '../../hooks/useFileHandler';
import { useFilesModalContext } from '../../contexts/FilesModalContext'; import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { BASE_PATH } from '../../constants/app';
const LandingPage = () => { const LandingPage = () => {
const { addFiles } = useFileHandler(); const { addFiles } = useFileHandler();
@ -72,7 +73,7 @@ const LandingPage = () => {
}} }}
> >
<img <img
src={colorScheme === 'dark' ? '/branding/StirlingPDFLogoNoTextDark.svg' : '/branding/StirlingPDFLogoNoTextLight.svg'} src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoNoTextDark.svg` : `${BASE_PATH}/branding/StirlingPDFLogoNoTextLight.svg`}
alt="Stirling PDF Logo" alt="Stirling PDF Logo"
style={{ style={{
height: 'auto', height: 'auto',
@ -98,7 +99,7 @@ const LandingPage = () => {
{/* Stirling PDF Branding */} {/* Stirling PDF Branding */}
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<img <img
src={colorScheme === 'dark' ? '/branding/StirlingPDFLogoWhiteText.svg' : '/branding/StirlingPDFLogoGreyText.svg'} src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
alt="Stirling PDF" alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }} style={{ height: '2.2rem', width: 'auto' }}
/> />

View File

@ -6,6 +6,7 @@ import { useTooltipPosition } from '../../hooks/useTooltipPosition';
import { TooltipTip } from '../../types/tips'; import { TooltipTip } from '../../types/tips';
import { TooltipContent } from './tooltip/TooltipContent'; import { TooltipContent } from './tooltip/TooltipContent';
import { useSidebarContext } from '../../contexts/SidebarContext'; import { useSidebarContext } from '../../contexts/SidebarContext';
import { BASE_PATH } from '../../constants/app';
import styles from './tooltip/Tooltip.module.css'; import styles from './tooltip/Tooltip.module.css';
export interface TooltipProps { export interface TooltipProps {
@ -328,7 +329,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
<div className={styles['tooltip-logo']}> <div className={styles['tooltip-logo']}>
{header.logo || ( {header.logo || (
<img <img
src="/logo-tooltip.svg" src={`${BASE_PATH}/logo-tooltip.svg`}
alt="Stirling PDF" alt="Stirling PDF"
style={{ width: '1.4rem', height: '1.4rem', display: 'block' }} style={{ width: '1.4rem', height: '1.4rem', display: 'block' }}
/> />

View File

@ -5,3 +5,19 @@ export const getBaseUrl = (): string => {
const { config } = useAppConfig(); const { config } = useAppConfig();
return config?.baseUrl || 'https://stirling.com'; 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}`;
};

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BASE_PATH } from '../constants/app';
declare global { declare global {
interface Window { interface Window {
@ -37,17 +38,17 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf
// Load the cookie consent CSS files first // Load the cookie consent CSS files first
const mainCSS = document.createElement('link'); const mainCSS = document.createElement('link');
mainCSS.rel = 'stylesheet'; mainCSS.rel = 'stylesheet';
mainCSS.href = '/css/cookieconsent.css'; mainCSS.href = `${BASE_PATH}/css/cookieconsent.css`;
document.head.appendChild(mainCSS); document.head.appendChild(mainCSS);
const customCSS = document.createElement('link'); const customCSS = document.createElement('link');
customCSS.rel = 'stylesheet'; customCSS.rel = 'stylesheet';
customCSS.href = '/css/cookieconsentCustomisation.css'; customCSS.href = `${BASE_PATH}/css/cookieconsentCustomisation.css`;
document.head.appendChild(customCSS); document.head.appendChild(customCSS);
// Load the cookie consent library // Load the cookie consent library
const script = document.createElement('script'); const script = document.createElement('script');
script.src = '/js/thirdParty/cookieconsent.umd.js'; script.src = `${BASE_PATH}/js/thirdParty/cookieconsent.umd.js`;
script.onload = () => { script.onload = () => {
// Small delay to ensure DOM is ready // Small delay to ensure DOM is ready
setTimeout(() => { setTimeout(() => {

View File

@ -7,6 +7,7 @@ import { ToolId } from '../types/toolId';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
import { ToolRegistry } from '../data/toolsTaxonomy'; import { ToolRegistry } from '../data/toolsTaxonomy';
import { firePixel } from '../utils/scarfTracking'; import { firePixel } from '../utils/scarfTracking';
import { withBasePath } from '../constants/app';
/** /**
* Hook to sync workbench and tool with URL using registry * Hook to sync workbench and tool with URL using registry
@ -51,7 +52,8 @@ export function useNavigationUrlSync(
} else if (prevSelectedTool.current !== null) { } else if (prevSelectedTool.current !== null) {
// Only clear URL if we had a tool before (user navigated away) // 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 // 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 clearToolRoute(false); // Use pushState for user navigation
} }
} }

View File

@ -74,7 +74,9 @@ i18n
loadPath: (lngs: string[], namespaces: string[]) => { loadPath: (lngs: string[], namespaces: string[]) => {
// Map 'en' to 'en-GB' for loading translations // Map 'en' to 'en-GB' for loading translations
const lng = lngs[0] === 'en' ? 'en-GB' : lngs[0]; 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`;
}, },
}, },

View File

@ -10,6 +10,7 @@ import App from './App';
import './i18n'; // Initialize i18next import './i18n'; // Initialize i18next
import posthog from 'posthog-js'; import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react'; import { PostHogProvider } from 'posthog-js/react';
import { BASE_PATH } from './constants/app';
// Compute initial color scheme // Compute initial color scheme
function getInitialScheme(): 'light' | 'dark' { function getInitialScheme(): 'light' | 'dark' {
@ -60,7 +61,7 @@ root.render(
<PostHogProvider <PostHogProvider
client={posthog} client={posthog}
> >
<BrowserRouter> <BrowserRouter basename={BASE_PATH}>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</PostHogProvider> </PostHogProvider>

View File

@ -1,10 +1,11 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { BaseToolProps } from "../types/tool"; import { BaseToolProps } from "../types/tool";
import { withBasePath } from "../constants/app";
const SwaggerUI: React.FC<BaseToolProps> = () => { const SwaggerUI: React.FC<BaseToolProps> = () => {
useEffect(() => { useEffect(() => {
// Redirect to Swagger UI // 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 ( return (
@ -12,7 +13,7 @@ const SwaggerUI: React.FC<BaseToolProps> = () => {
<p>Opening Swagger UI in a new tab...</p> <p>Opening Swagger UI in a new tab...</p>
<p> <p>
If it didn't open automatically,{" "} If it didn't open automatically,{" "}
<a href="/swagger-ui/5.21.0/index.html" target="_blank" rel="noopener noreferrer"> <a href={withBasePath("/swagger-ui/5.21.0/index.html")} target="_blank" rel="noopener noreferrer">
click here click here
</a> </a>
</p> </p>

View File

@ -8,12 +8,17 @@ import { getDefaultWorkbench } from '../types/workbench';
import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy'; import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy';
import { firePixel } from './scarfTracking'; import { firePixel } from './scarfTracking';
import { URL_TO_TOOL_MAP } from './urlMapping'; import { URL_TO_TOOL_MAP } from './urlMapping';
import { BASE_PATH, withBasePath } from '../constants/app';
/** /**
* Parse the current URL to extract tool routing information * Parse the current URL to extract tool routing information
*/ */
export function parseToolRoute(registry: ToolRegistry): ToolRoute { 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); const searchParams = new URLSearchParams(window.location.search);
// First, check URL mapping for multiple URL aliases // First, check URL mapping for multiple URL aliases
@ -83,7 +88,8 @@ export function updateToolRoute(toolId: ToolId, registry: ToolRegistry, replace:
return; return;
} }
const newPath = getToolUrlPath(toolId, tool); const toolPath = getToolUrlPath(toolId, tool);
const newPath = withBasePath(toolPath);
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
// Remove tool query parameter since we're using path-based routing // 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); const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('tool'); 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; const baseUrl = window.location.origin;
if (!toolId || !registry[toolId]) { if (!toolId || !registry[toolId]) {
return baseUrl; return `${baseUrl}${BASE_PATH || ''}`;
} }
const tool = registry[toolId]; const tool = registry[toolId];
const path = getToolUrlPath(toolId, tool); const toolPath = getToolUrlPath(toolId, tool);
return `${baseUrl}${path}`; const fullPath = withBasePath(toolPath);
return `${baseUrl}${fullPath}`;
} }

View File

@ -12,5 +12,5 @@ export default defineConfig({
}, },
}, },
}, },
base: "./", base: process.env.RUN_SUBPATH ? `/${process.env.RUN_SUBPATH}` : './',
}); });