Guía SEO para Next.js: metadata, sitemap, schemas y Core Web Vitals
Next.js es el framework frontend con mejor SEO out-of-the-box: rendering híbrido, Metadata API nativa, optimización automática de imágenes y fuentes, Core Web Vitals impecables por defecto. Pero para exprimirlo del todo hay que conocer bien sus APIs y patrones. En esta guía técnica cubrimos todo lo que un desarrollador necesita para llevar una web en Next.js (App Router) al top de Google, con código real y comandos ejecutables.
Esta guía asume Next.js 14+ con App Router. Si usas Pages Router, los conceptos son equivalentes pero la API es distinta. También asumimos TypeScript.
Por qué Next.js tiene ventaja SEO
Cuatro motivos estructurales por los que Next.js rankea mejor que la mayoría de frameworks o CMS:
- Rendering estático por defecto: HTML pre-generado → TTFB < 100 ms.
- Code-splitting automático: cada ruta carga solo el JS necesario.
- Optimización de imágenes y fuentes nativa: WebP/AVIF, lazy-loading, CLS = 0.
- Metadata API declarativa: sin plugins, control total del
<head>.
Si estás evaluando tecnología, lee también nuestra comparativa Next.js vs WordPress.
Metadata API: títulos, descripciones y OpenGraph
Desde Next.js 13 con App Router, la Metadata API reemplaza al antiguo next/head. Se exporta en cada layout o página.
Metadata estática
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Servicios de desarrollo web",
description: "Diseño, desarrollo a medida, SEO y marketing digital.",
alternates: { canonical: "/servicios" },
openGraph: {
title: "Servicios · Think! Madrid",
description: "Agencia full-service para tu proyecto digital.",
url: "https://www.thinkmadrid.com/servicios",
type: "website",
},
twitter: { card: "summary_large_image" },
};Metadata con template global
En tu layout.tsx define un template para que cada página solo tenga que dar su título:
export const metadata: Metadata = {
metadataBase: new URL("https://www.thinkmadrid.com"),
title: {
default: "Think! Madrid · Diseño, desarrollo y marketing digital",
template: "%s · Think! Madrid",
},
};Ahora una página con title: "Servicios" se renderizará como Servicios · Think! Madrid.
Metadata dinámica (con fetch)
Para páginas de detalle (blog, productos), usa generateMetadata:
import type { Metadata } from "next";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `/blog/${slug}` },
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.date,
authors: [post.author],
},
};
}URLs canónicas (y por qué son críticas)
Google penaliza contenido duplicado. Las URLs canónicas le dicen cuál es la versión "oficial" de una página. Tres reglas:
- Todas las páginas deben tener
alternates.canonicaldefinida. - Siempre en
metadataBaseabsoluto (evita errores cross-domain). - No canonicalizar a páginas con
noindex(provoca conflictos).
robots.txt dinámico
Crea src/app/robots.ts y Next.js servirá automáticamente /robots.txt:
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: "*", allow: "/" },
// Bots IA (GEO)
{ userAgent: "GPTBot", allow: "/" },
{ userAgent: "ClaudeBot", allow: "/" },
{ userAgent: "PerplexityBot", allow: "/" },
{ userAgent: "Google-Extended", allow: "/" },
],
sitemap: "https://www.thinkmadrid.com/sitemap.xml",
host: "https://www.thinkmadrid.com",
};
}El array incluye bots IA (GEO — Generative Engine Optimization). Para más detalle, revisa nuestra guía de SEO técnico.
sitemap.xml dinámico
Mismo patrón. src/app/sitemap.ts genera el sitemap en runtime o en build.
import type { MetadataRoute } from "next";
const BASE = "https://www.thinkmadrid.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Páginas estáticas
const staticPages: MetadataRoute.Sitemap = [
{ url: `${BASE}/`, priority: 1.0, changeFrequency: "weekly" },
{ url: `${BASE}/servicios`, priority: 0.9 },
{ url: `${BASE}/proyectos`, priority: 0.9 },
{ url: `${BASE}/contacto`, priority: 0.7 },
];
// Páginas dinámicas (blog)
const posts = await getAllPosts();
const blogPages: MetadataRoute.Sitemap = posts.map((p) => ({
url: `${BASE}/blog/${p.slug}`,
lastModified: new Date(p.date),
priority: 0.7,
changeFrequency: "monthly",
}));
return [...staticPages, ...blogPages];
}Open Graph images dinámicas
En vez de diseñar una imagen por página, Next.js puede generar cada OG image programáticamente con next/og y el componente ImageResponse.
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const contentType = "image/png";
export const size = { width: 1920, height: 1080 };
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
const fontData = await fetch(
new URL("../../../public/fonts/Bold.ttf", import.meta.url)
).then((r) => r.arrayBuffer());
return new ImageResponse(
(
<div style={{ display: "flex", background: "#0a0a0a", width: "100%", height: "100%" }}>
<div style={{ position: "absolute", left: 140, bottom: 120, color: "white", fontSize: 80 }}>
{post.title}
</div>
</div>
),
{
...size,
fonts: [{ name: "Bold", data: fontData, weight: 700 }],
}
);
}Importante: nunca uses query strings (?title=...) en URLs OG. Twitter, WhatsApp y otros las rechazan. Usa siempre opengraph-image.tsx por ruta.
Schema.org / JSON-LD
Los datos estructurados aumentan la visibilidad en SERPs (rich results) y en AI Overviews. Crea un helper reutilizable:
export function articleLd(post: Post) {
return {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
datePublished: post.date,
dateModified: post.updated ?? post.date,
author: { "@type": "Organization", name: "Think! Madrid" },
publisher: {
"@type": "Organization",
name: "Think! Madrid",
logo: { "@type": "ImageObject", url: "https://www.thinkmadrid.com/logo.png" },
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://www.thinkmadrid.com/blog/${post.slug}`,
},
};
}Y lo inyectas en la página:
export default async function Post({ params }) {
const post = await getPostBySlug(params.slug);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleLd(post)) }}
/>
<article>...</article>
</>
);
}Valida los schemas con el Rich Results Test de Google y con validator.schema.org.
Rendering strategies: SSG, SSR o ISR
Next.js elige por ti si no configuras nada, pero entender cada uno es crítico:
| Modo | Cuándo usarlo | Impacto SEO |
|---|---|---|
| SSG (estático) | Contenido que no cambia (about, servicios). | Óptimo. TTFB ideal. |
| ISR | Contenido que se actualiza periódicamente (blog, catálogo). | Óptimo con revalidate adecuado. |
| SSR | Personalización por usuario o datos ultra frescos. | Bueno si TTFB bajo. |
Cómo forzar cada modo en App Router:
// ISR — revalida cada 1 hora
export const revalidate = 3600;
// SSG puro (sin revalidación)
export const dynamic = "force-static";
// SSR — renderizado por request
export const dynamic = "force-dynamic";next/image: optimización automática y LCP
next/image genera automáticamente WebP/AVIF, lazy-loading, placeholders y evita CLS reservando espacio con width/height.
import Image from "next/image";
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero de la agencia"
width={1920}
height={1080}
priority // mejora el LCP si es imagen above-the-fold
sizes="(max-width: 768px) 100vw, 1920px"
/>
);
}priority es fundamental para el LCP (imagen above-the-fold). Sin ella, Next.js las carga lazy y destroza tu Largest Contentful Paint.
next/font: self-hosting automático y CLS
Carga fuentes de Google o locales sin CLS y sin pedirlas a google.com (todo se autoaloja en build).
import { Inter } from "next/font/google";
import localFont from "next/font/local";
// Desde Google Fonts (se autohospeda)
const inter = Inter({ subsets: ["latin"], display: "swap" });
// Fuente propia
const newBlack = localFont({
src: "../../public/fonts/NewBlackTypeface-Bold.ttf",
variable: "--font-newblack",
display: "swap",
});
export default function RootLayout({ children }) {
return (
<html lang="es" className={`${inter.className} ${newBlack.variable}`}>
<body>{children}</body>
</html>
);
}Internal linking con next/link
Usa siempre Link de next/link: hace prefetch del bundle en background cuando el link entra en viewport, acelerando la navegación (mejora INP) y manteniendo state entre rutas.
import Link from "next/link";
<Link href="/servicios" prefetch>
Ver servicios
</Link>
// Desactivar prefetch si el link rara vez se usa
<Link href="/cookies" prefetch={false}>
Política de cookies
</Link>Estrategia de linking interno: cada página prioritaria debe tener al menos 3 enlaces internos apuntando a ella desde otras páginas. Los pillars deben recibir links desde todos sus spokes, y los spokes desde el pillar.
Core Web Vitals en Next.js
Umbrales oficiales de Google (deben estar en verde para pasar Core Web Vitals):
- LCP < 2.5s
- INP < 200ms
- CLS < 0.1
Cómo medir en local:
# Build de producción
npm run build
# Arranca modo prod
npm run start
# En otra terminal, usa Lighthouse CLI
npx @lhci/cli autorun --collect.url=http://localhost:3000Cómo mediar en producción (web-vitals):
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
// Envía a GA4, Sentry, etc.
if (window.gtag) {
window.gtag("event", metric.name, {
value: Math.round(metric.value),
metric_id: metric.id,
metric_rating: metric.rating,
});
}
});
return null;
}Redirects y rewrites (migraciones)
Cuando migras una web, los 301 son críticos para preservar SEO:
import type { NextConfig } from "next";
const config: NextConfig = {
async redirects() {
return [
// Slug viejo → nuevo
{ source: "/articulo-viejo", destination: "/blog/articulo-nuevo", permanent: true },
// Forzar HTTPS + www (si aplica)
{
source: "/:path*",
has: [{ type: "host", value: "thinkmadrid.com" }],
destination: "https://www.thinkmadrid.com/:path*",
permanent: true,
},
];
},
};
export default config;Hreflang e internacionalización SEO
Si tu web es multiidioma, declara los hreflang para que Google muestre la versión correcta según país/idioma:
export const metadata: Metadata = {
alternates: {
canonical: "/servicios",
languages: {
"es-ES": "https://www.thinkmadrid.com/servicios",
"en-US": "https://www.thinkmadrid.com/en/services",
"x-default": "https://www.thinkmadrid.com/servicios",
},
},
};x-default es la URL de fallback cuando no hay match de idioma. Crítico para webs B2B internacionales.
Deploy + Google Search Console
Tras cada deploy, flujo recomendado:
- Comprueba que
/robots.txty/sitemap.xmlresponden 200. - En Search Console → Sitemaps → añade
/sitemap.xml. - En URL Inspection → pega URLs clave y pulsa Request Indexing.
- Valida schemas en Rich Results Test.
- Mide Core Web Vitals con PageSpeed Insights.
Herramientas y repos de referencia
- Examples oficiales de Next.js — patrones de referencia.
- Docs oficiales de Metadata.
- Templates de Vercel (blogs, e-commerce, SaaS) — todos optimizados SEO.
- next-sitemap (para casos avanzados: sitemap index, exclusiones por patrón).
- Lighthouse CI para automatizar auditorías en cada PR.
Checklist final de SEO en Next.js
Antes de lanzar o en cada release mayor, valida estos puntos:
Conclusión
Next.js es el framework que más acelera los resultados SEO sin fricción técnica. Aplicando este checklist, tu web parte con Core Web Vitals óptimos, metadata y schemas completos, sitemap y robots automáticos, y está preparada tanto para Google como para los motores IA (ChatGPT, Perplexity, Gemini).
Si quieres profundizar más, revisa nuestra guía de SEO técnico, el pillar de desarrollo web o la comparativa Next.js vs WordPress.
Si prefieres que hagamos nosotros el trabajo, en Think! Madrid somos especialistas en Next.js. Escríbenos y auditamos tu proyecto sin compromiso.
Preguntas frecuentes
¿Next.js es bueno para SEO?
Sí, Next.js es una de las mejores opciones para SEO. Soporta rendering estático (SSG), rendering en servidor (SSR) e ISR, optimización automática de imágenes y fuentes, metadata API nativa, sitemap y robots.txt dinámicos, y genera Core Web Vitals excelentes por defecto.
¿Cómo genero metadata dinámica por página en Next.js App Router?
Con la función generateMetadata async en cada page.tsx, recibiendo params y fetching datos. Devuelve un objeto Metadata con title, description, openGraph, twitter, canonical, etc.
¿SSG, SSR o ISR: cuál uso para SEO?
Para máxima velocidad y SEO usa SSG siempre que sea posible (contenido estático). ISR es ideal para blog/listados que se actualizan sin redeploy. SSR solo si necesitas personalización por request. Next.js permite combinarlos por ruta.
¿Cómo genero un sitemap dinámico?
Crea src/app/sitemap.ts exportando por defecto una función que devuelve un MetadataRoute.Sitemap con todas las URLs. Next.js lo sirve automáticamente en /sitemap.xml.
¿Tienes un proyecto en mente?
Cuéntanos qué necesitas y te proponemos la mejor solución sin compromiso.
Hablar con el equipo →