Duplicate schema blocks, wrong canonical references, and schema that contradicts your meta tags. Here is what to check.
Structured data is one of those SEO things that is easy to get mostly right and hard to get fully right. The mistakes are invisible unless you are actively looking for them, and some of them actively confuse Google's understanding of your pages.
Here are the issues we found on our own properties and how we fixed them.
Duplicate schema blocks
The most common issue with SPAs. You define your JSON-LD in a script tag in the React component, which is fine. But if you also added schema in your index.html template (maybe following an old tutorial), and you are prerendering, you end up with two schema blocks describing the same page. Sometimes with conflicting information.
Check for duplicates with:
grep -c '"@type"' dist/index.htmlShould be 1 per schema type you intend to have. Anything higher means duplicates.
The fix in React is to use a useEffect that explicitly removes any existing schema before injecting the new one:
export function JsonLd({ schema }: { schema: object }) {
useEffect(() => {
// Remove any existing schema tags
document.querySelectorAll('script[type="application/ld+json"]').forEach((el) => {
el.remove();
});
const script = document.createElement("script");
script.type = "application/ld+json";
script.textContent = JSON.stringify(schema);
document.head.appendChild(script);
return () => script.remove();
}, [schema]);
return null;
}Schema URL not matching canonical
Your schema's url field and your canonical meta tag should always point to the same URL. If they differ, Google can get confused about which URL to index. A common mismatch is one using a trailing slash and the other not, or one using www and the other not.
// Bad: these do not match
const schema = { "@type": "WebPage", url: "https://execross.com/blog/post" };
// canonical: https://execross.com/blog/post/ <-- trailing slash
// Good: always derive both from the same source
const canonicalUrl = `https://execross.com${route.path}`;
const schema = { "@type": "WebPage", url: canonicalUrl };WebSite schema on every page
WebSite schema (which enables the sitelinks search box) should only be on the homepage. Putting it on every page is technically valid but looks messy in Google's eyes and adds unnecessary bytes to every page response.
datePublished and dateModified on blog posts
If you have a blog, your Article schema should always have both datePublished and dateModified. Missing dateModified means Google cannot tell if your content is being maintained, which can hurt rankings for competitive queries.
const articleSchema = {
"@context": "https://schema.org",
"@type": "TechArticle",
headline: post.title,
description: post.excerpt,
datePublished: post.publishedAt,
dateModified: post.updatedAt ?? post.publishedAt,
author: {
"@type": "Organization",
name: "Execross",
url: "https://execross.com",
},
publisher: {
"@type": "Organization",
name: "Execross",
},
};Validate after every deploy
Use the Google Rich Results Test after any schema changes. It catches things that the manual review misses. We also have a test in our build pipeline that parses the JSON-LD from the prerendered HTML and validates that required fields are present.