Static Exports
Static Exports te permite exportar tu aplicación NextJS como archivos HTML, CSS y JavaScript estáticos. No necesitas servidor Node.js.
¿Qué es Static Export?
Convierte tu app NextJS en archivos estáticos:
out/
├── index.html
├── productos.html
├── contacto.html
├── _next/
│ ├── static/
│ │ ├── chunks/
│ │ └── css/
Estos archivos pueden hospedarse en:
- GitHub Pages
- Netlify
- Cloudflare Pages
- S3 + CloudFront
- Cualquier CDN o servidor web
¿Cuándo usar Static Export?
✅ Ideal para:
- Blogs - Contenido que no cambia frecuentemente
- Portfolios - Sitios personales
- Landing pages - Marketing
- Documentación - Como esta guía
- Sites informativos - Sobre nosotros, términos
❌ NO usar para:
- Dashboards - Datos en tiempo real
- Apps con auth - Login/logout
- Ecommerce dinámico - Carrito, checkout
- APIs - Necesitas servidor
- Datos personalizados - Por usuario
Limitaciones importantes
Static Export NO soporta:
- ❌ Server Components con datos dinámicos
- ❌ Server Actions
- ❌ API Routes
- ❌ Incremental Static Regeneration (ISR)
- ❌ Image Optimization (a menos que uses loader custom)
- ❌ Internationalization routing
- ❌ Dynamic Routes sin generateStaticParams
Básicamente, no puedes usar nada que requiera un servidor Node.js.
Configuración
1. Habilitar static export
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
2. Build
npm run build
NextJS genera la carpeta out/ con todos los archivos estáticos.
3. Preview local
# Instala servidor estático
npm install -g serve
# Sirve la carpeta out
serve out
# Abre http://localhost:3000
Rutas dinámicas
Para rutas dinámicas, debes especificar todos los paths:
// app/productos/[id]/page.tsx
// Genera estas páginas en build time
export async function generateStaticParams() {
const productos = await fetch('https://api.mitienda.com/productos')
.then(r => r.json())
return productos.map((producto) => ({
id: producto.id.toString(),
}))
}
export default async function ProductoPage({ params }) {
const producto = await fetch(`https://api.mitienda.com/productos/${params.id}`)
.then(r => r.json())
return (
<div>
<h1>{producto.nombre}</h1>
<p>${producto.precio}</p>
</div>
)
}
NextJS genera:
out/
├── productos/
│ ├── 1.html
│ ├── 2.html
│ └── 3.html
Imágenes
El componente Image no funciona con export estático por defecto. Tienes 3 opciones:
Opción 1: Usar img (sin optimización)
// ❌ No funciona en static export
import Image from 'next/image'
<Image src="/logo.png" width={100} height={100} />
// ✅ Funciona
<img src="/logo.png" width={100} height={100} alt="Logo" />
Opción 2: Loader custom
// next.config.js
module.exports = {
output: 'export',
images: {
loader: 'custom',
loaderFile: './image-loader.js',
},
}
// image-loader.js
export default function cloudinaryLoader({ src, width, quality }) {
const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`]
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`
}
// Ahora funciona con CDN externo
import Image from 'next/image'
<Image src="/producto.jpg" width={500} height={300} />
Opción 3: unoptimized
// next.config.js
module.exports = {
output: 'export',
images: {
unoptimized: true,
},
}
Ahora <Image> funciona pero sin optimización (mismo que usar <img>).
Ejemplo: Blog estático
// app/blog/[slug]/page.tsx
import { readdir, readFile } from 'fs/promises'
import matter from 'gray-matter'
import { remark } from 'remark'
import html from 'remark-html'
// Genera paths para todos los posts
export async function generateStaticParams() {
const files = await readdir('./content/blog')
return files.map((filename) => ({
slug: filename.replace('.md', ''),
}))
}
export default async function BlogPost({ params }) {
// Lee archivo markdown
const fileContent = await readFile(
`./content/blog/${params.slug}.md`,
'utf-8'
)
// Parsea frontmatter
const { data, content } = matter(fileContent)
// Convierte markdown a HTML
const processedContent = await remark()
.use(html)
.process(content)
const contentHtml = processedContent.toString()
return (
<article>
<h1>{data.title}</h1>
<time>{data.date}</time>
<div dangerouslySetInnerHTML={{ __html: contentHtml }} />
</article>
)
}
content/
└── blog/
├── primer-post.md
├── segundo-post.md
└── tercer-post.md
---
title: Mi primer post
date: 2024-01-15
---
# Hola mundo
Este es mi primer post en el blog.
Trailing slashes
Por defecto, NextJS genera /about.html. Algunos servidores esperan /about/index.html.
// next.config.js
module.exports = {
output: 'export',
trailingSlash: true,
}
Genera:
out/
├── about/
│ └── index.html
└── contact/
└── index.html
Links y navegación
Links funcionan normalmente:
import Link from 'next/link'
<Link href="/productos">Productos</Link>
<Link href="/contacto">Contacto</Link>
NextJS usa client-side navigation (SPA) incluso en export estático.
Deployment
GitHub Pages
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./out
Base path para GitHub Pages:
// next.config.js
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
output: 'export',
basePath: isProd ? '/nombre-repositorio' : '',
assetPrefix: isProd ? '/nombre-repositorio/' : '',
}
Netlify
- Conecta repositorio
- Build settings:
- Build command:
npm run build - Publish directory:
out
- Build command:
- Deploy
netlify.toml:
[build]
command = "npm run build"
publish = "out"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Cloudflare Pages
- Conecta repositorio
- Build settings:
- Build command:
npm run build - Build output:
out
- Build command:
- Deploy
AWS S3 + CloudFront
# Build
npm run build
# Sync a S3
aws s3 sync out/ s3://mi-bucket --delete
# Invalidar cache de CloudFront
aws cloudfront create-invalidation --distribution-id ID --paths "/*"
Servidor tradicional (Apache/Nginx)
Nginx:
server {
listen 80;
server_name mitienda.com;
root /var/www/mitienda/out;
location / {
try_files $uri $uri.html $uri/ /index.html;
}
# Cache static assets
location /_next/static {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
Apache (.htaccess):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
Client-side data fetching
Ya que no hay servidor, obtén datos en el cliente:
'use client'
import { useState, useEffect } from 'react'
export default function ProductosPage() {
const [productos, setProductos] = useState([])
useEffect(() => {
fetch('https://api.mitienda.com/productos')
.then(r => r.json())
.then(data => setProductos(data))
}, [])
if (!productos.length) {
return <div>Cargando...</div>
}
return (
<div>
{productos.map(p => (
<div key={p.id}>{p.nombre}</div>
))}
</div>
)
}
Metadata en static export
Metadata funciona normalmente:
// app/page.tsx
export const metadata = {
title: 'Mi Tienda',
description: 'Los mejores productos',
}
Se genera en el HTML:
<!DOCTYPE html>
<html>
<head>
<title>Mi Tienda</title>
<meta name="description" content="Los mejores productos">
</head>
404 personalizado
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404 - Página no encontrada</h1>
<a href="/">Volver al inicio</a>
</div>
)
}
Genera out/404.html.
Configurar en servidor:
Netlify: Automático
Cloudflare Pages: Automático
S3: Configura error document en bucket properties
Sitemap y robots
// app/sitemap.ts
export default function sitemap() {
return [
{
url: 'https://mitienda.com',
lastModified: new Date(),
},
{
url: 'https://mitienda.com/productos',
lastModified: new Date(),
},
]
}
Genera out/sitemap.xml
// app/robots.ts
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: 'https://mitienda.com/sitemap.xml',
}
}
Genera out/robots.txt
Ventajas de Static Export
Performance
✅ Súper rápido - Solo HTML, CSS, JS ✅ CDN-friendly - Se cachea perfectamente ✅ Sin latencia de servidor - No hay Node.js
Costo
✅ Gratis o muy barato
- GitHub Pages: Gratis
- Netlify: Gratis (100 GB/mes)
- Cloudflare Pages: Gratis (ilimitado)
Simplicidad
✅ Sin servidor que mantener ✅ Sin base de datos ✅ Escala infinito (es solo archivos)
Limitaciones
No puedes usar:
❌ Server Components dinámicos
// ❌ No funciona
export default async function Page() {
const data = await fetch('...', { cache: 'no-store' })
return <div>{data}</div>
}
❌ Server Actions
// ❌ No funciona
'use server'
export async function createTodo() {
await db.todo.create(...)
}
❌ API Routes
// ❌ No funciona
// app/api/productos/route.ts
export async function GET() {
return Response.json({ productos: [] })
}
❌ ISR (Incremental Static Regeneration)
// ❌ No funciona
export const revalidate = 60
❌ Image Optimization
Sin loader custom, optimización de imágenes no funciona.
❌ Redirects/Rewrites dinámicos
Solo funcionan redirects estáticos definidos en next.config.js.
Migrar a static export
Si tu app ya existe, estos pasos:
1. Cambiar configuración
// next.config.js
module.exports = {
output: 'export',
}
2. Eliminar funciones de servidor
// ❌ Eliminar
export const dynamic = 'force-dynamic'
export const revalidate = 60
// ❌ Cambiar fetch dinámico a estático
const data = await fetch('...', { cache: 'no-store' })
// ✅ Por
const data = await fetch('...', { cache: 'force-cache' })
3. Convertir Server Actions a client fetch
// ❌ Antes (Server Action)
'use server'
export async function createProduct(data) {
await db.product.create({ data })
}
// ✅ Después (Client fetch a API externa)
'use client'
export async function createProduct(data) {
await fetch('https://api.externa.com/products', {
method: 'POST',
body: JSON.stringify(data)
})
}
4. Agregar generateStaticParams
// Para rutas dinámicas
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
]
}
5. Build y revisar errores
npm run build
# Revisa errores y corrígelos uno por uno
Mejores prácticas
1. Usa para contenido estático
// ✅ Perfecto para static export
- Blogs
- Portfolios
- Landing pages
- Documentación
// ❌ Mal para static export
- Dashboards
- Apps con login
- Ecommerce checkout
2. Client-side para datos dinámicos
// Datos que cambian: fetch en el cliente
useEffect(() => {
fetch('https://api.com/latest')
.then(r => r.json())
.then(setData)
}, [])
3. Considera híbrido
// Static export para frontend
// API separada para backend
Frontend (static): GitHub Pages
Backend (API): Vercel serverless functions
4. Optimiza imágenes antes de build
# Comprime imágenes antes de agregar a /public
# Usa tinypng.com o imageoptim
Resumen
Static Export:
- Convierte NextJS a archivos HTML estáticos
- No necesita servidor Node.js
- Hospeda en cualquier CDN
Ideal para:
- Blogs, portfolios, landing pages
- Sitios con contenido que no cambia mucho
- Presupuesto limitado (hosting gratis)
No usar para:
- Apps con autenticación
- Dashboards con datos en tiempo real
- Ecommerce dinámico
- Cualquier cosa que necesite servidor
Setup:
// next.config.js
module.exports = {
output: 'export',
}
npm run build
# Archivos en ./out/
Hosting:
- GitHub Pages (gratis)
- Netlify (gratis)
- Cloudflare Pages (gratis)
- Cualquier CDN
¡Con esto completamos la guía de Deployment! 🚀