Tema
Despliegue
Tu app funciona en localhost. Ahora la publicarás en internet para que tu cliente la use. Aprenderás a generar el build de producción, manejar variables de entorno y desplegar en Netlify, Vercel y en un hosting compartido con Apache (cPanel), incluyendo la configuración necesaria para que las rutas de React Router funcionen.
Concepto
Una app de React con Vite es estática: después del build son sólo archivos HTML, CSS, JS e imágenes. No necesita Node en el servidor; cualquier hosting que sirva archivos funciona.
txt
npm run build
│
▼
dist/
├── index.html
├── favicon.svg
└── assets/
├── index-B3k9xL2a.js ← tu código minificado (el hash cambia en cada build)
└── index-Dq8wPz1c.cssEl nombre con hash permite al navegador guardar los archivos en caché para siempre: cuando cambias el código, cambia el nombre y se descarga el nuevo.
El problema de las rutas en una SPA
Si un usuario entra directo a https://misitio.com/productos/15, el servidor busca el archivo /productos/15 que no existe y responde 404. La solución: configurar el servidor para que cualquier ruta devuelva index.html, y React Router se encargue del resto. Cada plataforma tiene su forma de hacerlo.
Preparar el build
bash
npm run build # genera dist/
npm run preview # sirve dist/ en http://localhost:4173 para probarloRevisa antes de publicar:
- Que no haya errores ni advertencias en consola.
- Que las rutas funcionen al recargar en
preview. - El título, favicon y meta descripción en
index.html.
Variables de entorno
Vite carga archivos .env según el modo. Sólo las variables con prefijo VITE_ llegan al código del navegador:
env
# .env.development (npm run dev)
VITE_API_URL=http://localhost:3000/api
# .env.production (npm run build)
VITE_API_URL=https://api.misitio.com/apijs
const API = import.meta.env.VITE_API_URL
console.log(import.meta.env.MODE) // 'development' | 'production'
console.log(import.meta.env.PROD) // true en el buildImportante
Las variables VITE_ se incrustan en el JavaScript público durante el build. Cualquiera puede verlas abriendo DevTools. Nunca pongas secretos (claves privadas, contraseñas). Además, como se incrustan al compilar, si cambias una variable debes volver a hacer el build.
Subcarpeta (base)
Si la app no vivirá en la raíz del dominio (por ejemplo https://misitio.com/app/), indícalo:
js
// vite.config.js
export default defineConfig({
plugins: [react()],
base: '/app/'
})Y en React Router: createBrowserRouter(rutas, { basename: '/app' }).
Opción 1: Netlify
Con Git (recomendado)
- Sube tu proyecto a GitHub.
- En netlify.com → Add new site → Import an existing project → elige el repositorio.
- Configuración (Netlify detecta Vite automáticamente):
- Build command:
npm run build - Publish directory:
dist
- Build command:
- En Site configuration → Environment variables agrega
VITE_API_URL. - Deploy. Cada
git pushpublica una nueva versión, y cada Pull Request genera una vista previa con su propia URL.
Para que las rutas de la SPA funcionen, crea public/_redirects (Vite lo copia a dist):
txt
/* /index.html 200O, alternativamente, un netlify.toml en la raíz:
ini
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200Con arrastrar y soltar
Para una prueba rápida: ejecuta npm run build y arrastra la carpeta dist a app.netlify.com/drop.
Opción 2: Vercel
- En vercel.com → Add New → Project → importa el repositorio.
- Vercel detecta Vite: build
npm run build, salidadist. - Agrega las variables de entorno en Settings → Environment Variables.
- Deploy.
Para las rutas de la SPA, crea vercel.json en la raíz:
json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}Con la CLI:
bash
npm i -g vercel
vercel # despliegue de vista previa
vercel --prod # producciónOpción 3: hosting compartido (Apache / cPanel)
Muchos clientes ya pagan un hosting compartido (Hostinger, HostGator, Neubox...). Una SPA funciona perfecto ahí.
- Ejecuta
npm run build. - Crea
public/.htaccessantes del build (Vite lo copiará adist) con este contenido:
apache
# public/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Si el archivo o carpeta existe, sírvelo tal cual
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Cualquier otra ruta → index.html (React Router decide qué mostrar)
RewriteRule ^ index.html [L]
</IfModule>
# Caché larga para archivos con hash, sin caché para index.html
<IfModule mod_headers.c>
<FilesMatch "\.(js|css|woff2|png|jpg|jpeg|webp|svg)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "index\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
# Compresión
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript image/svg+xml application/json
</IfModule>- Sube el contenido de
dist/(no la carpeta en sí) apublic_html/con el Administrador de archivos de cPanel o por FTP (FileZilla). - Activa el SSL gratuito (AutoSSL / Let's Encrypt) desde cPanel y fuerza HTTPS.
Archivos ocultos
Los archivos que empiezan con punto (.htaccess) están ocultos por defecto en muchos clientes FTP y en el administrador de archivos. Activa "mostrar archivos ocultos" para verificar que se subió.
Si la app va en una subcarpeta (public_html/tienda/), usa base: '/tienda/' en Vite, basename: '/tienda' en el router, y en el .htaccess cambia RewriteBase /tienda/ y la última regla a RewriteRule ^ /tienda/index.html [L].
Automatizar con GitHub Actions + FTP
yaml
# .github/workflows/deploy.yml
name: Deploy a hosting
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
- uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USER }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./dist/
server-dir: /public_html/Los valores secretos se guardan en GitHub → Settings → Secrets and variables → Actions.
Opción 4: VPS con Nginx
Si ya tienes un VPS (por ejemplo, donde corre tu API de Node):
nginx
server {
listen 80;
server_name misitio.com www.misitio.com;
root /var/www/misitio/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html; # fallback de la SPA
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_types text/css application/javascript image/svg+xml application/json;
}SEO y metadatos
Una SPA entrega un HTML casi vacío y el contenido aparece con JavaScript. Google puede indexarlo, pero otras redes (al compartir en WhatsApp o Facebook) sólo leen el HTML inicial.
- Pon título, descripción e imagen Open Graph en
index.html. - React 19 permite
titleymetadentro de los componentes para actualizar el título por página:
jsx
function PaginaProducto({ producto }) {
return (
<>
<title>{`${producto.titulo} | MiTienda`}</title>
<meta name="description" content={producto.descripcion} />
<h1>{producto.titulo}</h1>
</>
)
}- Si el SEO es crítico (tienda pública, blog, landing para posicionar), usa Next.js, que genera el HTML en el servidor.
Checklist antes de entregar
- [ ]
npm run buildsin errores y probado connpm run preview. - [ ] Recargar en cualquier ruta funciona (fallback a
index.html). - [ ] Variables de entorno de producción configuradas.
- [ ] HTTPS activo y dominio conectado.
- [ ] Favicon, título y meta descripción correctos.
- [ ] Lighthouse con buena puntuación en móvil.
Ejercicios
- Despliega en Netlify la app de la lección de React Router y verifica que recargar en
/productos/3funcione. - Prepara el
.htaccessy la configuración de Vite y React Router para publicar una app enhttps://cliente.com/catalogo/en un hosting compartido.
Ver solución
Crea
public/_redirectscon/* /index.html 200, sube el repositorio a GitHub, impórtalo en Netlify (buildnpm run build, publishdist) y prueba la URL/productos/3recargando la página.
js
// vite.config.js
export default defineConfig({
plugins: [react()],
base: '/catalogo/'
})jsx
// src/router.jsx
export const router = createBrowserRouter(rutas, { basename: '/catalogo' })apache
# public/.htaccess (se sube a public_html/catalogo/)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /catalogo/
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^ /catalogo/index.html [L]
</IfModule>Después: npm run build y sube el contenido de dist/ a public_html/catalogo/.
Resumen
npm run buildgenera archivos estáticos endist/; pruébalos connpm run preview.- Sólo las variables
VITE_llegan al navegador y son públicas. - Configura el fallback a
index.html:_redirects(Netlify),vercel.json(Vercel),.htaccess(Apache),try_files(Nginx). - Netlify y Vercel despliegan con cada push; en hosting compartido subes
dist/por FTP o con GitHub Actions.