Support PathBase deployments under subfolders

This commit is contained in:
2026-02-26 16:03:19 +01:00
parent ba8141b336
commit 3026221cd6
4 changed files with 49 additions and 6 deletions

View File

@@ -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:

View File

@@ -5,9 +5,9 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<base href="@BaseHref"/>
<title>RpgRoller</title>
<link rel="stylesheet" href="/styles.css"/>
<link rel="stylesheet" href="styles.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
@@ -16,7 +16,24 @@
</head>
<body>
<Routes @rendermode="InteractiveServer"/>
<script src="/js/rpgroller-api.js"></script>
<script src="js/rpgroller-api.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
@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}/";
}
}
}

View File

@@ -10,6 +10,18 @@ builder.Services.AddScoped<RpgRollerApiClient>();
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<App>().AddInteractiveServerRenderMode();
app.Run();
public partial class Program;
public partial class Program;

View File

@@ -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,