Friendly URLs, URL Rewrite & Reverse Proxy
This document explains how to integrate Mindtraining Platform with friendly URLs and configure URL rewriting or reverse proxy so that direct navigation, bookmarks, and refresh work correctly.
Table of Contents
The Problem
Mindtraining Platform use client-side routing: the browser loads a single index.html (or equivalent) and JavaScript updates the view based on the URL path. When a user:
Navigates directly to
https://yoursite.com/games/crossword/archiveBookmarks a deep link
Refreshes the page on a non-root route
…the browser sends a request to the server for that exact path. Without configuration, the server looks for a file at /games/crossword/archive and returns 404, because that path exists only in the SPA router, not on disk.
The Solution
All requests for SPA routes must be served the same entry file (index.html or script.js). The SPA then reads the URL and renders the correct view. This is achieved by:
URL rewrite — internally map all SPA paths to the entry file
Reverse proxy — forward requests to a backend that serves the SPA
Fallback / catch-all — treat any non-asset path as an SPA route
Apache
Using .htaccess (mod_rewrite)
Place this in the document root or in the SPA subdirectory (e.g. /games/.htaccess):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /games/
# Don't rewrite files or directories that exist
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Rewrite everything else to index.html
RewriteRule ^ index.html [L]
</IfModule>Notes:
Replace
/games/with your SPA base path. Use/if the SPA is at the root.Ensure
mod_rewriteis enabled:a2enmod rewrite(Debian/Ubuntu).AllowOverride Allmust be set for the directory so.htaccessis honoured.
Using VirtualHost (no .htaccess)
<VirtualHost *:80>
ServerName yoursite.com
DocumentRoot /var/www/mindtraining
<Directory /var/www/mindtraining>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
RewriteEngine On
RewriteBase /games/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
</Directory>
</VirtualHost>SPA in a subdirectory (e.g. /games)
If the SPA lives under /games and your entry is index.html in that folder:
<Directory /var/www/html/games>
RewriteEngine On
RewriteBase /games/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /games/index.html [L]
</Directory>Nginx
Basic SPA fallback
server {
listen 80;
server_name yoursite.com;
root /var/www/mindtraining;
location / {
try_files $uri $uri/ /index.html;
}
}SPA in a subdirectory (e.g. /games)
server {
listen 80;
server_name yoursite.com;
root /var/www/html;
location /games {
alias /var/www/html/games;
try_files $uri $uri/ /games/index.html;
}
}With reverse proxy to a static/CDN origin
If the SPA is served from a CDN or another origin:
server {
listen 80;
server_name yoursite.com;
location / {
proxy_pass <https://cdn.example.com/mindtraining/>;
proxy_http_version 1.1;
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;
# SPA fallback: if upstream returns 404, serve index
proxy_intercept_errors on;
error_page 404 = /index.html;
}
}More robust subpath + proxy
server {
listen 80;
server_name yoursite.com;
location /games/ {
proxy_pass <https://cdn.example.com/mindtraining/>;
proxy_http_version 1.1;
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;
# Don't rewrite static assets
proxy_intercept_errors on;
proxy_next_upstream error timeout http_404;
error_page 404 = @spa_fallback;
}
location @spa_fallback {
rewrite ^ /games/index.html break;
proxy_pass <https://cdn.example.com/mindtraining/>;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}Alternatives (No Apache/Nginx)
If you cannot use Apache or Nginx (e.g. serverless, PaaS, or a simple Node server), use one of these approaches.
1. Node.js (Express)
const express = require('express')
const path = require('path')
const app = express()
const PORT = process.env.PORT || 3000
// Serve static files (JS, CSS, images)
app.use(express.static(path.join(__dirname, 'dist')))
// SPA fallback: all other routes serve index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'))
})
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})SPA in subdirectory (e.g. /games):
const express = require('express')
const path = require('path')
const app = express()
const BASE = '/games'
app.use(BASE, express.static(path.join(__dirname, 'dist')))
app.get(`${BASE}/*`, (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'))
})
app.listen(process.env.PORT || 3000)2. Vercel (vercel.json)
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}For a subpath:
{
"rewrites": [
{ "source": "/games/:path*", "destination": "/games/index.html" }
]
}3. Netlify (_redirects or netlify.toml)
_redirects (in public/ or project root):
/* /index.html 200netlify.toml:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200For subpath /games:
/games/* /games/index.html 2004. AWS S3 + CloudFront (Static hosting)
S3 does not support rewrites. Use CloudFront Functions or Lambda@Edge:
CloudFront Function (viewer request or origin request):
function handler(event) {
var request = event.request
var uri = request.uri
// Don't rewrite if it looks like a file
if (uri.includes('.') && !uri.endsWith('.html')) {
return request
}
// SPA fallback
if (!uri.endsWith('/') && !uri.includes('.')) {
request.uri = '/index.html'
} else if (uri.endsWith('/')) {
request.uri = uri + 'index.html'
}
return request
}5. Firebase Hosting (firebase.json)
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}6. GitHub Pages
GitHub Pages does not support server-side rewrites. Options:
Use a 404.html trick: create a custom 404 page that loads your SPA and redirects client-side. Not ideal for SEO or direct links.
Use a client-side hash router (
#/games/crossword) instead of path-based routing — no server config needed, but URLs are less friendly.Host the SPA elsewhere (Vercel, Netlify, etc.) and point your domain there.
Mindtraining Platform specifics
This platform uses TanStack Router with a dynamic basepath from the API. Typical routes:
| Path pattern | Example |
|---|---|
| Home | / or /{basepath}/ |
| Today's game | /{basepath}/crossword/ |
| Archive | /{basepath}/crossword/archive |
| Statistics | /{basepath}/crossword/statistics |
| Date-specific | /{basepath}/crossword/2024-03-18 |
The basepath is configured per site/domain. Ensure your rewrite rules cover the full basepath. For example, if basepath is /games:
Apache:
RewriteBase /games/and serveindex.htmlfor all non-file requests under/gamesNginx:
location /games { try_files $uri $uri/ /games/index.html; }Express: Mount static + fallback under
/games