# Dynamic metadata in the App Router

generateMetadata, params as a Promise, metadataBase and titles without duplicates: the metadata crawlers read in the first HTML.

> Page: https://ui.eneawork.it/blog/nextjs-metadata-dinamica
> Markdown: https://ui.eneawork.it/blog/nextjs-metadata-dinamica.md

*2026-11-01*

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.

Misurato il 1 novembre 2026 sul repository EneUI: public/blog-covers/it contiene 596 file per 21.793.882 byte; i PNG pesano in media 71.036 byte ciascuno, cioè 298 articoli per due lingue. Metodo: lettura di fs.statSync().size su ogni file della cartella con uno script Node.

## 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

## 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.

## FAQ

### 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.
