Next.js en producción: qué configuración ajustamos antes de lanzar un sitio real (y por qué)

Checklist de producción para Next.js: metadata y sitemap, imágenes con next/image, headers de cache y seguridad, variables de entorno, bundle y monitoreo. Cada fragmento de configuración listo para copiar.

Next.js en producción: qué configuración ajustamos antes de lanzar un sitio real (y por qué)

El sitio anda perfecto en localhost. Lo subís a producción y aparecen los problemas que no viste en dev: Google indexa páginas sin título, las imágenes del hero pesan 1,5 MB, el preview de WhatsApp sale genérico y nadie sabe por qué la home tarda tanto. No es mala suerte: es la diferencia entre una app que compila y un sitio configurado para producción.

La configuración de producción en Next.js es una checklist, y en este artículo la recorremos completa: metadata y sitemap, imágenes, headers, variables de entorno, bundle y monitoreo, con cada fragmento listo para copiar. Son los ajustes que aplicamos en Gambit Lab en cada sitio que sale a producción.

Metadata, sitemap y robots: que Google entienda tu sitio

Un sitio sin metadata es un sitio invisible: Google no sabe qué título mostrar, las redes muestran un preview genérico y el SEO arranca perdiendo. En el App Router de Next.js todo se declara desde el layout raíz, con un solo export const metadata:

js
// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://tusitio.com'),
  title: {
    default: 'Nombre de tu empresa',
    template: '%s | Nombre de tu empresa',
  },
  description: 'Descripción de tu sitio en menos de 160 caracteres.',
  openGraph: {
    type: 'website',
    locale: 'es_AR',
    url: 'https://tusitio.com',
    siteName: 'Nombre de tu empresa',
    images: ['/og-image.webp'],
  },
  robots: { index: true, follow: true },
};

metadataBase es el que casi nadie configura y el que rompe los Open Graph con URLs relativas. El template hace que cada página herede el nombre de tu empresa sin repetirlo a mano.

Después, sitemap y robots: dos archivos en app/:

js
// app/sitemap.ts
import type { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    { url: 'https://tusitio.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
    { url: 'https://tusitio.com/servicios', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
  ];
}
js
// app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/' },
    sitemap: 'https://tusitio.com/sitemap.xml',
  };
}
Sitemap dinámico

Si tu sitio genera páginas (un blog, un catálogo), el sitemap puede construirse con fetch o desde la base de datos dentro de la misma función. Eso evita el sitemap desactualizado que Google termina ignorando.

next/image en producción: priority y sizes para no arruinar el LCP

next/image ya resuelve lo básico: convierte a WebP/AVIF, sirve el tamaño correcto para cada pantalla y difiere la carga fuera de la vista. Pero en producción hay dos props que hacen la diferencia entre un LCP bueno y uno malo: priority y sizes.

El componente <Image> no se optimiza solo si no le decís cuál es la imagen que manda en el ranking de rendimiento:

jsx
import Image from 'next/image';

// El hero es el LCP: priority + sizes correctos
<Image
  src="/hero.jpg"
  alt="Descripción real de la imagen"
  fill
  priority
  sizes="(min-width: 1024px) 50vw, 100vw"
  quality={80}
/>

priority carga la imagen apenas arranca el documento, sin esperar a que entre en viewport (el lazy loading por defecto es justo lo que mata al LCP si se aplica al hero). sizes le dice al navegador qué ancho reservar para elegir el archivo correcto. Sin sizes, una imagen fill con contenedor ancho puede servir el mismo archivo a un celular.

Priority solo para el LCP

Poner priority en todas las imágenes de la home anula el propósito: el navegador descarga todo de una y el LCP mejora el de una imagen a costa del resto. La regla es una sola imagen con priority por vista, la del LCP.

Headers de cache y seguridad: la configuración que nadie ve

Next.js aplica defaults razonables, pero producción exige dos cosas que vienen desactivadas: caché larga para assets estáticos y headers de seguridad. Todo se declara en next.config.ts:

ts
// next.config.ts
const securityHeaders = [
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];

const nextConfig = {
  async headers() {
    return [
      {
        source: '/images/:path*',
        headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
      },
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

export default nextConfig;

El immutable en imágenes y fuentes evita que el navegador las vuelva a pedir en cada visita: el usuario vuelve y la página carga desde caché local. Los headers de seguridad bloquean ataques básicos: clickjacking, MIME sniffing y el robo de referrer en links externos.

CSP: empezá simple

El Content-Security-Policy es el header más potente y el más fácil de romper (bloquea scripts inline, estilos, fuentes). Arrancá con una política básica y revisá la consola del navegador antes de ir a la versión estricta: un CSP que rompe el sitio en producción es peor que no tenerlo.

Variables de entorno, bundle y monitoreo: cerrar el círculo

Tres frentes que no se ven hasta que el sitio está en el aire. Primero, las variables de entorno: en Next.js, solo las que llevan el prefijo NEXT_PUBLIC_ quedan expuestas al navegador. Las que no lo llevan viven solo en el servidor — esa separación protege tus claves de API.

bash
# .env.production
DATABASE_URL=postgres://usuario:pass@host:5432/db   # solo servidor
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXX                  # visible en el bundle

Regla simple: lo que empiece con NEXT_PUBLIC_ asumí que lo ve cualquiera que abra las DevTools.

Segundo, el bundle. next build te muestra el tamaño de cada chunk, pero para ver qué módulo pesa de más necesitás el analyzer:

bash
pnpm add -D @next/bundle-analyzer
ts
// next.config.ts
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

export default withBundleAnalyzer(nextConfig);

Con ANALYZE=true pnpm run build se abre un reporte interactivo de cada bundle y encontrás el lodash que coló un componente o el chart que nadie importó donde debía. Tercero, el monitoreo: los errores en producción no se ven en tu máquina. Un servicio como Sentry (o el monitor de Vercel si hosteás ahí) te avisa del primer crash antes que el cliente. Diez minutos de setup, y es lo que separa “el sitio se cayó” de “el sitio se cayó y ya sabemos por qué”.

La configuración de producción no es glamorosa, pero es la diferencia entre un sitio que anda y uno que anda bien: metadata con metadataBase, sitemap y robots, imágenes con priority y sizes, headers de caché y seguridad, variables públicas separadas de las privadas y monitoreo activo. Si querés ver cómo estas decisiones se miden en el sitio real, tenés la guía completa de Core Web Vitals y rendimiento web, y si te interesa por qué un desarrollo a medida con Next.js termina con mejor configuración que una plantilla, este análisis compara ambos caminos.

¿Tienes un proyecto en mente?

Si estás trabajando en algo parecido y querés contrastar ideas, escribinos.

Compartir: