SEO with Next.js App Router: Adding Structured Data for Rich Results

Table of contents
- What is JSON-LD and Why Should You Care?
- LD-JSON in Next.js App Router – The Problem
- What’s the Fix?
- The Solution: useSkipHydration Hook + SchemaRenderer Component
- Use It in Your Page (Server Component)
- Validating Your Structured Data
- How This Fix Actually Works – Under the Hood
- Final Thoughts: Structured Data Done Right in Next.js

What is JSON-LD and Why Should You Care?
JSON-LD stands for JavaScript Object Notation for Linked Data. It’s a way to describe your website’s content so that search engines understand not just what’s on the page but how everything is connected.
It uses regular JSON format to embed structured data in your HTML via a<script type="application/ld+json">
tag. This script isn’t visible to users but helps Google identify key information like authors, recipes, events, products, and more.
Why does this matter?
Because it enables rich results, enhanced search listings with star ratings, images, FAQs, and more. These can significantly boost your site's performance:
Rotten Tomatoes added structured data to 100,000 pages and saw a 25% higher click-through rate on enhanced pages.
The Food Network enabled search features on 80% of its pages and observed a 35% increase in visits.
Rakuten found users spent 1.5x more time and had a 3.6x higher interaction rate on AMP pages with structured data.
Nestlé reported an 82% higher click-through rate for pages showing rich results.
In short: JSON-LD helps your content stand out in search, improves click-through rates, and gives users more reason to engage with your site.
Read More:
LD-JSON in Next.js App Router – The Problem
Next.js provides official guidance on using JSON-LD with the App Router, introduced in Next.js 13. However, there’s a subtle but important issue.
When you add a<script type="application/ld+json">
tag directly in a server component, it gets rendered twice:
Once on the server – as part of the initial HTML.
Again on the client – during hydration, when React re-renders the component.
This results in duplicate JSON-LD tags, which can confuse tools like Google’s Structured Data Testing Tool. It might flag the duplication, or it might just ignore your markup altogether.
Why does this happen?
Because React Server Components embed data in the HTML, but then React hydrates and re-renders the page on the client, thus injecting your schema again.
This is a known issue with Next.js in certain setups.
What’s the Fix?
The solution: create a custom client component that checks if the schema tag is already present in the DOM before rendering it.
The Solution: useSkipHydration
Hook + SchemaRenderer
Component
components/SchemaRenderer.tsx
"use client";
import { useEffect, useState } from "react";
const useSkipHydration = (id: string) => {
const [skipHydration, setSkipHydration] = useState(false);
useEffect(() => {
if (document?.getElementById(id)) {
setSkipHydration(true);
}
}, [id]);
return skipHydration;
};
export default function SchemaRenderer({
id,
schema,
}: {
id: string;
schema: any;
}) {
const skipHydration = useSkipHydration(id);
if (skipHydration) return null;
return (
<script
id={id}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
Use It in Your Page (Server Component)
app/[slug]/page.tsx
import SchemaRenderer from '@/components/SchemaRenderer';
const jsonLdData = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Your Blog Title",
"author": {
"@type": "Person",
"name": "John Doe"
},
"datePublished": "2025-06-01"
};
export default function BlogPage() {
return (
<>
<article>
<h1>Your Blog Content</h1>
</article>
<SchemaRenderer id="json-ld-blog" schema={jsonLdData} />
</>
);
}
Note: You won’t see the script tag in the DOM using browser devtools in development (
npm run dev
). Instead, check the page source or build withnpm run build
and run withnpm run start
to verify the script is correctly rendered.
Validating Your Structured Data
Use the following tools to ensure your schema is valid:
How This Fix Actually Works – Under the Hood
useSkipHydration
Hook
const useSkipHydration = (id: string) => {
const [skipHydration, setSkipHydration] = useState(false);
useEffect(() => {
if (document?.getElementById(id)) {
setSkipHydration(true);
}
}, [id]);
return skipHydration;
};
This hook checks after mounting whether a script tag with the given id
already exists in the DOM.
Why it’s important:
The server already injected the
<script>
tag.During hydration, React tries to render it again.
This hook detects the presence and prevents a second render.
Conditional Rendering
if (skipHydration) return null;
If the tag is already present, skip rendering it again. This avoids duplication of the script tags.
Rendering the Schema
<script
id={id}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
If the script tag isn’t present, this renders it.dangerouslySetInnerHTML
is necessary to inject raw JSON into the script tag.
Final Thoughts: Structured Data Done Right in Next.js
Implementing JSON-LD is one of the simplest and most powerful ways to improve your site’s SEO:
✅ Unlocks rich search results
✅ Improves click-through rates
✅ Helps search engines deeply understand your content
But with React Server Components and hydration quirks, doing it naively can cause duplication and broken validation.
This approach solves that cleanly:
✅ Works with SSR + hydration
✅ Prevents duplicate
<script>
tags✅ Keeps your schema clean and Google-friendly
Whether it’s a blog, a product page, or a full-fledged app, this method keeps your structured data robust, scalable, and SEO-optimized.
Subscribe to my newsletter
Read articles from Chaitanya Raj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Chaitanya Raj
Chaitanya Raj
I'm a fullstack web developer from New Delhi and a CS alumni of the University of Delhi. I have worked with React (NextJS) / Vue (NuxtJS) for front-end dev, Node.js for back-end, Webflow for no-code dev and Figma for web design. I have a wide variety of interests, ranging from Web Dev, Design, CyberSecurity and AI/ML to History, Geopolitics, Psychology, Linguistics and Art. I love to learn new things. Curiosity is the prime motivator for me. If I find something that catches my eye, well, I'll be in my room tinkering with it for the next few weeks. I write sometimes.