diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c758bdd --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Copy to .env (next to docker-compose.yml) and adjust. Docker Compose reads .env automatically. + +# Full URL the browser uses to reach the game hub through the reverse proxy. +# Empty = "/hub". +SERVER_URL=https://game.example.com/server/hub + +# Prefix the game server should accept when the proxy forwards its sub-path without +# stripping it (e.g. /server). Leave empty if the proxy strips the prefix. +PATH_BASE= + +# Comma separated origins allowed to connect to the hub. Empty = allow any origin. +ALLOWED_ORIGINS=https://game.example.com + +# Optional gameplay tuning, passed to the server as Game__ (see GameSettings.cs). +# Add these under the server's "environment:" in docker-compose.yml to use them. +# Game__ItemSpawnChance=0.1 +# Game__CountdownSeconds=3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/README.md b/README.md index aa7f8d0..34b4884 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ docker network create actionBlocks_server docker compose up -d --build ``` +The website uses relative URLs. It works under a sub-path such as `/website/`, whether or not the proxy strips that prefix. + The reverse proxy (attached to both networks) should route: - the site to `actionblocks-website:80` @@ -47,6 +49,7 @@ The reverse proxy (attached to both networks) should route: | `SERVER_URL` | website | Hub URL as seen by the browser. Empty means `/hub`. Written into `config.js` at container start. | | `ALLOWED_ORIGINS` | server | Comma-separated CORS origins. Empty allows any origin. | | `HUB_PATH` | server | Hub endpoint path (default `/hub`). | +| `PATH_BASE` | server | Prefix to accept when the proxy forwards a sub-path unstripped, e.g. `/server`. | | `Game__*` | server | Overrides any `GameSettings` value, e.g. `Game__ItemSpawnChance=0.2`. | ## Extending diff --git a/actionBlocks_server/src/ActionBlocks.Server/Program.cs b/actionBlocks_server/src/ActionBlocks.Server/Program.cs index 2c274b1..312ff5f 100644 --- a/actionBlocks_server/src/ActionBlocks.Server/Program.cs +++ b/actionBlocks_server/src/ActionBlocks.Server/Program.cs @@ -10,6 +10,9 @@ var builder = WebApplication.CreateBuilder(args); var allowedOrigins = (builder.Configuration["ALLOWED_ORIGINS"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var hubPath = builder.Configuration["HUB_PATH"] ?? "/hub"; +// Optional prefix when the reverse proxy forwards a sub-path without stripping it, e.g. "/server". +// Requests without the prefix keep working. +var pathBase = builder.Configuration["PATH_BASE"] ?? ""; builder.Services.AddCors(options => options.AddDefaultPolicy(policy => { @@ -35,6 +38,11 @@ builder.Services.AddHostedService(); var app = builder.Build(); +if (!string.IsNullOrWhiteSpace(pathBase)) +{ + app.UsePathBase(pathBase); +} + app.UseCors(); app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.MapHub(hubPath); diff --git a/actionBlocks_website/docker/nginx.conf b/actionBlocks_website/docker/nginx.conf index bc420c7..699ae91 100644 --- a/actionBlocks_website/docker/nginx.conf +++ b/actionBlocks_website/docker/nginx.conf @@ -28,8 +28,14 @@ server { add_header Cache-Control "no-cache"; } - # Single page app fallback. location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ @prefixed; + } + + # The reverse proxy may forward a sub-path without stripping it (e.g. /website/static/x.js). + # Drop the leading path segment and try again; fall back to the app's index.html. + location @prefixed { + rewrite ^/[^/]+(/.*)$ $1 last; + rewrite ^ /index.html last; } } diff --git a/actionBlocks_website/index.html b/actionBlocks_website/index.html index c912138..07ec872 100644 --- a/actionBlocks_website/index.html +++ b/actionBlocks_website/index.html @@ -2,14 +2,18 @@ + + - + Action Blocks - +
diff --git a/actionBlocks_website/src/theme/assets.ts b/actionBlocks_website/src/theme/assets.ts index 3c8c561..8e9ee7c 100644 --- a/actionBlocks_website/src/theme/assets.ts +++ b/actionBlocks_website/src/theme/assets.ts @@ -3,6 +3,9 @@ * (or point these paths elsewhere) to change the look without touching components. */ +/** Resolves a file in /public relative to where the site is served (works under sub-paths). */ +const asset = (path: string) => `${import.meta.env.BASE_URL}${path}`; + export interface ItemVisual { /** Icon drawn on board cells, in inventories and item bars. */ icon: string; @@ -17,31 +20,31 @@ export interface ItemVisual { /** Keyed by the server item id (IItem.Id). Add an entry here when adding an item on the server. */ export const itemVisuals: Record = { add_line: { - icon: '/assets/items/add_line.svg', + icon: asset('assets/items/add_line.svg'), name: 'Add Line', color: '#ff4256', description: 'Pushes a garbage line into the target field.', }, clear_line: { - icon: '/assets/items/clear_line.svg', + icon: asset('assets/items/clear_line.svg'), name: 'Clear Line', color: '#4fe35f', description: 'Removes the bottom line of the target.', }, nuke: { - icon: '/assets/items/nuke.svg', + icon: asset('assets/items/nuke.svg'), name: 'Wipe Out', color: '#ffe23f', description: 'Clears every placed block of the target.', }, swap: { - icon: '/assets/items/swap.svg', + icon: asset('assets/items/swap.svg'), name: 'Switch', color: '#3fe3f2', description: 'Trades your whole field with the target.', }, blind: { - icon: '/assets/items/blind.svg', + icon: asset('assets/items/blind.svg'), name: 'Blackout', color: '#c452ff', description: 'Hides placed blocks and the drop shadow for 4 seconds.', @@ -49,7 +52,7 @@ export const itemVisuals: Record = { }; export const unknownItem: ItemVisual = { - icon: '/assets/items/unknown.svg', + icon: asset('assets/items/unknown.svg'), name: 'Item', color: '#ffffff', description: '', @@ -58,7 +61,7 @@ export const unknownItem: ItemVisual = { export const uiAssets = { /** Optional logo image. When null the text logo is rendered. */ logo: null as string | null, - favicon: '/assets/favicon.svg', + favicon: asset('assets/favicon.svg'), }; export function itemVisual(id: string): ItemVisual { diff --git a/actionBlocks_website/vite.config.ts b/actionBlocks_website/vite.config.ts index 0418ce5..a4ca4f5 100644 --- a/actionBlocks_website/vite.config.ts +++ b/actionBlocks_website/vite.config.ts @@ -6,6 +6,8 @@ const devServer = process.env.DEV_SERVER_URL ?? 'http://localhost:5080'; export default defineConfig({ plugins: [react()], + // Relative URLs so the site works under any sub-path (e.g. https://example.com/website/). + base: './', build: { // Hashed bundles go to /static; /assets holds the exchangeable graphics from public/. assetsDir: 'static', diff --git a/docker-compose.yml b/docker-compose.yml index 47d7725..bfbb46f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,11 @@ # website -> actionblocks-website:80 # game hub -> actionblocks-server:8080, path /hub (needs WebSocket upgrade support: # "Upgrade" and "Connection" headers must be forwarded). -# If the hub is served from a different host, set SERVER_URL accordingly and list the -# website origin in ALLOWED_ORIGINS. +# Sub-paths are supported (e.g. site at https://example.com/website/, server at /server): +# - the website works whether or not the proxy strips its prefix; +# - set SERVER_URL to the hub URL the browser should use, e.g. https://example.com/server/hub; +# - if the proxy does NOT strip the server prefix, set PATH_BASE=/server. +# If the hub is served from a different host, also list the website origin in ALLOWED_ORIGINS. services: actionblocks-website: @@ -26,6 +29,7 @@ services: environment: ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-} HUB_PATH: /hub + PATH_BASE: ${PATH_BASE:-} networks: - actionBlocks_server