El App Router sustituyó next/head por la Metadata API tipada. Dos formas de uso: exportación estática para páginas fijas, generateMetadata para las dinámicas:
// app/layout.tsx — ajustes por defecto de todo el sitio con plantilla de título
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
template: "%s | Acme",
default: "Acme — estudio de desarrollo web",
},
description: "Descripción por defecto que se usa cuando la página no define la suya.",
};
// app/blog/[slug]/page.tsx — metadatos dinámicos por post
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) return { title: "Entrada no encontrada" };
return {
title: post.title, // a través de la plantilla será "Título de la entrada | Acme"
description: post.excerpt,
};
}
Reglas prácticas que influyen de verdad en las posiciones:
- Title: ~50–60 caracteres, la palabra clave principal cerca del inicio, único en cada página. Google reescribe los títulos flojos: no le dé motivos.
- Description: ~150–160 caracteres. No influye directamente en el posicionamiento, pero es su texto publicitario en los resultados e impulsa el CTR.
metadataBasees obligatorio: sin él, las direcciones relativas de las imágenes OG y de los canonicals se rompen en producción.- Deduplique la consulta entre
generateMetadatay el componente de la página con Reactcache(); si no, consulta el CMS dos veces por cada petición.
import { cache } from "react";
export const getPost = cache(async (slug: string) => {
return fetchPostFromCMS(slug);
});
Checklist:
-
metadataBasedefinido en el layout principal - Plantilla de título definida una sola vez, las páginas solo ponen su parte
- Cada página tiene title y description únicos
- Las páginas de CMS usan
generateMetadatacon el fetch envuelto encache() - Metadatos de reserva para 404 / contenido ausente
