Skip to content

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/archive

  • Bookmarks 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:

  1. URL rewrite — internally map all SPA paths to the entry file

  2. Reverse proxy — forward requests to a backend that serves the SPA

  3. 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):

apache
<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_rewrite is enabled: a2enmod rewrite (Debian/Ubuntu).

  • AllowOverride All must be set for the directory so .htaccess is honoured.

Using VirtualHost (no .htaccess)

apache
<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:

apache
<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

nginx
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)

nginx
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:

nginx
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

nginx
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)

js
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):

js
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)

json
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

For a subpath:

json
{
  "rewrites": [
    { "source": "/games/:path*", "destination": "/games/index.html" }
  ]
}

3. Netlify (_redirects or netlify.toml)

_redirects (in public/ or project root):

txt
/*    /index.html   200

netlify.toml:

toml
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

For subpath /games:

txt
/games/*    /games/index.html    200

4. AWS S3 + CloudFront (Static hosting)

S3 does not support rewrites. Use CloudFront Functions or Lambda@Edge:

CloudFront Function (viewer request or origin request):

js
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)

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 patternExample
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 serve index.html for all non-file requests under /games

  • Nginx: location /games { try_files $uri $uri/ /games/index.html; }

  • Express: Mount static + fallback under /games


Client integration documentation maintained in-repo.