SEO

Technical SEO for Next.js Apps: The Complete 2025 Guide

Next.js gives you an incredible SEO foundation out of the box. But "out of the box" gets you 60% of the way there. This guide covers the remaining 40% — the details that separate ranking pages from invisible ones.

Ali Shams

Ali Shams

Business & Marketing Lead

March 10, 2025
15 min read
Technical SEO for Next.js Apps: The Complete 2025 Guide

Why Next.js Is an SEO Powerhouse (When Used Correctly)

Next.js App Router gives you server-side rendering by default, which means Google can crawl your content without executing JavaScript. Combined with the Metadata API, you have a best-in-class SEO foundation.

But most Next.js apps I audit are leaving significant SEO gains on the table.

1. The Metadata API — Do It Right

// app/layout.tsx — site-wide defaults
export const metadata: Metadata = {
  metadataBase: new URL('https://yoursite.com'),
  title: {
    template: '%s | Your Brand',
    default: 'Your Brand — Tagline',
  },
  description: 'Your site description (150-160 chars)',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://yoursite.com',
    siteName: 'Your Brand',
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@yourhandle',
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
}

The metadataBase is critical. Without it, relative URLs in OG images won't resolve correctly.

2. Dynamic Metadata for Blog Posts

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.seo_title || post.title,
    description: post.seo_description || post.excerpt,
    alternates: {
      canonical: `/blog/${post.slug}`, // Prevents duplicate content
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.published_at,
      authors: [post.author.name],
      images: [{
        url: post.cover_image,
        width: 1200,
        height: 630,
        alt: post.title,
      }],
    },
  }
}

3. JSON-LD Schema Markup

Schema markup helps Google understand your content and can trigger rich results (star ratings, breadcrumbs, FAQs).

Organization Schema (homepage):

const schema = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'NineLab',
  url: 'https://ninelab.ir',
  logo: 'https://ninelab.ir/assets/logo/icon.png',
  contactPoint: {
    '@type': 'ContactPoint',
    email: 'info@ninelab.ir',
    contactType: 'customer service',
  },
  sameAs: [
    'https://instagram.com/ninelab.ir',
    'https://linkedin.com/in/ninelab.ir',
  ],
}

// In your page component:
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>

Article Schema (blog posts):

const articleSchema = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: post.title,
  description: post.excerpt,
  image: post.cover_image,
  datePublished: post.published_at,
  dateModified: post.updated_at,
  author: {
    '@type': 'Person',
    name: post.author.name,
    url: `https://ninelab.ir/team/${post.author.slug}`,
  },
  publisher: {
    '@type': 'Organization',
    name: 'NineLab',
    logo: 'https://ninelab.ir/assets/logo/icon.png',
  },
}

4. Dynamic Sitemap

// app/sitemap.ts
import { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()

  const blogUrls = posts.map(post => ({
    url: `https://ninelab.ir/blog/${post.slug}`,
    lastModified: new Date(post.updated_at),
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }))

  return [
    { url: 'https://ninelab.ir', lastModified: new Date(), priority: 1.0 },
    { url: 'https://ninelab.ir/blog', lastModified: new Date(), priority: 0.9 },
    ...blogUrls,
  ]
}

5. The robots.txt

// app/robots.ts
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/dashboard/', '/api/', '/login'],
      },
    ],
    sitemap: 'https://ninelab.ir/sitemap.xml',
  }
}

The SEO Audit Checklist

Run through this after every launch:

  • [ ] `metadataBase` set in root layout
  • [ ] Unique title and description on every page
  • [ ] Canonical URLs on all pages
  • [ ] OG image (1200×630) for all key pages
  • [ ] JSON-LD schema on homepage + blog posts
  • [ ] Sitemap accessible at /sitemap.xml
  • [ ] robots.txt accessible at /robots.txt
  • [ ] No orphan pages (every page linked from somewhere)
  • [ ] Core Web Vitals all green in PageSpeed Insights
  • [ ] Google Search Console verified and sitemap submitted
  • SEONext.jsTechnical SEOMetadataSchema
    Ali Shams

    Written by

    Ali Shams

    Business & Marketing Lead at NineLab

    Work with us →