SEO
Add JSON-LD Article Markup to a Next.js Tech Blog
Emit BlogPosting and BreadcrumbList JSON-LD from a Server Component so article pages describe themselves to search engines.
- SEO
- JSON-LD
- Next.js
- Metadata
On this page
Search engines read your visible page, and they also read structured data when you give them a clear description of the same page. JSON-LD is that description: a script tag whose type is application/ld+json and whose body is a BlogPosting, a BreadcrumbList, or both. It does not replace a title, a useful article, or a canonical URL. It tells a crawler which string is the headline, which URL is the page, and which image belongs to the piece.
The data should be built on the server from the same object that renders the article. A client component that fetches a second copy will drift. If the headline in the JSON does not match the h1, you have two sources of truth and one of them is wrong. Generate both from the post record.
Fields worth sending
A BlogPosting that helps is short. Include the headline, the description, the canonical URL, the dates, the language, and an image URL that returns an image. Name the author and the publisher. Add a BreadcrumbList if the visible page shows breadcrumbs, and use the same labels. Skip aggregateRating unless you actually show reviews. Invented ratings are a policy problem, not an SEO trick.
| Property | Use | Skip when |
|---|---|---|
| headline | The same string as the h1 | You would have to shorten it differently |
| image | An absolute URL of the cover | The cover is decorative or missing |
| dateModified | The date the article last changed | You only have a publish date, then repeat datePublished |
| author | A person or organization you can stand behind | The byline is missing on the page |
Render it from the server
A tiny component that sets dangerouslySetInnerHTML is enough. JSON.stringify will escape angles that could break out of the script tag if you replace < with the unicode escape. Build absolute URLs with your real site origin. A relative image path is easy for a browser and ambiguous for a crawler that stores the JSON away from the page.
export function JsonLd({ data }: { data: unknown }) {
const json = JSON.stringify(data).replace(/</g, "\\u003c");
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: json }}
/>
);
}
type ArticleInput = {
title: string;
description: string;
url: string;
image: string;
datePublished: string;
};
export function articleJsonLd(post: ArticleInput) {
return {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.datePublished,
dateModified: post.datePublished,
mainEntityOfPage: post.url,
image: post.image,
author: { "@type": "Organization", name: "CodeAndBuild", url: "https://codeandbuild.tech" },
publisher: { "@type": "Organization", name: "CodeAndBuild", url: "https://codeandbuild.tech" },
};
}
Place the component in the article page, not in the root layout. Layout JSON would describe every URL as the same article. The page already knows the slug, the title, and the cover. Pass those in. If a post has no cover, omit image instead of pointing at the logo and calling it the article image.
Check the rendered HTML
- 01
View the page source
Search for application/ld+json. You want one BlogPosting for the article you opened, not a copy left over from a shared layout.
- 02
Compare three strings
The headline, the h1, and the title tag should describe the same piece. They do not have to be character-identical if the title tag adds the site name.
- 03
Open the image URL
Paste the image value into a browser. A 404 image in JSON-LD is a broken fact, even if the page looks fine because of a different img tag.
- 04
Validate, then wait
Use Google's Rich Results Test on the canonical URL after deploy. Structured data is a hint. It does not guarantee a rich result.
What not to mark up
- Content that is not visible on the page. The JSON should describe the article the person can read.
- A FAQ block you added only for schema and hid with CSS.
- Dates in the future used as a freshness trick. Use the real publish date.
- A different canonical in the JSON than in the link rel=canonical tag.
On a developer blog the highest-leverage markup is the article itself plus breadcrumbs. Organization and WebSite can live on the homepage. Article pages stay specific. When you add a field, add it because a human can see the matching fact, not because a checklist said the property exists.
Breadcrumbs should match the links
A BreadcrumbList is worth adding when the page already shows the trail. Use the same three hops a person can click: Home, Guides, and the article. Each item needs a position, a name, and an absolute item URL. Do not include a crumb that is not a link, and do not include the query string you use for previews. If the visible trail says Guides and the JSON says Blog, you have two sites. Build the list from the same array that renders the nav, then append the current title. That way a rename of the section updates both. After deploy, view source and count the script tags. One article and one breadcrumb list is the goal. A second BlogPosting left in a shared layout will make every URL look like the homepage article, which is worse than shipping no JSON-LD at all.
