Skip to content

URLs amigables, reescritura de URL y reverse proxy

Este documento explica cómo integrar Mindtraining Platform con URLs amigables y cómo configurar reescritura de URL o reverse proxy para que la navegación directa, los marcadores y el refresh funcionen correctamente.


Tabla de contenidos


El problema

Mindtraining Platform usa routing del lado cliente: el navegador carga un único index.html (o equivalente) y JavaScript actualiza la vista según el path de la URL. Cuando un usuario:

  • Navega directamente a https://yoursite.com/games/crossword/archive

  • Guarda en favoritos un deep link

  • Refresca la página en una ruta distinta de la raíz

…el navegador envía una petición al servidor para ese path exacto. Sin configuración adicional, el servidor busca un archivo en /games/crossword/archive y devuelve 404, porque ese path solo existe en el router SPA, no en disco.


La solución

Todas las peticiones para rutas SPA deben servir el mismo archivo de entrada (index.html o script.js). Después la SPA lee la URL y renderiza la vista correcta. Esto se consigue mediante:

  1. URL rewrite: mapear internamente todas las rutas SPA al archivo de entrada

  2. Reverse proxy: reenviar las peticiones a un backend que sirva la SPA

  3. Fallback / catch-all: tratar cualquier ruta que no sea asset como una ruta SPA


Apache

Usando .htaccess (mod_rewrite)

Coloca esto en el document root o en el subdirectorio de la SPA (por ejemplo, /games/.htaccess):

apache
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /games/

  # No reescribir archivos o directorios que existan
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d

  # Reescribir todo lo demás a index.html
  RewriteRule ^ index.html [L]
</IfModule>

Notas:

  • Sustituye /games/ por tu base path de la SPA. Usa / si la SPA está en la raíz.

  • Asegúrate de que mod_rewrite está habilitado: a2enmod rewrite (Debian/Ubuntu).

  • Debe estar configurado AllowOverride All para que .htaccess se respete.

Usando VirtualHost (sin .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 en un subdirectorio (por ejemplo /games)

Si la SPA vive bajo /games y tu entrypoint es index.html dentro de esa carpeta:

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

Fallback básico para SPA

nginx
server {
  listen 80;
  server_name yoursite.com;
  root /var/www/mindtraining;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

SPA en un subdirectorio (por ejemplo /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;
  }
}

Con reverse proxy hacia un origen estático/CDN

Si la SPA se sirve desde un CDN u otro origen:

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;

    # Fallback SPA: si el upstream responde 404, servir index
    proxy_intercept_errors on;
    error_page 404 = /index.html;
  }
}

Subpath + proxy más robusto

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;

    # No reescribir assets estáticos
    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;
  }
}

Alternativas (sin Apache/Nginx)

Si no puedes usar Apache o Nginx (por ejemplo, serverless, PaaS o un servidor Node sencillo), usa uno de estos enfoques.

1. Node.js (Express)

js
const express = require('express')
const path = require('path')

const app = express()
const PORT = process.env.PORT || 3000

// Servir archivos estáticos (JS, CSS, imágenes)
app.use(express.static(path.join(__dirname, 'dist')))

// Fallback SPA: todas las demás rutas sirven 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 en subdirectorio (por ejemplo /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" }
  ]
}

Para un subpath:

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

3. Netlify (_redirects o netlify.toml)

_redirects (en public/ o en la raíz del proyecto):

txt
/*    /index.html   200

netlify.toml:

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

Para el subpath /games:

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

4. AWS S3 + CloudFront (hosting estático)

S3 no soporta reescrituras. Usa CloudFront Functions o Lambda@Edge:

CloudFront Function (viewer request u origin request):

js
function handler(event) {
  var request = event.request
  var uri = request.uri

  // No reescribir si parece un archivo
  if (uri.includes('.') && !uri.endsWith('.html')) {
    return request
  }

  // Fallback SPA
  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 no soporta reescrituras del lado servidor. Opciones:

  • Usa el truco de 404.html: crea una página 404 personalizada que cargue la SPA y redirija del lado cliente. No es ideal para SEO ni deep links.

  • Usa un hash router del lado cliente (#/games/crossword) en vez de routing basado en path. No requiere configuración de servidor, pero las URLs son menos amigables.

  • Aloja la SPA en otro sitio (Vercel, Netlify, etc.) y apunta tu dominio allí.


Particularidades de Mindtraining Platform

Esta plataforma usa TanStack Router con un basepath dinámico obtenido desde la API. Rutas típicas:

Patrón de pathEjemplo
Home/ o /{basepath}/
Juego de hoy/{basepath}/crossword/
Archivo/{basepath}/crossword/archive
Estadísticas/{basepath}/crossword/statistics
Fecha concreta/{basepath}/crossword/2024-03-18

El basepath se configura por sitio/dominio. Asegúrate de que tus reglas cubren el basepath completo. Por ejemplo, si basepath es /games:

  • Apache: RewriteBase /games/ y servir index.html para todas las peticiones que no sean archivos bajo /games

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

  • Express: montar estáticos + fallback bajo /games


Client integration documentation maintained in-repo.