From 3026221cd6c133e2b9d502e96a78e1f6585336e6 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Thu, 26 Feb 2026 16:03:19 +0100 Subject: [PATCH] Support PathBase deployments under subfolders --- README.md | 1 + RpgRoller/Components/App.razor | 23 ++++++++++++++++++++--- RpgRoller/Program.cs | 14 +++++++++++++- RpgRoller/wwwroot/js/rpgroller-api.js | 17 +++++++++++++++-- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1dc5973..6b8c463 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ VS Code F5 debug profiles are available in `.vscode/launch.json`: - `RpgRoller: Server + Firefox (F5)` To use a custom SQLite database path, set `ConnectionStrings__RpgRoller`. +To run under a subfolder (for example `/rpgroller`), set `PathBase` (for example `PathBase=/rpgroller`). For migration authoring, use the local tool command form: diff --git a/RpgRoller/Components/App.razor b/RpgRoller/Components/App.razor index 56a5ee1..889ee3a 100644 --- a/RpgRoller/Components/App.razor +++ b/RpgRoller/Components/App.razor @@ -5,9 +5,9 @@ - + RpgRoller - + @@ -16,7 +16,24 @@ - + + +@code { + [CascadingParameter] + private Microsoft.AspNetCore.Http.HttpContext? HttpContext { get; set; } + + private string BaseHref + { + get + { + var pathBase = HttpContext?.Request.PathBase.Value; + if (string.IsNullOrWhiteSpace(pathBase)) + return "/"; + + return pathBase.EndsWith('/') ? pathBase : $"{pathBase}/"; + } + } +} diff --git a/RpgRoller/Program.cs b/RpgRoller/Program.cs index 4cba0fe..8733b17 100644 --- a/RpgRoller/Program.cs +++ b/RpgRoller/Program.cs @@ -10,6 +10,18 @@ builder.Services.AddScoped(); var app = builder.Build(); app.InitializeRpgRollerState(); +var configuredPathBase = builder.Configuration["PathBase"]; +if (!string.IsNullOrWhiteSpace(configuredPathBase)) +{ + var normalizedPathBase = configuredPathBase.Trim(); + if (!normalizedPathBase.StartsWith('/')) + normalizedPathBase = $"/{normalizedPathBase}"; + + normalizedPathBase = normalizedPathBase.TrimEnd('/'); + if (normalizedPathBase.Length > 0) + app.UsePathBase(normalizedPathBase); +} + app.UseStaticFiles(); app.UseAntiforgery(); @@ -17,4 +29,4 @@ app.MapRpgRollerApi(); app.MapRazorComponents().AddInteractiveServerRenderMode(); app.Run(); -public partial class Program; \ No newline at end of file +public partial class Program; diff --git a/RpgRoller/wwwroot/js/rpgroller-api.js b/RpgRoller/wwwroot/js/rpgroller-api.js index 1e8d5cc..9cc2fe5 100644 --- a/RpgRoller/wwwroot/js/rpgroller-api.js +++ b/RpgRoller/wwwroot/js/rpgroller-api.js @@ -9,6 +9,19 @@ window.rpgRollerApi = (() => { stopped: true }; + function toAppUrl(url) { + if (!url || typeof url !== "string") { + return url; + } + + if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { + return url; + } + + const relativeUrl = url.startsWith("/") ? url.slice(1) : url; + return new URL(relativeUrl, document.baseURI).toString(); + } + function clearReconnectTimer() { if (stateStream.reconnectTimer) { clearTimeout(stateStream.reconnectTimer); @@ -50,7 +63,7 @@ window.rpgRollerApi = (() => { clearReconnectTimer(); invokeDotNet("OnConnectionStateChanged", "reconnecting"); - const source = new EventSource(`/api/events/state?campaignId=${encodeURIComponent(stateStream.campaignId)}`); + const source = new EventSource(toAppUrl(`api/events/state?campaignId=${encodeURIComponent(stateStream.campaignId)}`)); stateStream.source = source; source.onopen = () => { @@ -122,7 +135,7 @@ window.rpgRollerApi = (() => { let response; try { - response = await fetch(url, options); + response = await fetch(toAppUrl(url), options); } catch (error) { return { ok: false,