How to Fix: Request for Native structuredData Support in Metadata API
Native structuredData is not supported by the Next.js Metadata API, and that is why this issue breaks at build or render time when JSON-LD is placed directly inside the metadata object.
The current Metadata API supports a defined schema for fields such as title, description, openGraph, twitter, robots, and related SEO primitives. It does not expose a native structuredData field for arbitrary schema payloads like JSON-LD. If you try to add unsupported properties to the exported metadata object, TypeScript and the framework metadata pipeline will reject or ignore them, depending on how the code is written.
Table of Contents
Understanding the Root Cause
This issue happens because the Next.js Metadata API is a typed, framework-controlled abstraction. It is not a free-form container for every SEO-related format.
Under the hood, Next.js reads the exported metadata object and transforms only the supported keys into actual head elements. That transformation layer expects a known shape. A field like structuredData is currently outside that supported contract, so one of these outcomes usually happens:
- TypeScript error if the object is typed as Metadata.
- Silent omission if the object is loosely typed.
- Unexpected behavior if developers assume unsupported metadata keys will render into the document head.
This is why the request for native structured data support exists in the first place: developers want the same first-class API for JSON-LD that already exists for Open Graph and Twitter metadata.
Until that support is added, the correct pattern is to render structured data manually using a <script type=”application/ld+json”> tag in a server component, layout, or page.
Step-by-Step Solution
The fix is to keep supported SEO fields inside metadata and move your JSON-LD into a rendered script tag.
1. Keep the Metadata API limited to supported fields
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Product Page',
description: 'A product page with SEO metadata',
openGraph: {
title: 'Product Page',
description: 'A product page with SEO metadata',
type: 'website'
}
};
2. Render JSON-LD manually in the page or layout
const structuredData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: 'Example Product',
description: 'A product page with SEO metadata',
brand: {
'@type': 'Brand',
name: 'Example Brand'
}
};
export default function Page() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData)
}}
/>
<main>
<h1>Example Product</h1>
</main>
</>
);
}
3. Reuse a helper for cleaner implementation
If multiple routes need schema markup, create a small reusable component.
type JsonLdProps = {
data: Record<string, unknown>;
};
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data)
}}
/>
);
}
import type { Metadata } from 'next';
import { JsonLd } from './JsonLd';
export const metadata: Metadata = {
title: 'Article Page',
description: 'An article with structured data'
};
export default function Page() {
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'Article Page',
author: {
'@type': 'Person',
name: 'Jane Developer'
}
};
return (
<>
<JsonLd data={articleSchema} />
<main>Content</main>
</>
);
}
4. Sanitize output if data is user-controlled
If any field inside the schema comes from a CMS or user input, validate it before stringifying. The biggest concern is not JSON-LD itself, but unsafe content flowing into a script tag.
function safeJsonLd(data: Record<string, unknown>) {
return JSON.stringify(data).replace(/</g, '\\u003c');
}
export function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: safeJsonLd(data)
}}
/>
);
}
5. Validate the rendered schema
After implementing the manual script approach, inspect the page source and confirm the JSON-LD appears exactly once and contains valid schema. Then test it with a structured data validator from your preferred SEO workflow.
For framework behavior and supported metadata fields, check the Next.js Metadata API documentation.
Common Edge Cases
- Putting JSON-LD inside metadata anyway: If you add structuredData to the metadata export, it will not become a valid head script just because the object compiles.
- Escaping issues: Raw characters like < inside serialized content can create parsing or security concerns. Escaping before injection is safer.
- Duplicate schema: Rendering the same JSON-LD in both a layout and a page can create duplicate entities that confuse crawlers.
- Client component placement: JSON-LD is better rendered in a server component when possible so it appears in the initial HTML response.
- Dynamic routes: If schema depends on fetched data, generate it from the same server-side data source used to build the page content.
- Mismatch with metadata fields: Do not try to force Open Graph, Twitter, and JSON-LD into one object model. They are different SEO surfaces with different rendering paths.
FAQ
Can I extend the Metadata type to add structuredData?
You can extend a local TypeScript type, but Next.js still will not natively process that extra field into a JSON-LD script. Typing does not change framework behavior.
Should I use next/script for JSON-LD?
You can, but for simple schema output in the App Router, a plain <script type=”application/ld+json”> inside a server component is usually enough and keeps the implementation straightforward.
Where should JSON-LD live: layout or page?
Put shared site-wide schema in a layout and route-specific schema in the page. The goal is to avoid duplication while matching the actual content scope.
The practical resolution to this GitHub issue is simple: do not place JSON-LD inside the Metadata API until native support exists. Use the Metadata API for supported fields, and inject structured data manually with a script tag for reliable SEO behavior today.