Metadata duplication during hydration with generateMetadata in Next.js 15 - specific to product pages using next js 15 medusa v2 starter template #84645
-
SummaryMetadata duplication during hydration with generateMetadata in Next.js 15 - specific to product pagesProblemMetadata tags are being duplicated during client-side hydration in Next.js 15, but only on product pages. The server-side rendered HTML is clean with no duplication, but after hydration, duplicate metadata tags appear in the DOM. This does NOT happen on other pages (home, category, collection, blog pages). Environment
Steps to Reproduce
Code ExamplesProduct Page (Has Duplication Issue)// src/app/(main)/products/[handle]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
console.log('[generateMetadata] Called for product page')
const { countryCode, handle } = await params
try {
const result = await getProduct(countryCode, handle)
if (!result?.product) {
return {
title: "Product Not Found",
description: "The requested product could not be found.",
}
}
const { product } = result
// Test with simplified metadata like category page
return generateCategoryMetadata({
title: product.title,
description: product.description || `Shop ${product.title} at Example Store. Premium products with delivery available.`,
keywords: product.tags?.map(tag => tag.value) || [],
canonical: `/products/${product.handle}`,
ogImage: product.images?.[0]?.url,
})
} catch (error) {
console.error("Error generating product metadata:", error)
return {
title: "Product",
description: "Premium products at Example Store.",
}
}
}Category Page (Works Correctly)// src/app/(main)/categories/[...category]/page.tsx
export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params
try {
const productCategory = await getCategoryByHandle(params.category)
// ... metadata processing logic ...
return generateCategoryMetadata({
title: baseTitle,
description,
keywords: allKeywords ? allKeywords.split(', ') : generateKeywords('category', { name: baseTitle }),
canonical: url,
ogImage: finalOgImage,
})
} catch (error) {
notFound()
}
}Home Page (Works Correctly)// src/app/(main)/page.tsx
export async function generateMetadata(): Promise<Metadata> {
const title = "Example Products, Items & Services"
const description =
"Shop high-quality products, items and services. Premium options, safe and low-maintenance. Delivery available."
return {
title: title, // Let the template handle the formatting
description,
keywords: [
"example products",
"example items",
"example services",
"premium products",
"high quality",
"safe products",
"indoor items",
"outdoor items",
"low maintenance",
],
alternates: { canonical: `/` },
openGraph: {
// Let Next.js fall back to top-level title and description
url: `/`,
images: ["/opengraph-image.jpg"],
siteName: "Example Store",
type: "website",
},
twitter: {
card: "summary_large_image",
images: ["/twitter-image.jpg"],
},
}
}Metadata Generator FunctionsgenerateCategoryMetadata (Works Correctly)export function generateCategoryMetadata(config: BaseMetadataConfig): Metadata {
const {
title,
description,
keywords = [],
canonical,
ogImage,
noIndex = false
} = config
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
const canonicalUrl = canonical ? `${baseUrl}${canonical}` : undefined
return {
title: title, // Let the template add "| Example Store"
description,
keywords: keywords.join(", "),
// Use alternates.canonical for proper canonical link tag
alternates: canonicalUrl ? { canonical: canonicalUrl } : undefined,
robots: {
index: !noIndex,
follow: !noIndex,
googleBot: {
index: !noIndex,
follow: !noIndex,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
openGraph: {
title: title, // Let the template add "| Example Store"
description,
url: canonicalUrl,
images: ogImage ? [{
url: ogImage,
width: 1200,
height: 630,
alt: title,
}] : undefined,
type: "website",
siteName: "Example Store",
locale: "en_GB",
},
twitter: {
card: "summary_large_image",
title: title, // Let the template add "| Example Store"
description,
images: ogImage ? [ogImage] : undefined,
},
}
}generateProductMetadata (Causes Duplication)export function generateProductMetadata(config: ProductMetadataConfig): Metadata {
const {
title,
description,
keywords = [],
canonical,
ogImage,
noIndex = false,
price,
availability = "in stock",
brand = "Example Store",
category = "Example Products",
images = [],
structuredData
} = config
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
const canonicalUrl = canonical ? `${baseUrl}${canonical}` : undefined
return {
title: title, // Let the template add "| Example Store"
description,
keywords: keywords.join(", "),
// Use alternates.canonical for proper canonical link tag
alternates: canonicalUrl ? { canonical: canonicalUrl } : undefined,
robots: {
index: !noIndex,
follow: !noIndex,
googleBot: {
index: !noIndex,
follow: !noIndex,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
openGraph: {
title: title, // Let the template add "| Example Store"
description,
url: canonicalUrl,
images: images.length > 0 ? [{
url: images[0], // Only use first image to prevent duplication
width: 1200,
height: 630,
alt: title,
}] : ogImage ? [{
url: ogImage,
width: 1200,
height: 630,
alt: title,
}] : undefined,
type: "website",
siteName: "Example Store",
locale: "en_GB",
},
twitter: {
card: "summary_large_image",
title: title, // Let the template add "| Example Store"
description,
images: images.length > 0 ? [images[0]] : ogImage ? [ogImage] : undefined, // Only use first image
},
}
}Interface Definitionsexport interface BaseMetadataConfig {
title: string
description: string
keywords?: string[]
canonical?: string
ogImage?: string
noIndex?: boolean
structuredData?: any
}
export interface ProductMetadataConfig extends BaseMetadataConfig {
price?: string
availability?: string
brand?: string
category?: string
images?: string[]
}Expected vs Actual Behavior
Additional Notes
Possible Causes
This appears to be a Next.js 15 specific issue with metadata handling during hydration on product pages. Additional informationNo response ExampleNo response |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 4 replies
-
|
i am also using the medusa js v2 next js 15 starter storefront |
Beta Was this translation helpful? Give feedback.
-
|
SOLVED component was calling a server action (trackRecentlyViewed) from a client component during React hydration, which caused Next.js 15 to re-execute the generateMetadata function, leading to metadata duplication. |
Beta Was this translation helpful? Give feedback.
SOLVED component was calling a server action (trackRecentlyViewed) from a client component during React hydration, which caused Next.js 15 to re-execute the generateMetadata function, leading to metadata duplication.