From ea8c3bf6022cb941e942f00aaaba94e16a92a168 Mon Sep 17 00:00:00 2001 From: mrkmntal Date: Wed, 8 Apr 2026 21:31:56 -0400 Subject: [PATCH] Makes the webpack build use relative paths to allow for deploying on web subdirectories --- server/scripts/modules/icons.mjs | 3 +- server/scripts/modules/linuxnews.mjs | 3 +- server/scripts/modules/serverobservations.mjs | 3 +- server/scripts/modules/utils/base-path.mjs | 25 ++++++++++++ server/scripts/modules/utils/cache.mjs | 3 +- server/scripts/modules/utils/data-loader.mjs | 4 +- .../modules/utils/leaflet-weather-map.mjs | 5 ++- server/scripts/modules/utils/mapclick.mjs | 3 +- server/scripts/modules/utils/theme.mjs | 39 ++++++++++--------- server/scripts/modules/utils/url-rewrite.mjs | 22 ++++++----- 10 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 server/scripts/modules/utils/base-path.mjs diff --git a/server/scripts/modules/icons.mjs b/server/scripts/modules/icons.mjs index db30d83..cf6f1ef 100644 --- a/server/scripts/modules/icons.mjs +++ b/server/scripts/modules/icons.mjs @@ -1,6 +1,7 @@ import largeIcon from './icons/icons-large.mjs'; import smallIcon from './icons/icons-small.mjs'; import hourlyIcon from './icons/icons-hourly.mjs'; +import { withBasePath } from './utils/base-path.mjs'; const getWeatherGovTokenFromWmoCode = (code) => { switch (Number(code)) { @@ -44,7 +45,7 @@ const getWeatherGovTokenFromWmoCode = (code) => { } }; -const buildSyntheticIconUrl = (code, isDaytime = true) => `/icons/land/${isDaytime ? 'day' : 'night'}/${getWeatherGovTokenFromWmoCode(code)}`; +const buildSyntheticIconUrl = (code, isDaytime = true) => withBasePath(`icons/land/${isDaytime ? 'day' : 'night'}/${getWeatherGovTokenFromWmoCode(code)}`); const getLargeIconFromWmoCode = (code, isDaytime = true) => largeIcon(buildSyntheticIconUrl(code, isDaytime), !isDaytime); const getSmallIconFromWmoCode = (code, isDaytime = true) => smallIcon(buildSyntheticIconUrl(code, isDaytime), !isDaytime); diff --git a/server/scripts/modules/linuxnews.mjs b/server/scripts/modules/linuxnews.mjs index 7ff3c8e..ba37c19 100644 --- a/server/scripts/modules/linuxnews.mjs +++ b/server/scripts/modules/linuxnews.mjs @@ -3,6 +3,7 @@ import STATUS from './status.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay } from './navigation.mjs'; import { debugFlag } from './utils/debug.mjs'; +import { withBasePath } from './utils/base-path.mjs'; const STORIES_PER_PAGE = 2; const PAGE_DURATION_MS = 9000; @@ -17,7 +18,7 @@ class LinuxNews extends WeatherDisplay { if (!super.getData(weatherParameters, refresh)) return; try { - const response = await safeJson('/api/linux-news', { + const response = await safeJson(withBasePath('api/linux-news'), { retryCount: 0, }); diff --git a/server/scripts/modules/serverobservations.mjs b/server/scripts/modules/serverobservations.mjs index 7b2d25e..c74deff 100644 --- a/server/scripts/modules/serverobservations.mjs +++ b/server/scripts/modules/serverobservations.mjs @@ -4,6 +4,7 @@ import STATUS from './status.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay } from './navigation.mjs'; import { debugFlag } from './utils/debug.mjs'; +import { withBasePath } from './utils/base-path.mjs'; const LINES_PER_PAGE = 4; const PAGE_DURATION_MS = 7000; @@ -19,7 +20,7 @@ class ServerObservations extends WeatherDisplay { try { // Fetch server info from the API - const response = await safeJson('/api/server-info', { + const response = await safeJson(withBasePath('api/server-info'), { retryCount: 0, }); diff --git a/server/scripts/modules/utils/base-path.mjs b/server/scripts/modules/utils/base-path.mjs new file mode 100644 index 0000000..e7b7d0d --- /dev/null +++ b/server/scripts/modules/utils/base-path.mjs @@ -0,0 +1,25 @@ +const normalizeBasePath = (pathname) => { + if (!pathname) return '/'; + if (pathname.endsWith('/index.html')) { + const trimmed = pathname.slice(0, -'index.html'.length); + return trimmed.endsWith('/') ? trimmed : `${trimmed}/`; + } + if (pathname.endsWith('/')) return pathname; + const lastSlash = pathname.lastIndexOf('/'); + if (lastSlash === -1) return '/'; + return `${pathname.slice(0, lastSlash + 1)}`; +}; + +const getBasePath = () => normalizeBasePath(window.location.pathname); + +const withBasePath = (relativePath = '') => { + const sanitizedPath = relativePath.replace(/^\/+/, ''); + const basePath = getBasePath(); + if (basePath === '/') return `/${sanitizedPath}`; + return `${basePath}${sanitizedPath}`; +}; + +export { + getBasePath, + withBasePath, +}; diff --git a/server/scripts/modules/utils/cache.mjs b/server/scripts/modules/utils/cache.mjs index d4c3413..72308bb 100644 --- a/server/scripts/modules/utils/cache.mjs +++ b/server/scripts/modules/utils/cache.mjs @@ -1,4 +1,5 @@ import { rewriteUrl } from './url-rewrite.mjs'; +import { withBasePath } from './base-path.mjs'; // Clear cache utility for client-side use const clearCacheEntry = async (url, baseUrl = '') => { @@ -15,7 +16,7 @@ const clearCacheEntry = async (url, baseUrl = '') => { } // Call the cache clear endpoint - const fetchUrl = baseUrl ? `${baseUrl}/cache${cachePath}` : `/cache${cachePath}`; + const fetchUrl = baseUrl ? `${baseUrl}/cache${cachePath}` : withBasePath(`cache${cachePath}`); const response = await fetch(fetchUrl, { method: 'DELETE', }); diff --git a/server/scripts/modules/utils/data-loader.mjs b/server/scripts/modules/utils/data-loader.mjs index 59cf7b7..1b98629 100644 --- a/server/scripts/modules/utils/data-loader.mjs +++ b/server/scripts/modules/utils/data-loader.mjs @@ -1,5 +1,7 @@ // Data loader utility for fetching JSON data with cache-busting +import { withBasePath } from './base-path.mjs'; + let dataCache = {}; // Load data with version-based cache busting @@ -9,7 +11,7 @@ const loadData = async (dataType, version = '') => { } try { - const url = `/data/${dataType}.json${version ? `?_=${version}` : ''}`; + const url = withBasePath(`data/${dataType}.json${version ? `?_=${version}` : ''}`); const response = await fetch(url); if (!response.ok) { diff --git a/server/scripts/modules/utils/leaflet-weather-map.mjs b/server/scripts/modules/utils/leaflet-weather-map.mjs index cd8ec39..b41641f 100644 --- a/server/scripts/modules/utils/leaflet-weather-map.mjs +++ b/server/scripts/modules/utils/leaflet-weather-map.mjs @@ -3,12 +3,13 @@ import { loadData } from './data-loader.mjs'; import { getSmallIconFromWmoCode } from '../icons.mjs'; import { getOpenMeteoObservationSnapshot } from './weather.mjs'; import { temperature } from './units.mjs'; +import { withBasePath } from './base-path.mjs'; const getBaseMapUrl = () => (window.WS4KP_SERVER_AVAILABLE - ? '/arcgis-server/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}' + ? withBasePath('arcgis-server/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}') : 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}'); const getBoundaryMapUrl = () => (window.WS4KP_SERVER_AVAILABLE - ? '/arcgis-services/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}' + ? withBasePath('arcgis-services/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}') : 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}'); const DEFAULT_MAX_NEARBY_MARKERS = 7; const MIN_CITY_DISTANCE_METERS = 25000; diff --git a/server/scripts/modules/utils/mapclick.mjs b/server/scripts/modules/utils/mapclick.mjs index 532f386..9608dcf 100644 --- a/server/scripts/modules/utils/mapclick.mjs +++ b/server/scripts/modules/utils/mapclick.mjs @@ -11,6 +11,7 @@ import { safeJson } from './fetch.mjs'; import { debugFlag } from './debug.mjs'; +import { withBasePath } from './base-path.mjs'; /** * Parse MapClick date format to JavaScript Date @@ -178,7 +179,7 @@ const convertMapClickIcon = (weatherImage) => { return null; } - return `/icons/land/${timeOfDay}/${condition}?size=medium`; + return withBasePath(`icons/land/${timeOfDay}/${condition}?size=medium`); }; /** diff --git a/server/scripts/modules/utils/theme.mjs b/server/scripts/modules/utils/theme.mjs index a94ee70..1540606 100644 --- a/server/scripts/modules/utils/theme.mjs +++ b/server/scripts/modules/utils/theme.mjs @@ -1,12 +1,15 @@ +import { withBasePath } from './base-path.mjs'; + const THEME_STORAGE_KEY = 'settings-theme-select'; const DEFAULT_THEME = 'default'; + const BUILTIN_ASSETS = { - background1: '../images/backgrounds/1.png', - background1Chart: '../images/backgrounds/1-chart.png', - background2: '../images/backgrounds/2.png', - background3: '../images/backgrounds/3.png', - background4: '../images/backgrounds/4.png', - background5: '../images/backgrounds/5.png', + background1: 'images/backgrounds/1.png', + background1Chart: 'images/backgrounds/1-chart.png', + background2: 'images/backgrounds/2.png', + background3: 'images/backgrounds/3.png', + background4: 'images/backgrounds/4.png', + background5: 'images/backgrounds/5.png', logoCorner: 'images/logos/logo-corner.png', }; @@ -20,33 +23,33 @@ const getStoredTheme = () => { const getThemeAssetUrl = (themeName, assetKey) => { if (themeName === DEFAULT_THEME) { - return BUILTIN_ASSETS[assetKey]; + return withBasePath(BUILTIN_ASSETS[assetKey]); } const themeAssetAvailability = getThemeAssets()[themeName] ?? {}; if (!themeAssetAvailability[assetKey]) { - return BUILTIN_ASSETS[assetKey]; + return withBasePath(BUILTIN_ASSETS[assetKey]); } switch (assetKey) { case 'background1': - return `../themes/${themeName}/1.png`; + return withBasePath(`themes/${themeName}/1.png`); case 'background1Chart': - return `../themes/${themeName}/1-chart.png`; + return withBasePath(`themes/${themeName}/1-chart.png`); case 'background2': - return `../themes/${themeName}/2.png`; + return withBasePath(`themes/${themeName}/2.png`); case 'background3': - return `../themes/${themeName}/3.png`; + return withBasePath(`themes/${themeName}/3.png`); case 'background4': - return `../themes/${themeName}/4.png`; + return withBasePath(`themes/${themeName}/4.png`); case 'background5': - return `../themes/${themeName}/5.png`; + return withBasePath(`themes/${themeName}/5.png`); case 'logoCorner': - return `themes/${themeName}/logo-corner.png`; + return withBasePath(`themes/${themeName}/logo-corner.png`); default: - return BUILTIN_ASSETS[assetKey]; - } -}; + return withBasePath(BUILTIN_ASSETS[assetKey]); + } + }; const applyTheme = (themeName) => { const selectedTheme = getAvailableThemes().includes(themeName) ? themeName : DEFAULT_THEME; diff --git a/server/scripts/modules/utils/url-rewrite.mjs b/server/scripts/modules/utils/url-rewrite.mjs index cc4267f..6df531a 100644 --- a/server/scripts/modules/utils/url-rewrite.mjs +++ b/server/scripts/modules/utils/url-rewrite.mjs @@ -1,4 +1,6 @@ // rewrite URLs to use local proxy server +import { withBasePath } from './base-path.mjs'; + const rewriteUrl = (_url) => { if (!_url) { throw new Error(`rewriteUrl called with invalid argument: '${_url}' (${typeof _url})`); @@ -25,44 +27,44 @@ const rewriteUrl = (_url) => { if (url.origin === 'https://api.weather.gov') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/api${url.pathname}`; + url.pathname = withBasePath(`api${url.pathname}`); } else if (url.origin === 'https://forecast.weather.gov') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/forecast${url.pathname}`; + url.pathname = withBasePath(`forecast${url.pathname}`); } else if (url.origin === 'https://www.spc.noaa.gov') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/spc${url.pathname}`; + url.pathname = withBasePath(`spc${url.pathname}`); } else if (url.origin === 'https://radar.weather.gov') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/radar${url.pathname}`; + url.pathname = withBasePath(`radar${url.pathname}`); } else if (url.origin === 'https://mesonet.agron.iastate.edu') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/mesonet${url.pathname}`; + url.pathname = withBasePath(`mesonet${url.pathname}`); } else if (url.origin === 'https://api.open-meteo.com') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/open-meteo${url.pathname}`; + url.pathname = withBasePath(`open-meteo${url.pathname}`); } else if (url.origin === 'https://api.rainviewer.com') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/rainviewer${url.pathname}`; + url.pathname = withBasePath(`rainviewer${url.pathname}`); } else if (url.origin === 'https://server.arcgisonline.com') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/arcgis-server${url.pathname}`; + url.pathname = withBasePath(`arcgis-server${url.pathname}`); } else if (url.origin === 'https://services.arcgisonline.com') { url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/arcgis-services${url.pathname}`; + url.pathname = withBasePath(`arcgis-services${url.pathname}`); } else if (typeof OVERRIDES !== 'undefined' && OVERRIDES?.RADAR_HOST && url.origin === `https://${OVERRIDES.RADAR_HOST}`) { // Handle override radar host url.protocol = window.location.protocol; url.host = window.location.host; - url.pathname = `/mesonet${url.pathname}`; + url.pathname = withBasePath(`mesonet${url.pathname}`); } return url;