Wire demo for production deploy + add server config

Fixes images 404'ing under /arrangement/ — Vite's publicDir copies assets
to the build root, but the base prefix is only applied to bundled assets
(JS/CSS), not to runtime URL strings. assetUrl() helper resolves paths
against import.meta.env.BASE_URL so '/images/foo.png' becomes
'/arrangement/images/foo.png' in production while staying '/images/foo.png'
in dev.

- src/demo/shared/assets.ts — assetUrl() helper
- providers.ts + DemoNav.tsx — wrap all public asset paths
- nginx/parsons-demos.conf — swag site-conf for parsons.tensordesign.com.au
  (asset cache regex above SPA fallback regex per nginx first-match rule)
- docs/reference/client-demo-deploy.md — server runbook (DNS, swag
  SUBDOMAINS, mount, htpasswd, deploy loop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 17:22:40 +10:00
parent 45d73759c1
commit cd7f99f59d
5 changed files with 289 additions and 12 deletions

81
nginx/parsons-demos.conf Normal file
View File

@@ -0,0 +1,81 @@
# Parsons demo host — drop into swag's /config/nginx/site-confs/ directory.
#
# Serves static demo slices at parsons.tensordesign.com.au/<slice>/ behind
# basic auth. One server block, one cert (Let's Encrypt via swag), one
# htpasswd covering all slices.
#
# Document root layout (host filesystem):
# <host_path>/parsons-demos/
# index.html ← optional landing page listing slices
# arrangement/
# index.html
# assets/...
# <other-slices>/
#
# Bind-mount that directory into swag at /config/www/parsons-demos/ — the
# `root` directive below assumes that path. Adjust if you mount elsewhere.
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name parsons.*;
# swag manages the cert chain via SUBDOMAINS — make sure `parsons` is in
# the SUBDOMAINS env var of the swag container so this resolves.
include /config/nginx/ssl.conf;
root /config/www/parsons-demos;
index index.html;
# One credential file covering every slice. Create with:
# docker exec -it swag htpasswd -c /config/nginx/.htpasswd-parsons client
auth_basic "Parsons demos";
auth_basic_user_file /config/nginx/.htpasswd-parsons;
# Optional: don't auth the root listing if you want it publicly visible.
# (Currently auth covers it too — change to `auth_basic off;` to expose.)
# Root path serves the optional landing index.html if present, else 404.
location = / {
try_files /index.html =404;
}
# Long cache for fingerprinted assets — Vite produces hashed filenames so
# this is safe. HTML is short-cache so updates land on next refresh.
# NOTE: asset + html regex locations must come BEFORE the slice fallback
# below, because nginx uses the first matching regex location.
location ~* \.(?:js|css|woff2?|ttf|otf|eot|png|jpg|jpeg|gif|svg|webp|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# SPA fallback per slice. /<slice>/<react-route> resolves to that
# slice's index.html so React Router handles the rest. Static assets
# (.js/.css/.png/etc.) are handled by the regex blocks above.
location ~ ^/(?<slice>[^/]+)/ {
try_files $uri $uri/ /$slice/index.html;
}
# Hide hidden files (e.g. .htpasswd if it ever ends up in webroot)
location ~ /\. {
deny all;
}
}
# HTTP → HTTPS redirect — swag's default server already covers this for
# wildcard subdomains, but include explicitly here in case the default is
# customised.
server {
listen 80;
listen [::]:80;
server_name parsons.*;
return 301 https://$host$request_uri;
}