Objetivos de aprendizaje
Después de leer este artículo podrás:
- Entender por qué recargar rompe rutas que funcionaban al navegar
- Aplicar el fallback a index.html en nginx, Express, Apache o Netlify
- Evitar que se oculten las páginas 404 reales y los assets que faltan
Haces el deploy de tu app de React o Vue. La página de inicio carga, los enlaces funcionan, y entonces recargas en /dashboard o pegas un enlace profundo y el servidor te devuelve un 404. En local nunca pasó.
La app está bien. El servidor simplemente no sabe que /dashboard es una pantalla de tu JavaScript y no un archivo. A continuación, el mecanismo y la solución para los servidores más comunes.
¿Por qué recargar devuelve 404 si la navegación funciona?#
Una aplicación de una sola página (SPA) entrega un único archivo HTML, normalmente index.html, más los bundles de JavaScript. Cuando haces clic en un enlace, el router (React Router, Vue Router) llama a la History API y cambia la vista. La URL cambia, pero el navegador no envía ninguna petición.
Recargar es distinto. El navegador pide al servidor exactamente esa ruta:
GET /dashboard HTTP/1.1
Host: example.comUn hosting estático asocia URLs con archivos. En la salida de tu build no existe ningún archivo ni carpeta dashboard, así que responde:
404 Not FoundEl servidor de desarrollo lo oculta porque ya hace fallback a index.html para las rutas desconocidas. La documentación de Vite también aclara que vite preview solo sirve para previsualizar el build en local y no está pensado como servidor de producción, así que una vista previa que funciona demuestra poco sobre tu hosting.
¿Cómo lo arreglo en el servidor?#
Configura el servidor para que devuelva index.html en cualquier ruta que no sea un archivo existente. Así el navegador carga la app, el router lee la URL y renderiza la pantalla correcta. La documentación de Vue Router describe esto como la configuración de servidor necesaria para el modo history.
La regla tiene dos partes, en este orden:
- Si la petición coincide con un archivo real (un script, una hoja de estilos, una imagen), sírvelo.
- Si no, sirve
index.htmly deja que decida el router del cliente.
Esta es esa regla para los servidores más comunes.
nginx#
Usa try_files en el bloque location /:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}nginx prueba el archivo y luego el directorio. Si ninguno existe, se aplica el último parámetro, que aquí es una redirección interna a /index.html. Recarga nginx después de editar.
Express u otro servidor Node.js#
Sirve la carpeta del build y luego añade una ruta comodín que envíe index.html. Este ejemplo usa Express 5, que exige un comodín con nombre:
import express from "express";
import path from "node:path";
import { fileURLToPath } from "node:url";
const dist = path.join(path.dirname(fileURLToPath(import.meta.url)), "dist");
const app = express();
app.use(express.static(dist));
app.get("/{*splat}", (req, res) => {
res.sendFile(path.join(dist, "index.html"));
});
app.listen(process.env.PORT || 3000);En Express 4 el comodín es app.get("*", ...). En Express 5 el comodín debe tener nombre, y /{*splat} también coincide con la ruta raíz, como explica la guía de migración de Express. Registra el comodín después de express.static y de las rutas de tu API, o se las tragará.
Apache#
Pon esto en un archivo .htaccess junto a index.html, con mod_rewrite activado:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.html [L]
</IfModule>Las dos condiciones saltan los archivos y directorios existentes, así que solo las rutas desconocidas caen en el fallback.
Netlify y Firebase Hosting#
En Netlify, añade un archivo _redirects a la carpeta publicada (en una app de Vite, ponlo en public/ para que se copie a dist/):
/* /index.html 200El 200 lo convierte en un rewrite, no en una redirección, así que la URL no cambia. Por defecto Netlify no oculta las URLs que coinciden con un archivo real, así que los assets se siguen sirviendo con normalidad.
En Firebase Hosting, añade un rewrite en firebase.json (los archivos que existen se siguen sirviendo primero):
{
"hosting": {
"public": "dist",
"rewrites": [
{ "source": "**", "destination": "/index.html" }
]
}
}Otras plataformas tienen una opción equivalente, a menudo llamada "SPA mode", "rewrites" o "fallback". Busca esas palabras en la documentación de tu hosting.
¿Por qué ahora veo una página en blanco o un error de tipo MIME?#
Un fallback demasiado amplio provoca el problema contrario. Si el navegador pide /assets/app.abc123.js y el archivo no existe, el servidor devuelve index.html con estado 200, y el navegador recibe HTML donde esperaba JavaScript. Con scripts de módulo, que es lo que genera Vite, Chrome muestra el primer error de abajo; con scripts clásicos, el segundo:
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html".
Uncaught SyntaxError: Unexpected token '<'Causas comunes:
- Ruta base incorrecta. La app se sirve desde una subruta (
/app/) pero se compiló para/. Ajustabaseen Vite para que coincida. - URLs relativas de assets en rutas profundas. En
/users/42, unsrc="assets/app.js"se resuelve como/users/assets/app.js. Usa rutas absolutas que empiecen por/. - Deploy desactualizado. El
index.htmlen caché apunta a archivos con hash del build anterior que ya no existen. Sube los archivos nuevos junto con el nuevoindex.htmly evita una caché larga enindex.html.
Abre la pestaña Network y revisa la petición que falla. Si un archivo .js vuelve como text/html, el fallback está atrapando un asset que falta.
¿Cómo mantengo una página 404 real?#
Con un fallback que lo atrapa todo, el servidor nunca devuelve 404 para las páginas. La documentación de Vue Router señala esta consecuencia y sugiere una ruta comodín dentro de la app:
// Vue Router
{ path: "/:pathMatch(.*)", component: NotFound }// React Router
<Route path="*" element={<NotFound />} />El usuario ve una pantalla de no encontrado, pero el servidor sigue respondiendo 200, y los buscadores pueden tratar esas páginas como soft 404. Si eso te importa, añade una metaetiqueta noindex en la pantalla de no encontrado, o pasa a renderizado en el servidor o prerenderizado para que el servidor pueda devolver un 404 real. Consulta SSR vs SSG vs estático para comparar las opciones.
¿Y HashRouter?#
HashRouter (React) y createWebHashHistory (Vue) ponen la ruta después del #, como en /#/dashboard. Los navegadores nunca envían el fragmento al servidor, así que cada recarga pide / y no hace falta configurar nada en el servidor. Es una opción válida en hostings donde no puedes añadir rewrites. La desventaja es que las URLs se ven peor y el servidor no puede ver ni indexar las rutas individuales.
Cómo hacerlo en Square Cloud#
Square Cloud es hosting self-service, así que tú decides cómo se sirve el build. En el runtime de sitios estáticos, el fallback es un solo paso: renombra el index.html del build a vite.html. El runtime responde con vite.html a toda ruta que no sea un archivo, así que cualquier enlace profundo carga la app. Sin vite.html, las rutas desconocidas reciben tu 404.html.
Si prefieres controlar el enrutamiento tú mismo, usa el runtime de Node.js con un servidor Express como el de arriba (con express en tu package.json). Las dos opciones están en el tutorial de React de la documentación. Una aplicación web necesita al menos 512 MB de RAM del pool compartido de tu plan y recibe un subdominio HTTPS *.squareweb.app. Puedes hacer el deploy subiendo un ZIP en el panel, con la CLI o desde GitHub.
Después del deploy, abre un enlace profundo directamente y recárgalo para confirmar la solución. El camino completo del build a la URL publicada está en cómo hospedar una app de React, y los recursos para sitios están en la página de Sites.
Hosting de sitios web
Sitios estáticos y SSR con TLS automático y CDN global
Preguntas frecuentes
¿Por qué mi SPA funciona en la página de inicio pero da 404 al recargar?
Al hacer clic en un enlace, la URL cambia en el navegador sin ninguna petición, así que el router se encarga de ella. Al recargar se envía una petición real a esa ruta, y un servidor estático no tiene ningún archivo ahí.
¿HashRouter lo soluciona?
Sí. La ruta va después del # en la URL, que el navegador nunca envía al servidor, así que cada recarga pide el mismo index.html. El precio son URLs más feas, y el servidor no puede ver la ruta.
¿El fallback debe devolver estado 200 o 404?
Para una SPA renderizada en el cliente, 200 con index.html, porque es la app la que decide qué mostrar. Muestra una pantalla de no encontrado dentro de la app para las rutas desconocidas.
¿Por qué veo una página en blanco o un error de tipo MIME en lugar de un 404?
Un fallback que lo atrapa todo también responde con index.html a las peticiones de archivos JavaScript o CSS que faltan. El navegador recibe HTML donde esperaba un script. Revisa las rutas de los assets y la opción base.
Más en Sitios web
Ver los 20 artículos- Qué es un sitio estático y cómo hospedarloUn sitio estático es un conjunto de archivos HTML, CSS y JavaScript ya listos, entregados igual a todos los visitantes. Mira cómo funciona y dónde hospedarlo.
- ¿Cuánto cuesta hospedar una página web?Hospedar una página web cuesta desde cero hasta unas decenas de dólares al mes. Mira qué entra en la cuenta: hosting, dominio, tráfico y extras, con límites reales.
- ¿Cuánto cuesta hospedar un sitio estático?Hospedar un sitio estático cuesta entre cero y unas decenas de dólares al mes en la mayoría de los casos. Mira cómo el tráfico define el precio, paso a paso.
- ¿Cuánto cuesta hospedar una aplicación Next.js?Hospedar una app Next.js cuesta desde cero, con export estático, hasta decenas de dólares al mes con servidor. Modelos de cobro, cuentas de tráfico y costos ocultos.