Next.js Metadata API: Dynamisk SEO, Open Graph og generateMetadata (2026)
Komplet guide til Next.js 15 Metadata API i App Router. Lær generateMetadata til dynamisk SEO, dynamiske Open Graph-billeder med next/og, og type-sikre sitemap.ts og robots.ts med kodeeksempler.
Next.js Metadata API er det indbyggede system i App Router, som lader dig generere SEO-tags, Open Graph-billeder og structured data direkte fra dine Server Components, enten statisk via et eksporteret metadata-objekt eller dynamisk via en generateMetadata-funktion. I praksis betyder det, at du kan styre <title>, beskrivelser, canonical URL'er og social cards uden manuelt at redigere <head>, og at dine værdier kan komme fra database-kald eller eksterne API'er per route. Denne guide dækker hele API'et for Next.js 15.x i 2026.
Eksportér enten et statisk metadata-objekt eller en asynkron generateMetadata-funktion fra page.tsx eller layout.tsx. Aldrig begge i samme fil.
Metadata fra child-segmenter merger ovenpå parent layouts; arrays som openGraph.images overskrives helt, mens objekter merges felt for felt.
Dynamiske Open Graph-billeder genereres med ImageResponse fra next/og og kører på Edge Runtime per default i Next.js 15.
sitemap.ts og robots.ts bor i app/ og eksporteres som type-sikre funktioner, så der er ingen statisk XML-fil at vedligeholde.
JSON-LD structured data tilføjes ved at rendere en <script type="application/ld+json"> direkte i layoutet; Metadata API'et eksporterer det ikke for dig.
Brug metadataBase i root layout for at gøre alle relative billed-URL'er absolutte. Uden den får Open Graph-crawlere ufuldstændige links.
Hvad er Next.js Metadata API?
Metadata API'et er en filsystem-baseret konvention i App Router, som erstatter den gamle next/head-komponent fra Pages Router. I stedet for at importere en komponent og rendere tags imperativt, eksporterer du data (enten et objekt eller en funktion) fra dine page.tsx- eller layout.tsx-filer. Next.js serialiserer derefter dataene til <head> på serveren før HTML når browseren. Det betyder, at metadata er tilgængelig for crawlere fra første response og ikke afhænger af klient-side hydrering.
Jeg har skrevet metadata med next/head i fire år før App Router landede, og den største praktiske forskel er, at du nu ikke kan ende i en race condition, hvor en client component opdaterer document.title efter første paint. Crawleren får altid det rigtige output, fordi det rendres server-side. Til gengæld er API'et mere statisk i sin natur. Det er ikke designet til at ændre sig efter mount, og forsøg på det er en stærk indikator på, at routes burde være splittet ud.
For en oversigt over tilgængelige felter henviser Next.js' egen dokumentation til den officielle Metadata API-reference, som dækker alle ~40 understøttede properties inklusive alternates, verification og appleWebApp.
Statisk metadata-objekt: det simpleste setup
Til sider, hvor titel og beskrivelse er kendt på build-tidspunkt (landing pages, om-os, vilkår), eksporterer du bare en navngivet konstant. Next.js parser eksporten ved build og renderer tags ind i <head>. Bemærk at title kan være en streng, et template-objekt eller et "absolute" objekt, der bryder ud af layout-templates.
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://example.dk'),
title: {
default: 'Akme — Værktøj til udviklere',
template: '%s | Akme',
},
description: 'Bygget af danske udviklere til danske udviklere.',
openGraph: {
type: 'website',
locale: 'da_DK',
siteName: 'Akme',
},
robots: {
index: true,
follow: true,
googleBot: { 'max-image-preview': 'large' },
},
}
Når en child-side senere eksporterer title: 'Priser', bliver det færdige tag <title>Priser | Akme</title> takket være template-strengen. Vil du eksplicit bryde ud af templatet (f.eks. på en kampagne-landing), sender du title: { absolute: 'Black Friday hos Akme' } i stedet. metadataBase er nøglen til at få relative openGraph.images: '/og.png' til at rendere som absolutte URL'er, hvilket er et krav fra både Facebook og LinkedIn.
generateMetadata til dynamiske routes
Når titlen afhænger af params (produkt-ID, slug, bruger-handle), bruger du generateMetadata. Funktionen er async, modtager samme params og searchParams som siden selv, og kører på serveren før render. Her er et eksempel fra et blog-system, der trækker en post fra databasen:
To detaljer, der ofte snyder folk: params er en Promise i Next.js 15 (det ændrede sig i 15.0; tidligere var det et synkront objekt i 14), og du skal await'e den. Og det getPost-kald, du foretager i generateMetadata, dedupes automatisk med det samme kald inde i page-komponenten takket være Reacts cache()-wrapper, så vidt du bruger fetch med samme URL eller pakker funktionen ind i cache(). Det er den samme deduplication, vi udnytter i Next.js Route Handlers og API-endpoints for at undgå dobbelt-roundtrips.
Hvordan sætter jeg Open Graph og Twitter Cards i Next.js?
Open Graph og Twitter Card-tags er bare nestede objekter under openGraph og twitter i Metadata-typen. Crawlere som Facebook, LinkedIn, Slack og Discord læser og:title, og:description og og:image; Twitter (X) supplerer med twitter:card. Du behøver normalt kun sætte ét billede. twitter arver fra openGraph, hvis du ikke overskriver det eksplicit.
export const metadata: Metadata = {
title: 'Akmes nye prisplan',
openGraph: {
title: 'Akmes nye prisplan',
description: 'Halv pris for små teams under 5 brugere.',
url: 'https://example.dk/priser',
images: [
{
url: '/og/priser.png', // bliver absolut via metadataBase
width: 1200,
height: 630,
alt: 'Akme prisplan oversigt',
},
],
locale: 'da_DK',
type: 'website',
},
twitter: {
card: 'summary_large_image',
creator: '@akmedk',
},
}
Anbefalede mål for OG-billeder er 1200×630 (1.91:1), hvilket er hvad både Facebook og LinkedIn beskærer til. Twitter accepterer det også uden problemer. Hvis du sætter card: 'summary_large_image', men leverer et 800×800-billede, vil X centrere og beskære, hvilket sjældent ser pænt ud. Test altid med opengraph.xyz eller LinkedIns Post Inspector, før du shipper.
Dynamiske OG-billeder med next/og og ImageResponse
I stedet for at vedligeholde statiske PNG'er for hver landing page, kan du generere Open Graph-billeder on demand fra JSX. next/og-pakken eksporterer en ImageResponse-klasse, der renderer en lille Tailwind-lignende subset af CSS via Satori (Vercels OSS-bibliotek) og returnerer en PNG. Filen hedder altid opengraph-image.tsx eller twitter-image.tsx og bor ved siden af den page.tsx, den dækker.
Filen er reelt en specialiseret Route Handler. Den kører ved første request (og caches derefter), så cold-start matters, og runtime = 'edge' er standard af samme grund. Satori understøtter flexbox, men ikke grid; det fanger folk, som forsøger at portere et komplet React-design direkte. Hold layoutet enkelt. Du finder hele subsettet i Vercels OG image-dokumentation.
sitemap.ts og robots.ts som type-sikre filer
Frem for at generere en sitemap.xml via et build-script, eksporterer du en funktion fra app/sitemap.ts, og Next.js producerer XML'en på /sitemap.xml. Det samme gælder app/robots.ts. Begge filer er fuldt type-sikre via importerede typer fra next.
Hvis dit site har over 50.000 URL'er, eksporterer du en generateSitemaps-funktion ved siden af sitemap, som returnerer et array af ID'er. Next.js kalder så sitemap(id) én gang per chunk og genererer et sitemap-index.xml automatisk. Denne deling fungerer fint sammen med ISR-strategier. Se vores guide til Partial Prerendering i Next.js 15 for hvordan sitemap-generering interagerer med PPR-cachen.
JSON-LD og structured data til rich results
Metadata API'et eksporterer ikke JSON-LD for dig, så du skal selv rendere <script type="application/ld+json">. Det er bevidst: schema.org-formatet udvikler sig hurtigt, og Vercel ville ikke fastlåse en typedefinition. Det enkleste mønster er en lille komponent, du dropper ind i din page:
Bemærk, at vi bruger dangerouslySetInnerHTML i stedet for at sende objektet som children. React ville ellers escape'e tegnene og bryde JSON-parseren. Hvis dine værdier indeholder bruger-input, sanitér med en lib som serialize-javascript, ellers kan en post med </script> i titlen bryde ud af tag'et. Jeg ramte præcis den bug i et nyhedsbrev-flow sidste år, og det er ikke noget, du opdager, før Google Search Console begynder at fortælle dig om markup-fejl. For en komplet liste over typer, valider altid med Googles Rich Results Test.
Hvordan merger metadata på tværs af layouts og pages?
Next.js evaluerer metadata-eksporter fra rod-layoutet og ned til den aktive page. Hvert dybere niveau merges ovenpå parent-niveauet. Reglerne er ikke helt intuitive: simple felter overskrives, nestede objekter spread-merges per felt, og arrays overskrives helt. Det er især vigtigt at huske for openGraph.images: sætter du en images: [...] i et child layout, mister du parents billeder, du supplerer ikke.
I praksis betyder mergingen, at du normalt sætter et hav af openGraph-defaults i root layout (siteName, locale, type) og kun overskriver title, description og images per side. Hvis du har brug for en sti, hvor en hel sektion får forskellige OG-defaults (f.eks. en /da-route med dansk locale og en /en-route med engelsk), placerer du metadata-eksporten i det respektive sub-layout, ikke i hver enkelt page.
Almindelige fejl og hvordan du fixer dem
De fleste produktions-bugs, jeg har set i Metadata API'et, falder i fire kategorier. Her er dem, sorteret efter hvor ofte de rammer.
Symptom
Årsag
Fix
OG-billede vises ikke på LinkedIn/Facebook
Relativ URL og manglende metadataBase
Sæt metadataBase: new URL(...) i root layout
Build fejler med "duplicate metadata export"
Både metadata og generateMetadata eksporteret
Behold kun én, typisk generateMetadata
Title viser kun template-streng uden side-titel
Page eksporterer ikke title
Tilføj title i page eller sæt title.default
params er undefined inde i generateMetadata
Glemt await i Next.js 15
Skriv const { slug } = await params
En sidste fælde værd at nævne: hvis du caller en server action eller en mutation inde i generateMetadata, vil Next.js opte routen ud af static rendering. Det er korrekt opførsel, fordi metadataet afhænger af request-tid, men det betyder, at en route, du troede var statisk, pludselig bliver dynamisk. Hold generateMetadata til read-only data, og brug Next.js Middleware til rutebeskyttelse, hvis du har brug for request-niveau logik før render.
Ofte stillede spørgsmål
Kan jeg bruge useEffect eller client state til at sætte metadata?
Nej. Metadata API'et er kun tilgængeligt fra Server Components. Forsøger du at importere Metadata-typen i en fil med 'use client', fejler bygget. Hvis du absolut skal opdatere titlen efter mount, sæt document.title direkte, men de fleste tilfælde løses bedre ved at splitte routen, så data hentes server-side.
Skal jeg bruge generateMetadata, hvis min side er statisk?
Kun hvis værdierne afhænger af params, searchParams eller eksterne kald. For helt statiske sider er det almindelige metadata-objekt enklere, hurtigere at læse og rendres uden ekstra funktionskald ved build.
Hvordan opdaterer jeg metadata efter en ISR-revalidate?
Når en route revalideres via revalidatePath eller revalidateTag, kører generateMetadata igen som del af den nye render. Du behøver ikke gøre noget særligt. Så længe din data-kilde returnerer den opdaterede værdi, vil næste request få fornyet metadata.
Hvorfor vises mit dynamiske OG-billede ikke på Twitter?
Tre mistænkte i prioriteret rækkefølge: (1) metadataBase er ikke sat, så URL'en bliver relativ; (2) Twitter cacher aggressivt, så kør URL'en gennem deres Card Validator for at force-refreshe; (3) din opengraph-image.tsx renderer ikke i preview-environment, fordi den kræver edge runtime og dine miljø-variabler.
Hvad er forskellen på metadata, viewport og generateViewport?
Siden Next.js 14.2 er themeColor, colorScheme og viewport blevet flyttet til en separat viewport-eksport (eller generateViewport). Hvis du stadig sender dem i metadata, ser du en deprecation-warning ved build. Migrér ved at flytte felterne til en ny eksport.
Lær hvordan Partial Prerendering kombinerer statisk HTML-shell med streamede Suspense-grænser i Next.js 15, og se hvordan du sætter det op rute-for-rute med praktiske kodeeksempler.