React
Metadata dinamica nell’App Router
generateMetadata, params come Promise, metadataBase e titoli senza duplicati: la metadata che i crawler leggono nel primo HTML.
In sintesi
- `generateMetadata` riceve gli stessi `params` della pagina e restituisce un oggetto `Metadata`.
- `metadataBase` nel layout radice trasforma i percorsi relativi in URL assoluti.
- Un titolo figlio non ripete il nome del sito: il `template` del layout lo aggiunge.
La metadata dinamica si scrive con generateMetadata, una funzione async che riceve gli stessi params della pagina e restituisce un oggetto Metadata. I crawler vedono i tag nel primo HTML, non dopo l’idratazione.
generateMetadata, la forma minima
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise },
): Promise {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Article not found" };
return {
title: post.title,
description: post.summary,
alternates: { canonical: `/blog/${slug}` },
openGraph: {
title: post.title,
description: post.summary,
images: [`/og/${slug}.png`],
},
};
}
I params sono una Promise: vanno attesi anche quando sembrano già disponibili. La fetch dentro la funzione e quella dentro la pagina vengono deduplicate nello stesso render, quindi la riga non costa una seconda richiesta.
Per controllare cosa esce davvero, non fidarti dell’anteprima del browser: leggi l’HTML della prima risposta.
curl -s http://localhost:3000/blog/hello | grep -E "|og:title"
Statico e dinamico nello stesso file
Con generateStaticParams Next.js prerenderizza una pagina per ogni slug al build. generateMetadata gira in quel momento, quindi titolo e descrizione finiscono nell’HTML statico. Un articolo nuovo compare dopo il build o dopo una rivalidazione: è il prezzo della velocità.
export async function generateStaticParams() {
const posts = await listPosts();
return posts.map((post) => ({ slug: post.slug }));
}
metadataBase: gli URL assoluti
Un percorso relativo in openGraph.images viene risolto contro metadataBase. Senza quella riga Next.js usa l’host della richiesta e in build ottieni un avviso: la stessa pagina produce URL diversi a seconda di chi la visita. Imposta la base una volta nel layout.
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://ui.eneawork.it"),
title: { default: "EneUI", template: "%s — EneUI" },
openGraph: { type: "article", siteName: "EneUI" },
};
Titoli che non si ripetono
Il template nel layout aggiunge il nome del sito a ogni titolo figlio. La pagina figlia scrive solo il proprio titolo: se aggiungi anche il suffisso, il risultato è Articolo, EneUI, EneUI. Il default serve alla home, che non ha un padre da cui ereditare.
Open Graph generato, non disegnato a mano
Ogni articolo di questo sito ha due cover, una per lingua, prodotte da uno script che parte dal titolo e dai dati dell’articolo. La stessa idea si implementa con un file opengraph-image.tsx per route: una funzione che restituisce ImageResponse. Il vantaggio non è estetico: nessuno dimentica di aggiornare la grafica quando cambia il titolo.
Dove entrano i componenti del catalogo
Un elenco di articoli, una griglia di icone e una card hanno bisogno di markup già composto: i blocchi blog coprono il primo caso, la griglia di icone il secondo e le primitive di shadcn il terzo. La metadata resta nella route: un componente non può cambiarla.
Errori che si vedono in produzione
- Aggiungere il nome del sito nel titolo della pagina figlia: il template lo aggiunge già.
- Restituire
nulldagenerateMetadatainvece di un oggetto con il titolo di fallback. - Dimenticare
await params: l’oggetto Promise finisce nell’URL e il titolo diventa illeggibile. - Mettere
metadataBasein una pagina: vale per il layout, non per i figli.
Un limite da dichiarare
La metadata riflette lo stato al momento del render. Su una route statica, un titolo cambiato nel database non compare finché la pagina non viene rigenerata: nessuna funzione dinamica aggira il build. La seconda limitazione è dei client: chi non esegue JavaScript legge solo l’HTML iniziale, quindi quello che generi lì è tutto quello che vedrà.
Dove tenere la funzione rispetto alla pagina è in struttura di un progetto App Router. I dati strutturati che accompagnano la metadata sono in come aggiungere schema markup; il confronto Next.js vs Vite per un sito marketing spiega quando la metadata statica basta.
Domande frequenti
Posso usare generateMetadata in un client component?
No. La funzione gira sul server: un file con use client non può esportarla, e i tag devono esistere prima dell’idratazione.
generateMetadata fa una seconda richiesta dati?
No, se la stessa fetch ha gli stessi argomenti: React la deduplica nel render e la pagina riceve il risultato già in memoria.
Dove metto metadataBase?
Nel layout radice, una volta sola. Serve a risolvere in URL assoluti i percorsi relativi di openGraph, twitter e canonical.
Perché il titolo mostra il nome del sito due volte?
Perché il template del layout lo aggiunge e la pagina lo ha già scritto. La pagina figlia deve scrivere solo il proprio titolo.
La metadata si aggiorna senza rebuild?
Solo se la route è dinamica o se la rivalidazione è configurata. Su una route statica il titolo resta quello del build.