Next.js has a reputation as an SEO-friendly framework, and it earns it. But "I use Next.js, so my SEO is good" is a bit like "I have a gym membership, so I'm fit." The framework does a lot of the heavy lifting, but only if you actually use it the way it's meant to be used.
Everything below is what we did on the AppDuce site (yes, the one you're reading right now), not a list of things you're theoretically supposed to do.
Why is Next.js good for SEO?
The short version: it can render on the server.
A traditional React app renders in the browser. The page loads, the browser gets a nearly empty HTML shell, JavaScript runs, and only then does the content appear. Google can follow that, but there's a delay, and some content slips through the cracks.
Next.js gives you three rendering strategies instead:
- SSG (Static Site Generation): the HTML is built once at build time. It's the fastest option, and a good fit for blog posts and service pages.
- SSR (Server-Side Rendering): the server builds the HTML fresh on every request. Use it for content that changes per visit or per user.
- ISR (Incremental Static Regeneration): static generation plus a periodic refresh. For most sites this is the balance you actually want.
Either way, you're handing Google real HTML instead of asking it to wait around for JavaScript to run. That's a genuine advantage.
Metadata API: titles and descriptions
The Metadata API in Next.js 15 is where most of your on-page SEO lives. You can define custom metadata per page:
export async function generateMetadata({ params }) {
return {
title: "Page Title, Brand",
description: "150-160 character description",
openGraph: { ... },
twitter: { ... },
alternates: { canonical: "..." },
};
}
A few things worth getting right:
Title tag. Keep it to 50-60 characters, primary keyword first, brand name last. A title template keeps things consistent across the site:
// layout.tsx
title: {
default: "AppDuce",
template: "%s | AppDuce"
}
Meta description. 150-160 characters, saying plainly what the page is about, ideally with a reason to click. Write a fresh one for every page.
Canonical URL. Every page needs one official URL so Google doesn't treat near-identical pages as duplicates.
Structured data (JSON-LD)
Google reads structured data to make sense of your content. In Next.js you add it as JSON-LD:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
author: { "@type": "Person", name: post.author },
datePublished: post.date,
dateModified: post.dateModified,
})
}}
/>
Which schema goes with which page:
| Page Type | Schema Type |
|---|---|
| Homepage | Organization, WebSite |
| Blog listing | BreadcrumbList |
| Blog post | Article, BreadcrumbList |
| Service page | Service, BreadcrumbList, FAQPage |
| Contact | ContactPage, BreadcrumbList |
| About | AboutPage, BreadcrumbList |
Match each page type to the schema that describes it best, then run every template through Google's Rich Results Test before you ship it.
Sitemap and robots
Next.js 15 lets you build a dynamic sitemap from a sitemap.ts file:
export default async function sitemap() {
const posts = getAllPosts("en");
const blogEntries = posts.map((post) => ({
url: `https://appduce.com/en/blog/${post.slug}`,
lastModified: new Date(post.frontmatter.date),
alternates: {
languages: {
en: `https://appduce.com/en/blog/${post.slug}`,
tr: `https://appduce.com/tr/blog/${post.alternateSlug}`,
},
},
}));
return [...staticPages, ...blogEntries];
}
And you control what gets indexed with robots.ts:
export default function robots() {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: "https://appduce.com/sitemap.xml",
};
}
Multilingual SEO (hreflang)
If you publish in more than one language, hreflang isn't optional. It's how you tell Google "the Turkish version of this page lives here, the English one over there."
In Next.js you set it in two places:
- Metadata alternates:
alternates: {
languages: {
tr: "https://appduce.com/tr/blog/post-slug-tr",
en: "https://appduce.com/en/blog/post-slug-en",
}
}
- Sitemap alternates: As shown in the sitemap example above.
One thing people miss: both language versions have to point at each other. TR to EN and EN back to TR. Google ignores one-way hreflang entirely.
Image optimization
The Next.js Image component handles optimization for you:
import Image from "next/image";
<Image
src="/images/hero.webp"
alt="Descriptive alt text"
width={800}
height={450}
priority // for above-the-fold images
/>
The parts that matter for SEO:
- Write descriptive alt text for every image
- Add priority to anything above the fold
- Serve WebP
- Load images at the size they'll actually display (don't push a 3000px file into a 300px slot)
Core Web Vitals
Google treats Core Web Vitals as a ranking signal:
LCP (Largest Contentful Paint), under 2.5s. How long the largest element takes to load. SSG or ISR keeps it low; preload your fonts and mark hero images as priority.
INP (Interaction to Next Paint), under 200ms. How quickly the page responds when someone interacts with it. Heavy JavaScript hurts here, so keep client components to a minimum.
CLS (Cumulative Layout Shift), under 0.1. How much the layout jumps around while loading. Give images explicit width and height, and set a font loading strategy (font-display: swap).
Internal linking strategy
The easiest part of technical SEO, and the one most people ignore:
- Link each blog post to related posts
- Drop natural links from posts to your service pages
- Don't leave orphan pages with nothing linking to them
- Make anchor text describe the destination ("mobile app development with React Native" beats "click here")
Common mistakes
Canonical issues with client-side routing. If you're using next-intl or a similar i18n library, make sure your canonical URLs include the locale prefix.
Missing dynamic OG images. When nothing shows up on a social share, click-through drops. Generate OG images with next/og.
Unnecessary pages in the sitemap. Leave out 404, login, and admin pages.
Duplicate meta descriptions. Every page gets its own. No copy-paste.
Wrapping up
Next.js hands you a solid SEO foundation, but you still have to do your half. Metadata, structured data, sitemap, hreflang, image optimization, Core Web Vitals: each one wants its own attention.
The upside is that once it's set up properly, it turns into a template. Wiring up the SEO infrastructure for this site took us about a week, and now every new blog post meets the requirements on its own.
We got this right on our own site, the one you're reading now, and we're glad to share how it's done.