SEO u Next.js 15 više nije nešto što naknadno "zalijepite" na kraj projekta — i, iskreno, dobro je da nije. App Router donosi Metadata API koji integrira meta tagove, Open Graph slike, kanonske URL-ove i strukturirane podatke izravno u file-system route. Postavite li generateMetadata kako treba, Google dobiva točno ono što očekuje. Bez ručnog ubacivanja tagova u <head>, bez vanjskih biblioteka i, ono najbolje, bez onih iznenađenja kad shvatite da pola Twitter kartica nikad nije imalo preview sliku.
U ovom vodiču pokazat ću kako konfigurirati dinamičke meta tagove, generirati Open Graph slike u runtimeu, dodati JSON-LD strukturirane podatke i — što je možda i najvažnije — izbjeći greške koje uzrokuju duple title tagove ili nedostajuće preview kartice na društvenim mrežama.
Statički vs. dinamički metadata u App Routeru
Next.js App Router podržava dva načina definiranja metadata: statički objekt i dinamička funkcija. Oba se exportiraju iz layout.tsx ili page.tsx datoteka, a Next ih kombinira kroz hijerarhiju ruta. Što je u praksi vrlo zgodno jer ne morate ponavljati istu konfiguraciju na svakoj stranici.
Statički metadata objekt
Za stranice gdje su naslov i opis poznati već u build vremenu, dovoljan je jednostavan export:
// app/blog/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Blog | Next.js Launchpad',
description: 'Vodiči o Next.js 15, App Routeru i Vercel deploymentu.',
keywords: ['Next.js', 'App Router', 'React Server Components'],
openGraph: {
title: 'Blog | Next.js Launchpad',
description: 'Vodiči o Next.js 15.',
url: 'https://example.com/blog',
siteName: 'Next.js Launchpad',
locale: 'hr_HR',
type: 'website',
},
}
export default function BlogPage() {
return <main>...</main>
}
Dinamički metadata kroz generateMetadata
Za dinamičke rute (recimo /blog/[slug]), gdje se sadržaj učitava iz baze ili CMS-a, koristi se generateMetadata. Ta funkcija se izvršava na serveru i može biti async — što znači da slobodno fetcham podatke prije nego što vratim metadata objekt:
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params
const post = await fetchPostBySlug(slug)
if (!post) {
return { title: 'Članak nije pronađen' }
}
const previousImages = (await parent).openGraph?.images || []
return {
title: post.title,
description: post.excerpt,
alternates: {
canonical: `https://example.com/blog/${slug}`,
},
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [post.coverImage, ...previousImages],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
Važna napomena: u Next.js 15 params i searchParams su asinkroni. Morate ih await-ati. Ja sam se na ovome opekao prvi put kad sam migrirao projekt s 14 na 15 — TypeScript je vrištao, runtime je ispisivao upozorenja, a ja sam pola sata tražio "krivu" konfiguraciju koja zapravo nije bila kriva.
Naslijeđivanje i kompozicija metadata
Next.js gradi metadata od root layouta prema listu. Svaki segment može nadjačati ili dopuniti polja iz nadređenog. Ovo je ključno za konzistentnost — definirajte zajedničke vrijednosti jednom, u app/layout.tsx, a po stranicama nadjačajte samo ono što se razlikuje.
Template za naslove
Najčistija praksa? Iskoristite title.template u root layoutu da automatski dodate brand suffix:
// app/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | Next.js Launchpad',
default: 'Next.js Launchpad — Vodiči za moderni React',
},
metadataBase: new URL('https://example.com'),
}
Sad svaka podstranica koja postavi title: 'Tutorial' automatski dobiva "Tutorial | Next.js Launchpad" u <title> tagu. A ako želite stranicu bez prefiksa (tipično landing page), koristite title: { absolute: 'Custom Title' }.
metadataBase i relativni URL-ovi
Postavljanjem metadataBase na URL produkcijske domene dobivate jednu malu, ali jako korisnu stvar: slobodno koristite relativne putanje za Open Graph slike — images: ['/og.png'] umjesto da svaki put pišete pune URL-ove. Bez toga, Next.js ispisuje upozorenje i u nekim slučajevima generira neispravne preview linkove. Ne baš zabavno otkrivanje na Slacku.
Open Graph slike generirane u runtimeu
Jedna od stvarno moćnih značajki Metadata API-ja je generiranje dinamičkih OG slika pomoću ImageResponse iz next/og. Ne treba vam Photoshop, ne treba vam vanjski servis — pišete običan JSX i CSS koji se izvršava na edge runtimeu.
Konvencija opengraph-image.tsx
Stvar je smiješno jednostavna: dodajte datoteku opengraph-image.tsx u bilo koji segment rute, i Next.js će automatski generirati sliku te postaviti odgovarajuće og:image tagove.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'Cover slika članka'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({
params,
}: {
params: { slug: string }
}) {
const post = await fetchPostBySlug(params.slug)
return new ImageResponse(
(
<div
style={{
fontSize: 64,
background: 'linear-gradient(135deg, #0f172a, #1e293b)',
color: 'white',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 80,
}}
>
<div style={{ fontSize: 28, opacity: 0.7 }}>Next.js Launchpad</div>
<div style={{ marginTop: 24, lineHeight: 1.2 }}>{post.title}</div>
</div>
),
{ ...size }
)
}
Slika je statički generirana tijekom builda za poznate rute, ali možete je servirati i dinamički ako koristite generateImageMetadata za varijacije po jeziku ili temi (npr. tamna vs. svijetla verzija).
Twitter (X) kartice
Twitter koristi vlastite tagove, ali možete koristiti istu sliku — bilo kroz konvenciju twitter-image.tsx, bilo posebnim definiranjem u generateMetadata. Za summary_large_image format omjer je 2:1, pa je 1200×630 px gotovo univerzalno siguran zadani izbor.
Strukturirani podaci (JSON-LD) za rich snippete
Google koristi strukturirane podatke u formatu schema.org/JSON-LD za prikaz rich rezultata u SERP-u: zvjezdice za recenzije, breadcrumb, autora članka, vrijeme čitanja. Metadata API ne renderira JSON-LD direktno, no možete ga jednostavno ubaciti kao <script> tag unutar komponente:
// app/blog/[slug]/page.tsx
export default async function Post({ params }: Props) {
const { slug } = await params
const post = await fetchPostBySlug(slug)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
'@type': 'Person',
name: post.author,
url: `https://example.com/autori/${post.authorSlug}`,
},
publisher: {
'@type': 'Organization',
name: 'Next.js Launchpad',
logo: {
'@type': 'ImageObject',
url: 'https://example.com/logo.png',
},
},
}
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<h1>{post.title}</h1>
{/* sadržaj */}
</article>
)
}
Mali, ali važan savjet: validirajte rezultat kroz Google Rich Results Test prije nego što išta deployate u produkciju. Pogrešne sheme znaju potpuno zbuniti Google, koji onda ignorira sve strukturirane podatke na stranici. Vidio sam to nekoliko puta — frustrirajuće je dijagnosticirati, a još više popraviti retroaktivno.
Kanonski URL-ovi i internacionalizacija
Za sajtove s više jezika ili duplikatima sadržaja (klasičan primjer: /blog/post i /blog/post?utm_source=...), kanonski URL govori Googleu koja je primarna verzija:
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug, locale } = await params
return {
alternates: {
canonical: `/${locale}/blog/${slug}`,
languages: {
'en-US': `/en/blog/${slug}`,
'hr-HR': `/hr/blog/${slug}`,
'x-default': `/en/blog/${slug}`,
},
},
}
}
Polje languages generira <link rel="alternate" hreflang="..."> tagove. Google koristi x-default kao fallback — onda kad korisnikov jezik ne odgovara nijednoj eksplicitno definiranoj verziji.
robots, viewport i ostali tagovi koje ljudi zaborave
Metadata API podržava i one tagove koje obično zaboravimo dok ne dođe SEO audit:
robots— kontrola indeksiranja po stranici (index: falseza noindex)viewport— od Next.js 14 izdvojen u zasebni exportverification— Google Search Console, Yandex, BingthemeColor— boja toolbar-a na mobilnim uređajima (također unutarviewportexporta)
// app/layout.tsx
import type { Viewport } from 'next'
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: '#0f172a' },
],
width: 'device-width',
initialScale: 1,
}
export const metadata: Metadata = {
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: 'vas-google-verification-kod',
},
}
Najčešće greške i kako ih izbjeći
- Dupli title tagovi: ako ručno dodate
<title>u JSX uz Metadata API, dobit ćete dva taga. Uvijek ide samo jedan ili drugi pristup — preporuka: Metadata API. - Nedostaje metadataBase: Open Graph slike s relativnim putanjama neće raditi na društvenim mrežama. Postavite
metadataBaseu root layoutu i mirno spavajte. - generateMetadata u Client Componentu: funkcija mora biti exportirana iz Server Component stranice ili layouta. U Client Componentima jednostavno ne radi.
- Sinkroni params u Next.js 15: ako ne await-ate
params, dobit ćete TypeScript grešku i runtime upozorenje (a ako ignorirate oboje, dobit ćete i tihu produkcijsku grešku — tipičan jackpot). - OG slika prevelika: Facebook i LinkedIn imaju limit od oko 8 MB. ImageResponse generira PNG-ove koji su obično 50–200 KB, dakle sigurno ispod limita.
Testiranje i debug
Prije nego pošaljete promjene u produkciju, prođite kroz ovu kratku checklistu:
- View source u browseru — provjerite da generirani
<head>sadrži očekivane tagove - Facebook Sharing Debugger — testira OG tagove i refresha cache
- Twitter Card Validator — preview kartice prije objave
- Google Rich Results Test — validira JSON-LD i daje predviđanje SERP prikaza
- LinkedIn Post Inspector — LinkedIn agresivno keširra OG slike, ovo ih osvježi (vjerujte mi, trebat će vam)
Često postavljana pitanja
Koja je razlika između metadata objekta i generateMetadata funkcije?
Statički metadata export koristi se za vrijednosti poznate u build vremenu. generateMetadata je async funkcija koja prima params i može dohvatiti podatke s API-ja, baze ili CMS-a. Koristite je za dinamičke rute poput /blog/[slug], /proizvod/[id] ili korisničke profile.
Mogu li koristiti Metadata API u Pages Routeru?
Ne. Metadata API je ekskluzivan za App Router. U Pages Routeru koristite next/head komponentu ili biblioteke poput next-seo. Ako migrirate s Pages na App Router, Metadata API zamijenit će skoro sve što ste prije radili kroz <Head>.
Hoće li Google indeksirati dinamičke Open Graph slike?
Da, ali samo ako su dostupne na javnoj URL putanji u trenutku crawlanja. opengraph-image.tsx generira statičke slike za sve rute poznate u build vremenu (kroz generateStaticParams). Za on-demand rute koristite ISR ili generateImageMetadata kako bi slike bile dostupne na zahtjev.
Kako dodati Open Graph sliku samo za određene rute?
Smjestite opengraph-image.tsx u taj segment rute. Konvencija prati hijerarhiju datoteka — slika definirana u app/blog/[slug]/opengraph-image.tsx primjenjuje se samo na pojedinačne blog postove, dok app/opengraph-image.png u rootu služi kao fallback za sve ostale rute.
Mogu li animirati ImageResponse za GIF ili video?
Ne. ImageResponse generira statičke PNG/JPEG slike — što je, realno, sasvim dovoljno za Open Graph i Twitter kartice, koje ionako ne podržavaju animacije u preview kontekstu. Ako baš trebate video preview, koristite og:video tag s linkom na MP4 datoteku hostiranu negdje drugdje.
Zaključak
Next.js Metadata API praktički uklanja potrebu za bibliotekama poput next-seo i daje fino-zrnatu kontrolu nad SEO signalima u svakoj točki rute. Najbitnije pravilo, ako pamtite samo jedno iz cijelog ovog vodiča: definirajte metadataBase i title.template u root layoutu, koristite generateMetadata za sve dinamičke rute, i obavezno validirajte rezultat kroz Rich Results Test prije produkcije. Uz dinamičke OG slike i JSON-LD strukturirane podatke, vaši Next.js sajtovi 2026. imat će sve što je potrebno za pojavljivanje u Google rich rezultatima i lijepo izgledane preview kartice na društvenim mrežama.