Metadata dinámica en Next.js: cada página con su propia identidad

26-05-2026

¿Todas las páginas de tu blog comparten el mismo título genérico tipo “Mi Blog - Artículo”? Es hora de que cada artículo tenga su propia identidad en Google. Vamos a implementar metadatos dinámicos que se generan automáticamente.

El problema: títulos genéricos y SEO perdido

Imagina esto: has escrito un artículo brillante sobre arquitectura de software. Lo publicas, funciona perfectamente en tu sitio, pero cuando buscas el tema en Google… nada. Tu artículo no aparece, o peor, aparece, pero con un título y descripción tan genéricos que nadie hace clic.

El problema es que con rutas dinámicas en Next.js, no puedes simplemente escribir un título en el <head> y esperar que Google entienda de qué va cada artículo. Cada página necesita sus propios metadatos personalizados, su título específico, su descripción única, su URL canónica y toda la información que los motores de búsqueda y las redes sociales necesitan para entender y promover tu contenido.

Sin metadata dinámica, todas tus páginas compiten entre sí con títulos similares, Google no sabe cuál posicionar para qué búsqueda y terminas con un SEO diluido donde ninguna página destaca realmente.

La buena noticia es que Next.js 13+ tiene una solución elegante para esto. La mala noticia es que hay varios detalles que pueden arruinar todo si no los conoces. 

La solución: generateMetadata al rescate

Next.js nos da una función especial llamada generateMetadata() que se ejecuta en cada página antes de renderizarla. Esta función tiene acceso a los parámetros de la ruta, así que puede saber qué artículo estás viendo y generar los metadatos apropiados .

La belleza de esta función es que es completamente dinámica. Entonces si alguien visita /blog/mi-articulo-genial, la función recibe slug: "mi-articulo-genial", busca ese artículo en tu repositorio, y genera metadatos personalizados con el título real, la descripción real, y toda la información específica de ese artículo.

La implementación completa

En el anterior artículo se explican conceptos que enlazan con lo descrito de aquí en adelante. Recomiendo previamente la lectura de “Sitemap Dinámico en Next.js: Adiós a los XMLs Estáticos”.

La implementación es sencilla y sumamente efectiva.

 

import { Metadata } from "next" import { GetArticlesBySlug } from "@/backend/articles/application" import { FileSystemReader } from "@/backend/articles/infrastructure/file-system-reader" import { MarkdownArticlesRepository } from "@/backend/articles/infrastructure/markdown-articles-repository" interface BlogPageProps { params: Promise<{ slug: string }> } export async function generateMetadata({ params }: BlogPageProps): Promise<Metadata> { const { slug } = await params const fileSystemReader = new FileSystemReader() const articlesRepository = new MarkdownArticlesRepository(fileSystemReader) const getArticlesBySlug = new GetArticlesBySlug(articlesRepository) const article = await getArticlesBySlug.execute(slug) let canonicalUrl = `https://tudominio.com/blog/${slug}` if (article.canonical_url && isExternalUrl(article.canonical_url)) { canonicalUrl = article.canonical_url } const description = article.content?.slice(0, 160).trimStart() || article.slug return { title: article.title, description, alternates: { canonical: canonicalUrl }, openGraph: { title: article.title, description, url: canonicalUrl, type: "article" } } } function isExternalUrl(url: string): boolean { try { const hostname = new URL(url).hostname.replace(/^www\./, "") return hostname !== "tudominio.com" } catch { return false } } export default async function BlogPage({ params }: BlogPageProps) { const { slug } = await params return <div>Tu componente de artículo aquí</div> }

Este código tiene varios trucos que hacen que todo funcione perfectamente. Déjame desglosarlo.

Los detalles que marcan la diferencia

Parámetros como Promise (Next.js 15)

Primero, fíjate en cómo destructuramos los parámetros:

 

const { slug } = await params

Si vienes de versiones anteriores de Next.js, esto puede parecerte raro. En Next.js 15, los parámetros son ahora una Promise en que debes agregar await. Es un cambio que inicialmente puede parecer molesto, pero tiene sentido desde el punto de vista de la arquitectura de React Server Components. No olvides ese await,o vas a tener errores crípticos que te van a volver loco.

Consistencia con el repositorio

Luego instanciamos el mismo repositorio que usarías en el sitemap. Esto es importante porque mantiene la consistencia. Si el sitemap dice que un artículo existe, generateMetadata() debe poder encontrarlo con la misma lógica. Usamos GetArticlesBySlug que es un caso de uso específico para buscar artículos por su slug. Esto sigue el patrón de arquitectura limpia que mantiene tu código organizado y testeable.

La descripción automática

Ahora viene la parte de la descripción y aquí hay un truco profesional:

 

const description = article.content?.slice(0, 160).trimStart() || article.slug

¿Por qué 160 caracteres? Porque es aproximadamente el límite de lo que Google muestra en los resultados de búsqueda. Si tu artículo no tiene una descripción explícita en el frontmatter, este código toma los primeros 160 caracteres del contenido.

El .trimStart() es crucial. Si tu archivo Markdown empieza con un salto de línea o espacios (lo cual es común), sin este método tu descripción empezaría con espacios en blanco. Google no va a penalizarte por eso, pero se ve poco profesional en los resultados de búsqueda.

Y fíjate en el fallback final; si article.content es undefined o null, el operador || hace que usemos el slug como descripción. Es mejor tener algo, aunque no sea perfecto, que dejar campos vacíos o que la aplicación explote con un error del tipo “Cannot read property ‘slice’ of undefined”.

URLs canónicas y Open Graph

La metadata que retornamos incluye varias cosas importantes:

 

