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):

What Was Missing (The Final 5 Points):

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:

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:

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):

After OpenGraph (98/100):

Final Implementation (100/100):

What This Means for Search Rankings

Perfect SEO score doesn't guarantee #1 rankings, but it ensures:

  1. Better Click-Through Rates: Rich results with images, ratings, and FAQs stand out in search
  2. Improved Indexing: Search engines understand your content structure
  3. Knowledge Graph Eligibility: Your organization can appear in Google's knowledge panel
  4. Social Sharing: Professional OG images increase engagement when shared
  5. Authority Signals: Author verification builds trust with search engines

Key Takeaways

  1. Start with the basics: Meta tags, canonical URLs, sitemap
  2. Add OpenGraph: Use dynamic generation for scalability
  3. Implement structured data: Organization, Product, Author, FAQ schemas
  4. Use tags strategically: Help search engines categorize your content
  5. Test thoroughly: Use Google's tools to validate implementation
  6. 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:

Next Steps

After achieving 100/100 SEO:

  1. Monitor Search Console for impressions and clicks
  2. Track rich result performance
  3. Add structured data to more pages
  4. Consider adding Review schema for testimonials
  5. 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.