cd /news/developer-tools/why-google-can-t-see-your-react-brea… · home topics developer-tools article
[ARTICLE · art-10858] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Why Google Can't See Your React Breadcrumbs (And the 4-Line Fix)

The article explains that Google's crawler often fails to recognize breadcrumbs in React apps because the structured data (JSON-LD schema) is embedded in client-rendered markup, which is unreliable for rich results. The solution is to inject a separate JSON-LD script into the `<head>` of each page, using a custom React hook or server-side rendering with Next.js, to ensure Google can properly parse the breadcrumb schema. This four-line fix allows developers to achieve rich results in Google Search without relying on visible DOM elements.

read5 min views24 publishedMay 23, 2026

I wasted an entire afternoon wondering why Google Search Console kept showing zero rich results for my React app. The breadcrumbs looked perfect in the browser. Users could see them. But Google? Completely blind. The problem wasn't my breadcrumb component it was that I'd never told Google what those breadcrumbs actually were. Structured data. Four lines of JSON-LD. That was it. Here's everything I learned, so you don't lose the same afternoon.

Why React Breadcrumbs Are Invisible to Google #

React apps render in the browser. Google's crawler is getting better at parsing JavaScript, but structured data embedded in client-rendered markup is still unreliable for rich results. The gold standard for breadcrumb rich results in Google Search is JSON-LD schema markup injected into <head>

not your visible DOM breadcrumb trail.

The two things are completely separate:

Your breadcrumb UI component what users see - Breadcrumb Schema markup what search engines read

Most tutorials show you how to build the UI. Almost none explain the schema side. That's why your breadcrumbs look fine to you but don't show up as rich results in Google.

Here's what a valid BreadcrumbList

schema looks like, per Schema.org:

{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://example.com"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://example.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "My Article",
      "item": "https://example.com/blog/my-article"
    }
  ]
}

Your job is to get exactly this into a <script type="application/ld+json">

tag in <head>

on every page that has breadcrumbs.

The Manual Approach: A Custom React Hook #

If you're not using any SEO library, here's a clean, copy-paste hook that builds and injects breadcrumb schema based on your current route.

// hooks/useBreadcrumbSchema.js
import { useEffect } from "react";

export function useBreadcrumbSchema(crumbs) {
  useEffect(() => {
    const schema = {
      "@context": "https://schema.org",
      "@type": "BreadcrumbList",
      itemListElement: crumbs.map((crumb, index) => ({
        "@type": "ListItem",
        position: index + 1,
        name: crumb.label,
        item: crumb.url,
      })),
    };

    const script = document.createElement("script");
    script.type = "application/ld+json";
    script.id = "breadcrumb-schema";
    script.textContent = JSON.stringify(schema);

    // Remove any existing breadcrumb schema before inserting
    document.getElementById("breadcrumb-schema")?.remove();
    document.head.appendChild(script);

    return () => {
      document.getElementById("breadcrumb-schema")?.remove();
    };
  }, [crumbs]);
}

Then in your page component:

// pages/BlogPost.jsx
import { useBreadcrumbSchema } from "../hooks/useBreadcrumbSchema";

export default function BlogPost({ post }) {
  useBreadcrumbSchema([
    { label: "Home", url: "https://example.com" },
    { label: "Blog", url: "https://example.com/blog" },
    { label: post.title, url: `https://example.com/blog/${post.slug}` },
  ]);

  return <article>{/* your content */}</article>;
}

Result: Every time the component mounts, the correct JSON-LD block is injected into <head>

and cleaned up on unmount. Paste it into Google's Rich Results Test and you'll see a green checkmark.

One caveat: if you're using SSR (Next.js, Remix, etc.), this useEffect

approach only runs client-side. For SSR, you need to render the script tag server-side which brings us to the next section.

The SSR Problem: Getting Schema Into <head> #

at Build Time

With Next.js, you can't rely on useEffect

for schema that needs to be in the initial HTML response. The fix is rendering a <script>

tag directly in your component using next/head

or the App Router's <Head>

:

// app/blog/[slug]/page.jsx (Next.js App Router)
import Head from "next/head";

function buildBreadcrumbSchema(crumbs) {
  return {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    itemListElement: crumbs.map((crumb, i) => ({
      "@type": "ListItem",
      position: i + 1,
      name: crumb.label,
      item: crumb.url,
    })),
  };
}

export default function BlogPost({ params }) {
  const crumbs = [
    { label: "Home", url: "https://example.com" },
    { label: "Blog", url: "https://example.com/blog" },
    { label: "My Post", url: `https://example.com/blog/${params.slug}` },
  ];

  const schema = buildBreadcrumbSchema(crumbs);

  return (
    <>
      <Head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
        />
      </Head>
      <article>{/* your content */}</article>
    </>
  );
}

Now the schema is part of the server-rendered HTML Google sees it immediately, no JavaScript execution required.

Using a Library: When Manual Gets Tedious #

The manual approach is fine for one or two page types. But if your app has ten different page templates (product pages, category pages, blog posts, docs), you'll end up copy-pasting and slightly mangling that schema builder everywhere. That's where a dedicated library helps.

I tried a few options and ended up using @power-seo because it handles breadcrumb schema, OpenGraph, and canonical tags from a single config object which matched how I was already thinking about per-page SEO.

npm install @power-seo
js
import { SEO } from "@power-seo";

export default function BlogPost({ post }) {
  return (
    <>
      <SEO
        breadcrumbs={[
          { label: "Home", url: "https://example.com" },
          { label: "Blog", url: "https://example.com/blog" },
          { label: post.title, url: `https://example.com/blog/${post.slug}` },
        ]}
        title={post.title}
        canonical={`https://example.com/blog/${post.slug}`}
      />
      <article>{/* your content */}</article>
    </>
  );
}

It generates the same JSON-LD output as the manual approach no magic, just less repetition. Whether that's worth a dependency is your call. For small projects, the hook above is plenty.

What I Learned #

Your visible breadcrumb UI and your breadcrumb schema are two separate things. One is for users, one is for search engines. Both need to exist. - , but for SSR/SSG (Next.js, Remix), render theuseEffect

-injected schema works for CSR apps<script>

tag server-side inside<head>

to guarantee it's in the initial HTML. - Always validate with Google's Rich Results Test(search.google.com/test/rich-results

) before assuming it works. It'll show you exactly what schema Google can parse. - Keep your schema in sync with your actual URLs. Stale or mismatcheditem

values in your schema are a silent SEO killer Google will ignore the breadcrumb entirely if the URLs don't resolve.

If you want to see how the full implementation looks in a real Next.js blog (including dynamic route handling), here's a detailed walkthrough: https://ccbd.dev/blog/how-to-implement-breadcrumb-schema-in-react-using-power-seo

What's your setup? #

Are you handling structured data manually, using a library, or just skipping it entirely and hoping for the best? I'm curious how other React devs are solving this especially on large apps with lots of page templates. Drop your approach in the comments. If you've found a cleaner pattern than what I've shown here, I genuinely want to know.

── more in #developer-tools 4 stories · sorted by recency
── more on @google 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/why-google-can-t-see…] indexed:0 read:5min 2026-05-23 ·