Dynmic Metadata from API Appears Inside <body> Instead of <head> (App)

I am using Next.js App Router (app/) with dynamic routes and generating metadata based on API responses.
However, my metadata does NOT appear in the SSR <head> when I check “View Page Source”.
Instead, metadata is either missing, or appears inside the <body> after hydration.

Project Context

  • Route: /app/(propertyPages)/(SeoPages)/[...slugs]/page.tsx
  • Metadata is generated using generateMetadata()
  • Metadata depends on API calls (example: getPageBySlugOne, validateSlugsOne, etc.)
  • I am using:
export const revalidate = 86400; // ISR 24h
export const dynamicParams = true; // Enable on-demand generation
  • API functions run inside generateMetadata()

The Problem

Even though metadata is returned correctly from generateMetadata, it does not show inside <head> in SSR HTML.

What I actually see in “View Page Source”:

  • Only global metadata from layout.tsx
  • NO title
  • NO meta description
  • NOTHING from generateMetadata()

Instead, Next.js injects metadata after hydration inside the <body>

This is the HTML output I uploaded:
(uploaded exact file here: random.txt)

All metadata is missing from <head> in the raw server HTML.

How can I force metadata to be rendered server-side inside <head> even when it comes from an API?

3. Does ISR + dynamicParams still make generateMetadata() behave dynamically?

4. Do I need to use cache(), dynamic = "force-static", or { next: { revalidate }} inside fetch() to fix this?

@harpreet-3932 @amyegan
I hope you can help me out with this issue :folded_hands:

@pawlean @harpreet-3932 @emil-christensen

The Next.js team have said that this is intentional when streaming metadata is involved

whenever metadata resolution would potentially block rendering the page, we instead defer it and stream it into the page body. Browsers are still able to interpret the title tag properly regardless of where it’s rendered in the DOM

I hope that helps!

1 Like

Recently I worked on same scenario, check out the below code in your page.tsx

/app/(propertyPages)/(SeoPages)/[...slugs]/page.tsx



interface PageProps {

  params: Promise<{ slug: string }>;

}



export async function generateMetadata({

params,

}: PageProps): Promise<Metadata> {

const { slug } = await params;


const workPreview = await getWorkPreviewAPI({ type: 'slug', id: slug });


if (!workPreview.success) {

return {};

  }


const {

metaTitle,

metaDescription,

metaKeywords,

metaOGImg,

metaAltText,

bannerImage,

  } = workPreview.blogData;



return {

title: metaTitle,

description: metaDescription,

keywords: metaKeywords,

openGraph: {

title: metaTitle,

description: metaDescription,

images: {

url: getImageUrl(metaOGImg ?? bannerImage),

alt: metaAltText ?? '',

      },

    },

twitter: {

title: metaTitle,

description: metaDescription,

images: {

url: getImageUrl(metaOGImg ?? bannerImage),

alt: metaAltText ?? '',

      },

card: 'summary_large_image',

    },

  };

}

On local you might be not get expected result but if make it like it will work as expected.

2 Likes

@farzigalib Thanks for sharing what worked for you :slight_smile:

1 Like

thanks for replying , but i am doiing the same

I would help you if you provide more context of you code, specially layout.tsx and […slug]/page.tsx

I implemented the same in one of my project you can check it here

So basically /case-studies/[slug] is dynamic and every page have it own metadata

actually problem is in only this url , else dynamic route are woring fine as expectd

Create layout.tsx for (SeoPages) & (propertyPages) and then it will work. Try once.

1 Like