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

Chaitanya RajChaitanya Raj
4 min read

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:

  1. Once on the server – as part of the initial HTML.

  2. 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 with npm run build and run with npm 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.

0
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.