diff --git a/IIS.md b/IIS.md
index 997d8e1..93ff5d3 100644
--- a/IIS.md
+++ b/IIS.md
@@ -21,7 +21,7 @@
- `AllowedHosts=picknplay.example.com;www.picknplay.example.com`
- Optional: enable stdout logging in `web.config` during troubleshooting only; disable afterward.
- Data protection keys are persisted to `App_Data/keys`; ensure this folder is deployed and writable so auth cookies stay valid across app pool recycles.
-- Frontend base path: set `` in `wwwroot/index.html` for production so API calls include the subpath (keep blank for local/root).
+- Frontend base path is injected during deployment by `scripts/deploy-ftp.ps1` using deploy profile `BasePath` (falls back to last `RemoteDir` segment if omitted). This keeps local `wwwroot/index.html` unchanged while production API calls target `/picknplay/api`.
- Deployment script: copy `scripts/deploy-ftp.profile.sample.psd1` to `scripts/deploy-ftp.profile.psd1`, fill environment values, then run `pwsh ./scripts/deploy-ftp.ps1 -ProfilePath ./scripts/deploy-ftp.profile.psd1`.
- Shortcut command: run `pwsh ./deploy.ps1` from repo root to deploy with the local profile directly.
- Prefer `WinScpSessionName` in the deploy profile to avoid embedding FTP credentials in scripted URLs.
diff --git a/README.md b/README.md
index ed8e0e5..c371c32 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ Pick'n'Play is a .NET 10 ASP.NET Core Minimal API app with a static HTML/CSS/JS
- `GameList.Tests/`: integration and helper tests.
- `scripts/`: deployment scripts (`scripts/deploy-ftp.ps1`, `scripts/deploy-ftp1.ps1`).
- `deploy.ps1`: local shortcut wrapper that runs FTP deploy using `scripts/deploy-ftp.profile.psd1`.
+ Deploy sets frontend `` automatically from deploy profile `BasePath` (or inferred from `RemoteDir`).
## Operations
diff --git a/scripts/deploy-ftp.profile.sample.psd1 b/scripts/deploy-ftp.profile.sample.psd1
index 5e20b98..bcce81e 100644
--- a/scripts/deploy-ftp.profile.sample.psd1
+++ b/scripts/deploy-ftp.profile.sample.psd1
@@ -9,6 +9,7 @@
# Required sync settings
WinScpPath = "C:\Program Files (x86)\WinSCP\WinSCP.com"
RemoteDir = "/httpdocs/picknplay"
+ BasePath = "/picknplay"
# Preferred: use a named WinSCP stored session (no credential string in script)
WinScpSessionName = "picknplay-prod"
diff --git a/scripts/deploy-ftp.ps1 b/scripts/deploy-ftp.ps1
index bdea593..48c1c7e 100644
--- a/scripts/deploy-ftp.ps1
+++ b/scripts/deploy-ftp.ps1
@@ -54,6 +54,86 @@ function Resolve-ProfilePath {
return [System.IO.Path]::GetFullPath((Join-Path $BaseDirectory $expanded))
}
+function Normalize-BasePath {
+ param([string]$Value)
+
+ if ([string]::IsNullOrWhiteSpace($Value)) {
+ return ""
+ }
+
+ $normalized = $Value.Trim()
+ if (-not $normalized.StartsWith("/")) {
+ $normalized = "/$normalized"
+ }
+
+ if ($normalized.Length -gt 1) {
+ $normalized = $normalized.TrimEnd("/")
+ }
+
+ return $normalized
+}
+
+function Infer-BasePathFromRemoteDir {
+ param([string]$RemoteDir)
+
+ if ([string]::IsNullOrWhiteSpace($RemoteDir)) {
+ return ""
+ }
+
+ $segments = @($RemoteDir -split "[/\\]" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
+ if ($segments.Count -eq 0) {
+ return ""
+ }
+
+ $candidate = $segments[$segments.Count - 1]
+ if ($candidate -in @("httpdocs", "wwwroot", "www", "public_html", "site")) {
+ return ""
+ }
+
+ return Normalize-BasePath $candidate
+}
+
+function Resolve-AppBasePath {
+ param([Parameter(Mandatory = $true)][hashtable]$Config)
+
+ if ($Config.ContainsKey("BasePath")) {
+ $configured = Normalize-BasePath ([string]$Config.BasePath)
+ if (-not [string]::IsNullOrWhiteSpace($configured)) {
+ return $configured
+ }
+ }
+
+ return Infer-BasePathFromRemoteDir ([string]$Config.RemoteDir)
+}
+
+function Set-FrontendAppBaseMeta {
+ param(
+ [Parameter(Mandatory = $true)][string]$PublishDir,
+ [Parameter(Mandatory = $true)][string]$BasePath
+ )
+
+ $indexPath = Join-Path $PublishDir "index.html"
+ if (-not (Test-Path $indexPath)) {
+ throw "Publish output is missing index.html at '$indexPath'."
+ }
+
+ $pattern = ''
+ $content = Get-Content -Path $indexPath -Raw
+ if ($content -notmatch $pattern) {
+ throw "Could not find in '$indexPath'."
+ }
+
+ $replacement = ""
+ $updated = [System.Text.RegularExpressions.Regex]::Replace(
+ $content,
+ $pattern,
+ [System.Text.RegularExpressions.MatchEvaluator]{ param($match) $replacement },
+ 1
+ )
+
+ Set-Content -Path $indexPath -Value $updated -Encoding UTF8
+}
+
function Read-PlainOrPrompt {
param(
[string]$Value,
@@ -174,6 +254,10 @@ if (-not $selfContained) {
}
dotnet @publishArgs
+$appBasePath = Resolve-AppBasePath -Config $config
+Set-FrontendAppBaseMeta -PublishDir $publishDir -BasePath $appBasePath
+Write-Host "2) Frontend app-base configured as '$appBasePath'." -ForegroundColor Cyan
+
if ($recycleAppPool) {
Require-ConfigValue $config "AppPoolName"
$appPoolName = [string]$config.AppPoolName