App Router zastąpił next/head typowanym Metadata API. Dwa sposoby użycia — statyczny eksport dla stałych stron, generateMetadata dla dynamicznych:
// app/layout.tsx — domyślne ustawienia całej witryny z szablonem tytułu
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
template: "%s | Acme",
default: "Acme — studio web developmentu",
},
description: "Domyślny opis używany, gdy strona nie ustawi własnego.",
};
// app/blog/[slug]/page.tsx — dynamiczne metadane per post
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
if (!post) return { title: "Nie znaleziono wpisu" };
return {
title: post.title, // przez szablon stanie się „Tytuł wpisu | Acme"
description: post.excerpt,
};
}
Praktyczne zasady, które realnie wpływają na pozycje:
- Title: ~50–60 znaków, główne słowo kluczowe blisko początku, unikalny dla każdej strony. Google przepisuje słabe tytuły — nie dawaj mu powodu.
- Description: ~150–160 znaków. Nie wpływa bezpośrednio na ranking, ale jest twoim tekstem reklamowym w wynikach i napędza CTR.
metadataBasejest obowiązkowy — bez niego względne adresy obrazków OG i canonicali psują się na produkcji.- Deduplikuj zapytanie między
generateMetadataa komponentem strony przez Reactcache()— inaczej odpytujesz CMS dwa razy na każde żądanie.
import { cache } from "react";
export const getPost = cache(async (slug: string) => {
return fetchPostFromCMS(slug);
});
Checklista:
-
metadataBaseustawiony w głównym layoucie - Szablon tytułu zdefiniowany raz, strony ustawiają tylko swoją część
- Każda strona ma unikalny title i description
- Strony z CMS używają
generateMetadataz fetchem opakowanym wcache() - Metadane zapasowe dla 404 / brakującej treści