Next.js Metadata API: Dinamički SEO, Open Graph slike i strukturirani podaci

Praktičan vodič kroz Next.js 15 Metadata API u App Routeru: generateMetadata, dinamičke Open Graph slike, JSON-LD strukturirani podaci i kanonski URL-ovi — s primjerima koje možete odmah iskoristiti.

Next.js 15 Metadata API: SEO i Open Graph 2026

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: false za noindex)
  • viewport — od Next.js 14 izdvojen u zasebni export
  • verification — Google Search Console, Yandex, Bing
  • themeColor — boja toolbar-a na mobilnim uređajima (također unutar viewport exporta)
// 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

  1. 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.
  2. Nedostaje metadataBase: Open Graph slike s relativnim putanjama neće raditi na društvenim mrežama. Postavite metadataBase u root layoutu i mirno spavajte.
  3. generateMetadata u Client Componentu: funkcija mora biti exportirana iz Server Component stranice ili layouta. U Client Componentima jednostavno ne radi.
  4. 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).
  5. 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.

O Autoru Editorial Team

Our team of expert writers and editors.