Part 3 of a series on practical SEO for developers where i cover How to get your site indexed, On-Page SEO, and Controling Your Search Appearance
There’s a particular kind of frustration that comes from searching for your own site and finding that something looks subtly wrong. Maybe your site name is showing up differently than you expected. Maybe the search result is just a blue link and a description, while similar pages have author names, publication dates, or article labels. Maybe the preview card when you share a post on social media is pulling the wrong image, or no image at all.
These aren’t bugs in the traditional sense — your site is working, your pages are loading, your content is correct. What’s off is the presentation layer that sits between your HTML and how Google (or Twitter, or Slack) chooses to display your content. That layer is controlled by two things: structured data in the form of JSON-LD, and Open Graph meta tags. Together, they don’t just make your site look better in results — they close the gap between what you intend and what Google decides to do on its own when you leave things unspecified.
This is the final post in the series, and it’s where the three pieces — discoverability, on-page signals, and presentation — come together into something coherent.
Why Your Site Name Might Be Appearing Wrong
One of the more confusing things you can discover in Search Console is that Google is displaying your site’s name incorrectly in search results. You might see your domain name instead of your site’s title, an old name from before a rebrand, or some variation that doesn’t match what you intended.
The reason this happens is that Google doesn’t read your site name from a single authoritative source. It infers it from multiple signals — your <title> tag, your og:site_name Open Graph property, your JSON-LD WebSite schema’s name field, and sometimes even your domain name as a fallback. When these sources agree, Google displays your site name confidently and consistently. When they disagree — even slightly — Google has to make a judgment call, and it might not make the one you’d want.
The fix is to make all three sources say exactly the same thing. That sounds simple, but it’s easy for them to drift apart. Your <title> tags on individual pages might say “Post Title | My Engineering Blog” while your Open Graph og:site_name says “mobius.dev” and your JSON-LD says “Mobius Engineering Blog.” From Google’s perspective, these are three different candidates for the same field, and none of them clearly wins. Settling on one canonical name and making sure it appears identically everywhere is what resolves this.
In Next.js, you set the Open Graph site name globally in your root layout so it’s inherited by every page. Your JSON-LD WebSite schema, which we’ll walk through shortly, declares it as the name property. And your title template from the previous post already has the brand name baked in. When all three are in sync, Google’s decision becomes straightforward.
What JSON-LD Is and Why It Exists
To understand JSON-LD, it helps to think about the problem it’s solving. Google reads your HTML to understand your content, but HTML was designed to describe presentation, not meaning. A heading tag tells Google that some text is important, but it doesn’t tell Google whether that text is an article title, a product name, a person’s name, or a section header. A paragraph of text might contain a publication date, a price, a location, or a piece of narrative — the HTML alone doesn’t distinguish between them.
JSON-LD (JavaScript Object Notation for Linked Data) is a way of providing that meaning explicitly, in a separate, structured format that doesn’t interfere with your visible HTML. You embed it in a <script type="application/ld+json"> tag in your page’s <head>, and Google reads it alongside your HTML to build a richer understanding of what your page represents.
What Google does with that understanding is generate rich results — the enhanced search listings that include structured information directly in the result. An article with proper JSON-LD might show the publication date and author’s name beneath the link. A recipe might show ratings and cooking time. A product might show price and availability. These enhancements aren’t guaranteed — Google decides whether to show them based on its own assessment of the data’s quality and relevance — but providing correct, complete JSON-LD is the prerequisite for them appearing at all.
The schema vocabulary that JSON-LD uses comes from schema.org, a collaborative project backed by Google, Bing, Yahoo, and Yandex. There are hundreds of schema types, but you only need a small handful for a developer blog.
The Four Schemas That Matter for a Developer Blog
Rather than surveying all of schema.org, let’s focus on the types that are directly applicable to your site and that actually influence how search results look.
WebSite
The WebSite schema lives at the top level — it describes your site as a whole, not any individual page. This is where you establish your site name authoritatively, and where you can optionally declare a SearchAction that enables a sitelinks search box in Google results (the small search field that sometimes appears beneath the main result for well-known sites). For a newer blog, the sitelinks box is unlikely to appear yet, but setting it up correctly costs nothing and positions you for it later.
You place the WebSite schema in your root layout so it appears on every page:
// app/layout.tsx
// Define it as a plain object — Next.js will serialize it into the script tag
const websiteSchema = {
"@context": "https://schema.org",
"@type": "WebSite",
// This name must match your og:site_name and your title template brand name
name: "Your Blog Name",
url: "https://yourdomain.com",
// The SearchAction is optional, but harmless to include
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: "https://yourdomain.com/search?q={search_term_string}",
},
"query-input": "required name=search_term_string",
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* Embed the schema as a script tag in the document head */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
</head>
<body>{children}</body>
</html>
);
}
Person
The Person schema describes you — the author and owner of the blog. Including it creates a named entity that Google can associate with your content, which is part of how Google builds an understanding of authorship and expertise over time. Place this in your root layout alongside the WebSite schema, since you are the author of everything on the site:
// Add this alongside websiteSchema in app/layout.tsx
const personSchema = {
"@context": "https://schema.org",
"@type": "Person",
// Use your real name or the name you publish under consistently
name: "Your Name",
url: "https://yourdomain.com/about",
// Link to your professional profiles — these help Google verify the entity
sameAs: [
"https://github.com/yourusername",
"https://twitter.com/yourhandle",
"https://linkedin.com/in/yourprofile",
],
};
The sameAs array is worth taking seriously. By linking to your profiles on established platforms, you’re giving Google corroborating sources that confirm this Person entity is real and corresponds to accounts with existing activity. It’s one of the cleaner ways to start building what SEO practitioners call “entity authority” — Google’s confidence that your name refers to a real, consistent presence on the web.
BlogPosting
The BlogPosting schema is the most impactful for search results because it’s the one that produces visible enhancements — specifically, the article date and sometimes the author name that appear beneath the title in search listings. This schema is page-specific, so it belongs in your dynamic blog post route, generated from the actual post data:
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await fetch(
`https://your-django-api.com/api/posts/${params.slug}/`
).then((res) => res.json());
const blogPostSchema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
// The headline should match your <h1> and title tag
headline: post.title,
description: post.seo_description || post.excerpt,
// The canonical URL for this specific post
url: `https://yourdomain.com/blog/${post.slug}`,
datePublished: post.published_at,
// dateModified tells Google your content is being maintained
dateModified: post.updated_at,
author: {
"@type": "Person",
name: "Your Name",
url: "https://yourdomain.com/about",
},
publisher: {
"@type": "Person",
name: "Your Name",
url: "https://yourdomain.com",
},
// If you have a cover image, include it — Google may show it in results
...(post.cover_image && {
image: {
"@type": "ImageObject",
url: post.cover_image,
},
}),
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogPostSchema) }}
/>
{/* Rest of your page component */}
</>
);
}
A few details worth explaining here. The dateModified field matters more than it might seem — it signals to Google that you’re actively maintaining your content, which is a quality signal for older posts. The spread syntax for image is just a clean way to conditionally include that field only when a cover image actually exists, avoiding a null value in the schema that could confuse validators. And the publisher using a Person type (rather than an Organization) is appropriate for a personal blog — Google expects Organization for company publications, but Person is correct for individually-authored sites.
BreadcrumbList
Breadcrumbs are the navigational trail that shows where a page sits within your site’s hierarchy — something like “Home > Blog > Post Title.” When you declare a BreadcrumbList schema, Google can display this trail directly in the search result beneath the URL, giving users context about your site’s structure before they even click.
For a simple blog, this is straightforward:
// Add this inside BlogPostPage alongside blogPostSchema
const breadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: "https://yourdomain.com",
},
{
"@type": "ListItem",
position: 2,
name: "Blog",
item: "https://yourdomain.com/blog",
},
{
"@type": "ListItem",
position: 3,
// The final item is the current page
name: post.title,
item: `https://yourdomain.com/blog/${post.slug}`,
},
],
};
You can either embed this in the same <script> tag as the BlogPosting schema (as a JSON array) or use a separate tag for each schema — both work, and the choice is mostly organizational preference. Keeping them separate makes the code easier to read and maintain.
Open Graph: SEO’s Social Sibling
Open Graph tags don’t affect Google’s ranking algorithm directly, but they control something that matters quite a lot for a developer blog: how your content looks when someone shares it on Twitter/X, LinkedIn, or in a Slack channel. A post shared with a clean title, an accurate description, and a relevant image gets clicked significantly more often than a link that renders as bare URL text or pulls in the wrong thumbnail. For developer content, where a lot of discovery happens through social sharing and community links, this is real traffic.
Open Graph was originally developed by Facebook and has since been adopted by essentially every major platform. The core tags you need for a blog post are these five:
// Inside generateMetadata in app/blog/[slug]/page.tsx
return {
title: `${post.title} | Your Blog Name`,
description: post.seo_description || post.excerpt?.slice(0, 155),
alternates: {
canonical: `https://yourdomain.com/blog/${post.slug}`,
},
openGraph: {
// og:title can differ slightly from <title> — no need to include site name
title: post.title,
description: post.seo_description || post.excerpt?.slice(0, 155),
url: `https://yourdomain.com/blog/${post.slug}`,
// "article" is the correct type for blog posts
type: "article",
// This is the key field for the social preview card
images: post.cover_image
? [
{
url: post.cover_image,
// 1200x630 is the standard for most platforms
width: 1200,
height: 630,
alt: post.title,
},
]
: [],
// These two are specific to the "article" type
publishedTime: post.published_at,
authors: ["https://yourdomain.com/about"],
// This must match your WebSite schema name exactly
siteName: "Your Blog Name",
},
};
The og:image is where most developers run into problems. The image URL must be absolute (including the full https:// protocol and domain), publicly accessible without authentication, and ideally sized at 1200×630 pixels. Since your images are hosted on Cloudinary, this works in your favor — Cloudinary’s URL transformation API lets you generate correctly-sized derivatives on the fly without maintaining separate image versions. For example, appending /w_1200,h_630,c_fill to your Cloudinary URL produces a correctly-cropped version suitable for social sharing.
For pages that don’t have a post-specific cover image, consider creating a default Open Graph image — a simple branded graphic with your blog’s name — and using that as the fallback. A consistent branded card is far better than an empty preview or a randomly chosen inline image.
Validating Everything Before You Publish
After implementing JSON-LD and Open Graph tags, the most important step is verifying that they’re working correctly before your post goes live, because errors in structured data are silent — they don’t break your page, they just mean Google can’t generate rich results from it.
Google’s Rich Results Test (available at search.google.com/test/rich-results) is your primary tool here. Paste in the URL of your published page, or paste in the raw HTML directly for pre-publish testing, and it will tell you which schemas it detected, whether they’re valid, and whether they’re eligible to generate rich results. Pay attention to the distinction between errors (which prevent rich results entirely) and warnings (which may reduce their quality but don’t block them).
For Open Graph validation, Meta’s Sharing Debugger (developers.facebook.com/tools/debug) fetches your page the way Facebook would and shows you exactly what preview card would be generated. It also has a “Scrape Again” button that forces a cache refresh — useful when you’ve updated your tags and want to confirm the changes are live. Most other platforms (LinkedIn, Twitter/X’s Card Validator) have equivalent tools, but Facebook’s debugger tends to catch the most issues since it’s the strictest parser.
Once your pages are indexed, Search Console’s URL Inspection tool will also show you whether Google has detected structured data on a crawled page and whether there are any issues with it. This is the closest thing to seeing Google’s internal view of your page, and it’s worth checking for important posts a week or two after publishing.
One thing to set your expectations on: even after you add valid structured data, rich results don’t appear immediately. Google needs to re-crawl the page, process the schema, and decide whether the data meets its quality threshold for enhanced display. For a new site, this can take anywhere from a few days to a few weeks. The URL Inspection tool’s “Request Indexing” button from the first post is useful here — requesting a re-crawl after adding structured data speeds up the process.
Bringing the Three Posts Together
Looking across all three posts, there’s a through-line worth making explicit. Each layer builds on the one before it in a specific way. Search Console and sitemaps are about getting Google to visit your pages in the first place — they’re the prerequisite for everything else. Title tags, meta descriptions, headings, and canonical URLs are about giving Google clear, unambiguous signals about what each page contains — they determine whether a crawled page gets indexed and what it might rank for. JSON-LD, Open Graph, and consistent site naming are about controlling how your content is presented once it does rank — they determine whether a reader clicks your result over the nine others on the same page.
None of this produces instant results, and none of it substitutes for content that’s genuinely worth reading. But it removes the technical debt that causes new sites to be invisible longer than they need to be, and it closes the gap between what you build and what Google shows. For a developer blog that’s approaching a public launch, getting this infrastructure in place before you have traffic is far easier than trying to retrofit it afterward.
The work is mostly done once. The results compound over time.
