4 Commits

Author SHA1 Message Date
16b0568bf1 fix: bypass static index.html serving to allow dynamic header path rewriting
All checks were successful
Build and Push Presejpacky Docker Image / build-and-push (push) Successful in 6s
2026-06-08 00:08:25 +02:00
afd29c0a23 feat: add request header logging to dev and prod servers
All checks were successful
Build and Push Presejpacky Docker Image / build-and-push (push) Successful in 7s
2026-06-08 00:05:06 +02:00
c7c6bed56c docs: add Nginx reverse proxy configuration guide
All checks were successful
Build and Push Presejpacky Docker Image / build-and-push (push) Successful in 8s
2026-06-07 23:55:23 +02:00
f106ba0248 feat: support dynamic base path in Express server via proxy headers
All checks were successful
Build and Push Presejpacky Docker Image / build-and-push (push) Successful in 6s
2026-06-07 23:53:34 +02:00
3 changed files with 172 additions and 5 deletions

View File

@@ -0,0 +1,113 @@
# Nginx Reverse Proxy Configuration Guide
This guide describes how to configure an Nginx reverse proxy to host the `flat-stack-presejpacky` container, either at the root domain or under a specific subpath (e.g., `/presejpacky`), using dynamic path rewriting based on HTTP headers.
## Overview
The application is built to be path-agnostic at compile time. At runtime, the Express server inspects HTTP headers to dynamically rewrite script and style references in `index.html` and strip the path prefix from incoming resource requests.
This enables running the same Docker image under any subpath without rebuilds.
The server checks for the presence of the following headers (in order):
1. `X-Forwarded-Prefix`
2. `X-Base-Path`
---
## 1. Hosting under a Subpath (e.g., `/presejpacky`)
To serve the application on a subpath, configure Nginx to pass the request path prefix in the `X-Forwarded-Prefix` header.
### Option A: Stripping the prefix in Nginx (Recommended)
If your `proxy_pass` directive has a trailing slash, Nginx automatically strips the matched URI prefix before forwarding the request to the container.
```nginx
server {
listen 80;
server_name drills.home.hrajfrisbee.cz;
location /presejpacky/ {
# Forward requests to the container, stripping "/presejpacky"
proxy_pass http://presejpacky-container:3000/;
# Forward host and IP headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Pass the subpath prefix so the container can dynamically prefix asset paths
proxy_set_header X-Forwarded-Prefix /presejpacky;
}
# Redirect requests missing a trailing slash (e.g., /presejpacky -> /presejpacky/)
# to ensure relative browser assets resolve correctly.
location = /presejpacky {
return 301 $scheme://$host$request_uri/;
}
}
```
### Option B: Preserving the prefix in Nginx
If your `proxy_pass` directive does not have a trailing slash, Nginx forwards the request with the subpath prefix intact. The Express server's built-in middleware will strip it.
```nginx
server {
listen 80;
server_name drills.home.hrajfrisbee.cz;
location /presejpacky/ {
# Forward requests with "/presejpacky" intact
proxy_pass http://presejpacky-container:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /presejpacky;
}
location = /presejpacky {
return 301 $scheme://$host$request_uri/;
}
}
```
---
## 2. Hosting on a Root Domain (e.g., `https://presejpacky.example.com`)
If you want to dedicate an entire domain or subdomain to the application, no prefix stripping or rewriting is required.
```nginx
server {
listen 80;
server_name presejpacky.example.com;
location / {
proxy_pass http://presejpacky-container:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
---
## 3. Verifying the Setup
You can verify that the headers are being set and processed correctly by inspecting the source of the returned HTML in the browser:
- If Nginx is correctly sending `X-Forwarded-Prefix: /presejpacky`, the references in the `<head>` and `<body>` tags of `index.html` should be rendered with the prefix:
```html
<link rel="stylesheet" href="/presejpacky/assets/index-XYZ.css">
<script type="module" src="/presejpacky/assets/index-XYZ.js"></script>
```
- If the headers are not set or not forwarded, the paths will fallback to the root:
```html
<link rel="stylesheet" href="/assets/index-XYZ.css">
<script type="module" src="/assets/index-XYZ.js"></script>
```

View File

@@ -1,6 +1,7 @@
import express from 'express'; import express from 'express';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -8,12 +9,58 @@ const __dirname = path.dirname(__filename);
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// Serve static files from the 'dist' directory // Log all incoming request methods, URLs, and headers
app.use(express.static(path.join(__dirname, 'dist'))); app.use((req, res, next) => {
console.log(`[Express Request] ${req.method} ${req.url}`);
console.log('Headers:', JSON.stringify(req.headers, null, 2));
next();
});
// Fallback to index.html for Single Page Application routing // Middleware to strip base path prefix from request headers if present
app.use((req, res, next) => {
const prefix = (req.headers['x-forwarded-prefix'] as string) || (req.headers['x-base-path'] as string) || '';
const cleanPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
if (cleanPrefix && req.url.startsWith(cleanPrefix)) {
req.url = req.url.substring(cleanPrefix.length);
if (!req.url.startsWith('/')) {
req.url = '/' + req.url;
}
}
next();
});
// Serve static files from the 'dist' directory, but bypass index.html requests
// so they can be handled dynamically by our fallback route.
app.use((req, res, next) => {
if (req.url === '/index.html') {
return next();
}
next();
});
app.use(express.static(path.join(__dirname, 'dist'), { index: false }));
// Fallback to index.html for Single Page Application routing, injecting base path from headers
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html')); const indexPath = path.join(__dirname, 'dist', 'index.html');
fs.readFile(indexPath, 'utf8', (err, html) => {
if (err) {
return res.status(500).send('Error reading index.html');
}
const prefix = (req.headers['x-forwarded-prefix'] as string) || (req.headers['x-base-path'] as string) || '';
const cleanPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
// Dynamically prefix asset URLs in index.html with the base path
let modifiedHtml = html;
if (cleanPrefix) {
modifiedHtml = html
.replace(/(href|src)="\/assets\//g, `$1="${cleanPrefix}/assets/`)
.replace(/(href|src)="\/vite.svg"/g, `$1="${cleanPrefix}/vite.svg"`);
}
res.send(modifiedHtml);
});
}); });
app.listen(PORT, () => { app.listen(PORT, () => {

View File

@@ -12,8 +12,15 @@ export default defineConfig(() => {
}, },
}, },
server: { server: {
configureServer(server) {
server.middlewares.use((req, res, next) => {
console.log(`[Vite Request] ${req.method} ${req.url}`);
console.log('Headers:', JSON.stringify(req.headers, null, 2));
next();
});
},
// HMR is disabled in AI Studio via DISABLE_HMR env var. // HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits. // Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true', hmr: process.env.DISABLE_HMR !== 'true',
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits. // Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
watch: process.env.DISABLE_HMR === 'true' ? null : {}, watch: process.env.DISABLE_HMR === 'true' ? null : {},