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.

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 null da generateMetadata invece di un oggetto con il titolo di fallback.
  • Dimenticare await params: l’oggetto Promise finisce nell’URL e il titolo diventa illeggibile.
  • Mettere metadataBase in 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.