return { title: article.title, description, alternates: { canonical: canonicalUrl }, openGraph: { title: article.title, description, url: canonicalUrl, type: "article" } }

El título y la descripción son obvios, pero alternates: { canonical: canonicalUrl } le dice a Google cuál es la URL “verdadera” de este contenido. Si tu artículo se publicó primero en otro sitio (como Medium), esto evita problemas de contenido duplicado.

Y la sección de openGraph es crucial para cuando alguien comparte tu artículo en redes sociales como Twitter, Facebook o LinkedIn. Estas plataformas leen esa metadata para mostrar una preview bonita con el título, descripción e imagen destacada. Sin esto, tus artículos compartidos se ven horribles: solo una URL sin contexto.

Validando URLs externas vs internas

La función isExternalUrl() es importante:

 

function isExternalUrl(url: string): boolean { try { const hostname = new URL(url).hostname.replace(/^www\./, "") return hostname !== "tudominio.com" } catch { return false } }

Esta función verifica si una URL canónica apunta a tu dominio o a otro sitio. Nota que también elimina el “www.” antes de comparar, para que www.tudominio.com y tudominio.com se traten como el mismo dominio.

Si el artículo tiene canonical_url: "https://medium.com/@tu-usuario/articulo", esta función detecta que es externa y usa esa URL como canónica. Le estás diciendo a Google “este contenido se publicó primero allá, ve a ese sitio para indexarlo”. Esto es honesto y profesional y Google lo aprecia.

Los resultados: SEO que funciona

Después de implementar esto, cada artículo de tu blog tiene:

Su propio título específico en los resultados de búsqueda. Ya no es “Mi Blog - Artículo”, sino “Cómo Implementar Clean Architecture en TypeScript” o lo que sea el título real. Google entiende de qué trata cada página y puede rankearlas apropiadamente para búsquedas relevantes.

Una descripción única generada desde su propio contenido. Los primeros 160 caracteres de cada artículo se convierten en la descripción que la gente ve en Google. Esto aumenta dramáticamente el click-through rate porque las descripciones son relevantes y específicas.

URLs canónicas respetadas que evitan contenido duplicado. Si republicaste contenido de otro sitio, has marcado claramente cuál es la fuente original. Google sabe exactamente cómo manejar tu contenido.

Open Graph perfecto para redes sociales. Cuando alguien comparte uno de tus artículos en Twitter, LinkedIn o Facebook, se ve espectacular con una card bonita que incluye el título, descripción e imagen destacada. Esto aumenta significativamente el engagement porque la gente confía más en links que se ven profesionales.

Tips adicionales para metadata Pro :fire:

Priority en el Sitemap

Los valores de priority en el sitemap (que verías en el artículo complementario sobre sitemaps) son como darle pistas a Google sobre qué páginas te importan más. La home debería ser 1.0 porque es tu página principal. Las secciones importantes como el índice del blog podrían ser 0.9. Los artículos individuales generalmente están bien con 0.7. Y páginas secundarias o archivadas podrían bajar a 0.5. Por ejemplo en la función buildStaticRoutes() que genera las entradas del sitemap para tu página principal.

 

function buildStaticRoutes(): MetadataRoute.Sitemap { return [ { url: BASE_URL, lastModified: new Date(), priority: 1.0 } ] }

No es que Google obedezca estos valores al pie de la letra, pero sí los usa como una señal más en su algoritmo de priorización de crawling.

Descripción vs Excerpt

Si tu CMS o frontmatter tiene un campo específico para la descripción o excerpt, úsalo antes que los primeros 160 caracteres del contenido. Así tienes control total sobre lo que aparece en los resultados de búsqueda:

 

const description = article.excerpt || article.content?.slice(0, 160).trimStart() || article.slug

Imágenes en Open Graph

Aunque no lo incluimos en el ejemplo por simplicidad, deberías añadir imágenes a tu Open Graph metadata:

 

openGraph: { title: article.title, description, url: canonicalUrl, type: "article", images: [ { url: article.imageUrl, width: 1200, height: 630, alt: article.imageAlt } ] }

Las dimensiones 1200x630 son las recomendadas para que se vea bien en todas las plataformas sociales.

El flujo completo: Sitemap + Metadata

Esta metadata dinámica se complementa perfectamente con un sitemap dinámico (tema del artículo complementario Sitemap Dinámico en Next.js: Adiós a los XMLs Estáticos). El sitemap le dice a Google qué páginas existen y dónde encontrarlas. La metadata le explica de qué trata cada una y cómo debe mostrarlas en los resultados de búsqueda.

Juntos forman la solución perfecta para el SEO de tu blog. Es como darle a Google un mapa del tesoro con instrucciones detalladas en cada parada.

Conclusión: metadata que trabaja para ti

Una vez implementado esto, puedes básicamente olvidarte del tema. Cada nuevo artículo que escribas automáticamente tendrá su metadata optimizada. No hay tareas manuales de SEO que recordar. Tu flujo de trabajo es simple: escribe tu artículo, haz commit, deploya, y listo. El resto sucede automágicamente.

Google necesita tiempo para rastrear e indexar tu contenido (días o semanas), pero lo importante es que has hecho tu parte. Les has dado metadata clara y específica para cada página. Has respetado las URLs canónicas y evitado contenido duplicado. Básicamente, le has puesto las cosas muy pero muy fáciles a Google.

Y con el tiempo, empezarás a ver los resultados. Más páginas indexadas. Mejor posicionamiento en búsquedas relevantes. Más tráfico orgánico. Todo porque le diste a Google las herramientas que necesitaba para entender y promover tu contenido.