When I launched this Next.js website, I thought I had covered all the SEO basics: meta titles, descriptions, clean URLs, and a proper sitemap. Google's Lighthouse gave me a solid 95/100 SEO score.
But 95 isn't 100.
Those missing 5 points represented real opportunities to help search engines understand my content better—and to make my pages stand out in search results with rich snippets, knowledge panels, and enhanced listings.
Here's exactly how I took this website from 95/100 to a perfect SEO score, what I learned along the way, and how you can do the same for your site.
The Starting Point: What Was Missing?
Before diving into the fixes, I ran a comprehensive SEO audit to identify the gaps:
What Was Working (95/100):
- ✅ Meta titles and descriptions on all pages
- ✅ Twitter Cards implemented
- ✅ Canonical URLs to prevent duplicate content
- ✅ Sitemap and RSS feed
- ✅ Mobile responsive design
- ✅ Fast load times (Next.js 14 App Router)
What Was Missing (The Final 5 Points):
- ❌ OpenGraph images (og:image) - 0% coverage
- ❌ Complete OpenGraph metadata (og:url, og:type)
- ❌ Article tags for better categorization
- ❌ Enhanced author schema with social profiles
- ❌ Comprehensive organization and product schemas
The gap was clear: I had basic SEO, but I was missing the structured data that helps search engines truly understand my content and display rich results.
Step 1: Implementing Dynamic OpenGraph Images (98/100)
OpenGraph images are crucial for social sharing. When someone shares your content on Facebook, LinkedIn, or Twitter, the OG image is what appears in the preview card.
The Problem
I had a static logo, but 256x256 pixels is too small for OpenGraph (recommended: 1200x630). Creating individual images for 20+ pages would be tedious and hard to maintain.
The Solution: @vercel/og
Vercel's @vercel/og package lets you generate OpenGraph images dynamically using React components and Edge Runtime. It's fast, scalable, and maintainable.
Installation:
npm install @vercel/og
Creating the API Route (src/app/api/og/route.tsx):
import { ImageResponse } from '@vercel/og'
import { NextRequest } from 'next/server'
export const runtime = 'edge'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') || 'KuduTek'
const description = searchParams.get('description') ||
'Shipping Ideas. Sharing the Journey.'
const type = searchParams.get('type') || 'default'
let badge = ''
if (type === 'article') badge = 'Blog Post'
else if (type === 'blog') badge = 'Blog'
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1a1a1a',
padding: '80px',
}}
>
{badge && (
<div style={{
fontSize: 28,
color: '#4a9d6f',
marginBottom: 20,
}}>
{badge}
</div>
)}
<div style={{
fontSize: 72,
fontWeight: 'bold',
color: '#faf8f5',
textAlign: 'center',
marginBottom: 30,
}}>
{title}
</div>
<div style={{
fontSize: 32,
color: '#b0b0b0',
textAlign: 'center',
maxWidth: '80%',
}}>
{description}
</div>
<div style={{
position: 'absolute',
bottom: 40,
fontSize: 24,
color: '#4a9d6f',
}}>
KuduTek.com
</div>
</div>
),
{
width: 1200,
height: 630,
}
)
} catch (e: any) {
return new Response(`Failed to generate image`, { status: 500 })
}
}
Adding to Pages:
export const metadata: Metadata = {
openGraph: {
title: 'Your Page Title',
description: 'Your description',
url: '/your-page',
type: 'website',
images: [
{
url: `/api/og?title=${encodeURIComponent('Your Title')}&description=${encodeURIComponent('Your description')}&type=article`,
width: 1200,
height: 630,
alt: 'Your Page Title',
},
],
},
}
Result: All pages now have unique, branded OpenGraph images. SEO score: 98/100.
Step 2: Article Tags & Keywords (99/100)
Search engines use tags and keywords to understand what your content is about and match it to relevant queries.
Implementation
1. Update Your Data Schema:
// src/lib/blog.ts
export interface BlogPost {
slug: string
title: string
date: string
excerpt: string
category: string
subcategory?: string
author?: string
featured?: boolean
tags?: string[] // Add this
content: string
}
2. Display Tags in Your Layout:
// src/components/blog/BlogPostLayout.tsx
{tags.length > 0 && (
<div className="post-tags">
{tags.map((tag) => (
<span key={tag} className="post-tag">
{tag}
</span>
))}
</div>
)}
3. Add to Structured Data:
const structuredData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: title,
keywords: tags.length > 0 ? tags.join(', ') : undefined,
// ... other fields
}
4. Add Tags to Your Posts:
---
title: "Your Post Title"
tags: ["Next.js", "SEO", "structured data", "web development"]
---
Styling Tags:
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 24px;
}
.post-tag {
display: inline-block;
padding: 7px 16px;
background: var(--color-bg-secondary);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
border-radius: 20px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.post-tag:hover {
background: var(--color-accent);
color: white;
border-color: var(--color-accent);
}
Step 3: Enhanced Author Schema (100/100)
Google uses author information to build trust and authority. Enhanced author schema includes social profiles that help Google verify authorship.
Before:
{
"author": {
"@type": "Person",
"name": "Jon Muller"
}
}
After:
{
"author": {
"@type": "Person",
"name": "Jon Muller",
"url": "https://kudutek.com/about",
"sameAs": [
"https://www.linkedin.com/in/jon-muller-kudutek",
"https://github.com/kudutek",
"https://twitter.com/kudutek"
]
}
}
This simple change tells Google:
- Who the author is
- Where to find their profile
- How to verify their identity across platforms
Step 4: Organization & Product Schemas
For business websites, Organization and Product schemas are essential for appearing in Google's Knowledge Graph and product search results.
Organization Schema
Add this to your root layout.tsx:
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'KuduTek',
legalName: 'KuduTek',
url: 'https://kudutek.com',
logo: {
'@type': 'ImageObject',
url: 'https://kudutek.com/images/logos/kudutek-logo.png',
width: 512,
height: 512,
},
founder: {
'@type': 'Person',
name: 'Jon Muller',
url: 'https://kudutek.com/about',
sameAs: [
'https://www.linkedin.com/in/jon-muller-kudutek',
'https://github.com/kudutek',
'https://twitter.com/kudutek',
],
},
foundingDate: '2018',
address: {
'@type': 'PostalAddress',
streetAddress: '4850 Sugarloaf Pkwy, Suite 209-194',
addressLocality: 'Lawrenceville',
addressRegion: 'GA',
postalCode: '30044',
addressCountry: 'US',
},
sameAs: [
'https://www.linkedin.com/company/kudutek',
'https://github.com/kudutek',
'https://twitter.com/kudutek',
],
}
Product Schema (SoftwareApplication)
For product pages:
const softwareSchema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'XLNavigator',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Windows',
softwareVersion: '2.0',
description: 'Professional Excel add-in featuring vertical tabs...',
offers: {
'@type': 'Offer',
price: '29.99',
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
priceValidUntil: '2025-12-31',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '150',
bestRating: '5',
},
}
Step 5: Creating Reusable FAQ Schema
FAQ schema generates those expandable Q&A boxes in Google search results. I created a reusable component:
// src/components/blog/FAQSchema.tsx
'use client'
export interface FAQItem {
question: string
answer: string
}
interface FAQSchemaProps {
faqs: FAQItem[]
}
export function FAQSchema({ faqs }: FAQSchemaProps) {
const structuredData = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
)
}
Usage in MDX:
<FAQSchema faqs={[
{
question: "What is structured data?",
answer: "Structured data is code that helps search engines understand your content better and display rich results in search."
},
{
question: "Do I need structured data for SEO?",
answer: "While not required, structured data significantly improves how your content appears in search results and can increase click-through rates."
}
]} />
Testing Your Implementation
After implementing all these changes, validate your structured data:
1. Rich Results Test
Visit: https://search.google.com/test/rich-results
Paste your page URL or HTML to see what rich results Google can extract.
2. Schema.org Validator
Visit: https://validator.schema.org/
Validates your JSON-LD syntax and checks for errors.
3. Google Search Console
Monitor the "Enhancements" section to see:
- Product rich results
- FAQ rich results
- Breadcrumb navigation
- Organization info
4. Lighthouse SEO Audit
Run in Chrome DevTools:
DevTools → Lighthouse → SEO → Generate report
The Results: 100/100 SEO Score
Here's what the journey looked like:
Starting Point (95/100):
- Basic meta tags ✅
- Canonical URLs ✅
- Sitemap ✅
- Missing: OG images, complete schemas
After OpenGraph (98/100):
- Dynamic OG image generation ✅
- Complete OG metadata on all pages ✅
- Missing: Tags, enhanced schemas
Final Implementation (100/100):
- Article tags with keywords ✅
- Enhanced author schema ✅
- Organization schema ✅
- Product schema ✅
- FAQ schema component ✅
What This Means for Search Rankings
Perfect SEO score doesn't guarantee #1 rankings, but it ensures:
- Better Click-Through Rates: Rich results with images, ratings, and FAQs stand out in search
- Improved Indexing: Search engines understand your content structure
- Knowledge Graph Eligibility: Your organization can appear in Google's knowledge panel
- Social Sharing: Professional OG images increase engagement when shared
- Authority Signals: Author verification builds trust with search engines
Key Takeaways
- Start with the basics: Meta tags, canonical URLs, sitemap
- Add OpenGraph: Use dynamic generation for scalability
- Implement structured data: Organization, Product, Author, FAQ schemas
- Use tags strategically: Help search engines categorize your content
- Test thoroughly: Use Google's tools to validate implementation
- Monitor results: Check Search Console for rich result performance
The Code
All the code from this article is available on this website. Inspect the page source to see the structured data in action:
<!-- Right-click → View Page Source → Search for "application/ld+json" -->
You'll find:
- BlogPosting schema with author
- BreadcrumbList for navigation
- Keywords from tags
- Complete metadata implementation
Next Steps
After achieving 100/100 SEO:
- Monitor Search Console for impressions and clicks
- Track rich result performance
- Add structured data to more pages
- Consider adding Review schema for testimonials
- Implement Event schema for webinars or launches
Perfect SEO is achievable—it just requires attention to detail and the right structured data. The tools are free, the documentation is comprehensive, and the impact on search visibility is worth the effort.
Now it's your turn. What's your current SEO score, and what's stopping you from reaching 100?
Want to see this implementation in action? Check out the page source of this article or visit the blog homepage to see rich snippets in practice.