Skip to content

URLs amigáveis, reescrita de URL e proxy reverso

Este documento explica como integrar o Mindtraining Platform com URLs amigáveis e como configurar reescrita de URL ou proxy reverso para que navegação direta, favoritos e refresh funcionem corretamente.


Índice


O problema

O Mindtraining Platform usa routing client-side: o navegador carrega um único index.html (ou equivalente) e o JavaScript atualiza a visualização com base no caminho da URL. Quando um utilizador:

  • Navega diretamente para https://yoursite.com/games/crossword/archive

  • Guarda nos favoritos um deep link

  • Atualiza a página numa rota diferente da raiz

…o navegador envia um pedido ao servidor para esse caminho exato. Sem configuração, o servidor procura um ficheiro em /games/crossword/archive e devolve 404, porque esse caminho existe apenas no router da SPA, não em disco.


A solução

Todos os pedidos para rotas SPA têm de servir o mesmo ficheiro de entrada (index.html ou script.js). A SPA depois lê a URL e renderiza a vista correta. Isto é conseguido através de:

  1. URL rewrite — mapear internamente todos os caminhos SPA para o ficheiro de entrada

  2. Proxy reverso — encaminhar os pedidos para um backend que serve a SPA

  3. Fallback / catch-all — tratar qualquer caminho não estático como rota SPA


Apache

Usando .htaccess (mod_rewrite)

Coloque isto na raiz do documento ou no subdiretório da SPA (por exemplo, /games/.htaccess):

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

  # Não reescrever ficheiros ou diretórios existentes
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d

  # Reescrever tudo o resto para index.html
  RewriteRule ^ index.html [L]
</IfModule>

Notas:

  • Substitua /games/ pelo base path da sua SPA. Use / se a SPA estiver na raiz.

  • Garanta que mod_rewrite está ativado: a2enmod rewrite (Debian/Ubuntu).

  • AllowOverride All tem de estar definido para que .htaccess seja respeitado.

Usando VirtualHost (sem .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 num subdiretório (por exemplo /games)

Se a SPA vive sob /games e o entrypoint é index.html nessa pasta:

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 num subdiretório (por exemplo /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;
  }
}

Com proxy reverso para uma origem estática/CDN

Se a SPA for servida a partir de um CDN ou outra origem:

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: se o upstream responder 404, servir index
    proxy_intercept_errors on;
    error_page 404 = /index.html;
  }
}

Subpath + proxy mais 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;

    # Não reescrever 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 (sem Apache/Nginx)

Se não puder usar Apache ou Nginx (por exemplo, serverless, PaaS ou um servidor Node simples), use uma destas abordagens.

1. Node.js (Express)

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

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

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

// Fallback SPA: todas as outras rotas servem 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 em subdiretório (por exemplo /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 um subpath:

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

3. Netlify (_redirects ou netlify.toml)

_redirects (em public/ ou na raiz do projeto):

txt
/*    /index.html   200

netlify.toml:

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

Para o subpath /games:

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

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

O S3 não suporta rewrites. Use CloudFront Functions ou Lambda@Edge:

CloudFront Function (viewer request ou origin request):

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

  // Não reescrever se parecer um ficheiro
  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

O GitHub Pages não suporta rewrites do lado do servidor. Opções:

  • Use o truque do 404.html: crie uma página 404 personalizada que carregue a SPA e redirecione do lado do cliente. Não é ideal para SEO nem para deep links.

  • Use um hash router do lado do cliente (#/games/crossword) em vez de routing baseado em path. Não requer configuração de servidor, mas os URLs ficam menos amigáveis.

  • Aloje a SPA noutro local (Vercel, Netlify, etc.) e aponte o domínio para lá.


Especificidades do Mindtraining Platform

Esta plataforma usa TanStack Router com um basepath dinâmico vindo da API. Rotas típicas:

Padrão de pathExemplo
Home/ ou /{basepath}/
Jogo de hoje/{basepath}/crossword/
Arquivo/{basepath}/crossword/archive
Estatísticas/{basepath}/crossword/statistics
Data específica/{basepath}/crossword/2024-03-18

O basepath é configurado por site/domínio. Garanta que as suas regras cobrem o basepath completo. Por exemplo, se basepath for /games:

  • Apache: RewriteBase /games/ e servir index.html para todos os pedidos que não sejam ficheiros dentro de /games

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

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


Client integration documentation maintained in-repo.