Next.js Multilingual SEO Optimization: Ensuring Every Language Gets Indexed
Next.js Multilingual SEO Optimization: Ensuring Every Language Gets Indexed
Introduction
Have you ever experienced these frustrating issues: you’ve meticulously crafted a multilingual website, but search engines keep displaying the wrong language version? Users searching for content in Chinese get redirected to English pages? Different language versions compete against each other in search results, leading to lower rankings?
These are all typical problems caused by improper multilingual SEO configuration. According to Google’s statistics, over 60% of multilingual websites have hreflang configuration errors, significantly impacting their internationalization effectiveness.
This article will dive deep into how to properly implement multilingual SEO optimization in Next.js, covering:
- Correct configuration methods for hreflang tags
- Generation strategies for multilingual sitemaps
- Best practices for URL structure
- Troubleshooting and fixing common errors
Whether you’re using Pages Router or App Router, you’ll find the corresponding solutions here.
Introduction
1. Understanding Core Concepts of Multilingual SEO
1.1 What is hreflang
hreflang is an HTML attribute used to tell search engines about a page’s target language and region. Its main purposes are:
- Prevent duplicate content penalties: Tell search engines that different language versions are translations of the same content, not duplicate content
- Precise user matching: Display the most appropriate page version based on users’ language and region settings
- Improve user experience: Prevent users from seeing content in the wrong language
1.2 How Google Handles Multilingual Content
When Google’s crawler visits your multilingual website, it will:
- Detect the page’s language (through HTML lang attribute, hreflang tags, page content)
- Look for hreflang tags to understand relationships between pages
- Display the corresponding version in search results based on users’ language preferences
- Consolidate SEO authority across different language versions (rather than having them compete)
1.3 Common SEO Error Cases
Error 1: Missing hreflang tags
<!-- Wrong: No hreflang tags -->
<head>
<title>My Website</title>
<link rel="canonical" href="https://example.com/en/about" />
</head>
Error 2: Asymmetric hreflang configuration
<!-- English page -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<!-- Chinese page - Wrong: Missing hreflang tags -->
<!-- Must configure complete hreflang in every language version -->
Error 3: Using incorrect language codes
<!-- Wrong: Using non-standard language codes -->
<link rel="alternate" hreflang="cn" href="..." /> <!-- Should be zh -->
<link rel="alternate" hreflang="en-us" href="..." /> <!-- Should be en-US -->
2. URL Strategy Selection
Before implementing a multilingual website, you first need to choose an appropriate URL strategy. This decision will affect SEO, user experience, and technical implementation.
2.1 Comparison of Three Mainstream URL Strategies
| Strategy | Example | SEO Impact | Implementation Difficulty | Recommendation |
|---|---|---|---|---|
| Subdirectory | example.com/en/ example.com/zh/ | ⭐⭐⭐⭐⭐ Best | ⭐⭐⭐ Medium | ⭐⭐⭐⭐⭐ |
| Subdomain | en.example.com zh.example.com | ⭐⭐⭐ Average | ⭐⭐⭐⭐ Complex | ⭐⭐⭐ |
| URL Parameter | example.com?lang=en | ⭐⭐ Poor | ⭐⭐⭐⭐⭐ Simple | ⭐⭐ |
2.2 Detailed Analysis of Each Strategy
Option 1: Subdirectory Strategy (Recommended)
Advantages:
- SEO authority concentrated on the main domain, beneficial for overall ranking
- Simple configuration, no need for additional domain management
- Easy to maintain and expand
- Native Next.js support, simple implementation
Disadvantages:
- All languages share the same domain, cannot do DNS optimization for specific markets
Next.js Implementation:
// next.config.js
module.exports = {
i18n: {
locales: ['en', 'zh', 'ja', 'de'],
defaultLocale: 'en',
localeDetection: true
}
}
Option 2: Subdomain Strategy
Advantages:
- Can deploy to different servers for different markets
- Tech stack can be independent
- Convenient for CDN and geographic location optimization
Disadvantages:
- SEO authority is dispersed, need to independently build authority for each subdomain
- Requires additional domain management and SSL certificates
- Higher implementation and maintenance costs
Option 3: URL Parameter Strategy (Not Recommended)
Advantages:
- Simplest implementation
Disadvantages:
- Worst SEO performance, search engines may ignore parameters
- Poor user experience, URLs not friendly
- Difficult to optimize for CDN caching
Conclusion:
For most projects, we strongly recommend using the subdirectory strategy. It achieves the best balance between SEO effectiveness, implementation difficulty, and maintenance costs.
3. Detailed hreflang Configuration
3.1 The Role of hreflang Tags
hreflang tags tell search engines:
- What language versions this page has
- What the URL is for each version
- The language and region corresponding to each version
3.2 Configuring hreflang in Next.js App Router
Method 1: Using Metadata API (Recommended)
// app/[lang]/about/page.tsx
import { Metadata } from 'next'
type Props = {
params: { lang: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = params
// Define supported languages
const languages = ['en', 'zh', 'ja', 'de']
// Generate alternate links for all languages
const alternates = {
canonical: `https://example.com/${lang}/about`,
languages: languages.reduce((acc, locale) => {
acc[locale] = `https://example.com/${locale}/about`
return acc
}, {} as Record<string, string>)
}
return {
title: 'About Us',
alternates,
// Add x-default for unmatched languages
other: {
'x-default': 'https://example.com/en/about'
}
}
}
export default function AboutPage({ params }: Props) {
return <div>About page in {params.lang}</div>
}
Method 2: Using Custom Head Component
// components/I18nHead.tsx
import Head from 'next/head'
interface I18nHeadProps {
currentLang: string
pathname: string
languages?: string[]
}
export default function I18nHead({
currentLang,
pathname,
languages = ['en', 'zh', 'ja', 'de']
}: I18nHeadProps) {
const baseUrl = 'https://example.com'
return (
<Head>
{/* Canonical for current page */}
<link rel="canonical" href={`${baseUrl}/${currentLang}${pathname}`} />
{/* hreflang for all language versions */}
{languages.map(lang => (
<link
key={lang}
rel="alternate"
hrefLang={lang}
href={`${baseUrl}/${lang}${pathname}`}
/>
))}
{/* x-default pointing to default language */}
<link
rel="alternate"
hrefLang="x-default"
href={`${baseUrl}/en${pathname}`}
/>
</Head>
)
}
Usage:
// app/[lang]/about/page.tsx
import I18nHead from '@/components/I18nHead'
export default function AboutPage({ params }: { params: { lang: string } }) {
return (
<>
<I18nHead
currentLang={params.lang}
pathname="/about"
/>
<div>About page content</div>
</>
)
}
3.3 Configuring hreflang in Next.js Pages Router
// pages/about.tsx
import { GetStaticProps } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'
export default function AboutPage() {
const router = useRouter()
const { locale, locales, asPath } = router
const baseUrl = 'https://example.com'
return (
<>
<Head>
{/* Canonical for current page */}
<link rel="canonical" href={`${baseUrl}/${locale}${asPath}`} />
{/* hreflang for all language versions */}
{locales?.map(loc => (
<link
key={loc}
rel="alternate"
hrefLang={loc}
href={`${baseUrl}/${loc}${asPath}`}
/>
))}
{/* x-default */}
<link
rel="alternate"
hrefLang="x-default"
href={`${baseUrl}/en${asPath}`}
/>
</Head>
<div>About page content</div>
</>
)
}
export const getStaticProps: GetStaticProps = async ({ locale }) => {
return {
props: {
messages: (await import(`../locales/${locale}.json`)).default
}
}
}
3.4 Advanced Configuration Using Region Codes
If your website needs to target specific countries/regions, you can use the language-REGION format:
// For English users in different regions
const hreflangConfig = {
'en-US': 'https://example.com/en-us/about', // US English
'en-GB': 'https://example.com/en-gb/about', // UK English
'en-AU': 'https://example.com/en-au/about', // Australian English
'zh-CN': 'https://example.com/zh-cn/about', // Mainland China
'zh-TW': 'https://example.com/zh-tw/about', // Taiwan
'zh-HK': 'https://example.com/zh-hk/about', // Hong Kong
}
Implementation in Next.js:
// next.config.js
module.exports = {
i18n: {
locales: ['en-US', 'en-GB', 'en-AU', 'zh-CN', 'zh-TW', 'zh-HK'],
defaultLocale: 'en-US',
}
}
3.5 Common Configuration Errors and Fixes
Error 1: Missing Self-Reference
<!-- Wrong: Current page doesn't reference itself -->
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<!-- Correct: Must include self-reference for current page -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
Error 2: Missing x-default
<!-- Recommended: Add x-default as default language -->
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
The purpose of x-default is to provide a default version for users who don’t match any language.
Error 3: hreflang and canonical Conflict
<!-- Wrong: canonical points to different language version -->
<link rel="canonical" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<!-- Correct: canonical should point to current language version -->
<link rel="canonical" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
4. Multilingual Sitemap Implementation
Sitemaps are an important tool for helping search engines discover and index your pages. For multilingual websites, proper sitemap configuration is crucial.
4.1 Why You Need a Multilingual Sitemap
- Speed up indexing: Proactively tell search engines about all language version pages
- Ensure completeness: Avoid missing certain language versions
- Convey hreflang information: You can also configure hreflang in the sitemap
4.2 Sitemap Strategy Selection
There are two mainstream approaches:
Option 1: Single Sitemap (Recommended for Small Sites)
All language URLs in one sitemap.xml:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://example.com/en/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
<xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
</url>
<url>
<loc>https://example.com/zh/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
<xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
</url>
</urlset>
Option 2: Language-Separated Sitemap (Recommended for Large Sites)
A separate sitemap for each language, then aggregate with a sitemap index:
<!-- sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-en.xml</loc>
<lastmod>2024-01-01</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-zh.xml</loc>
<lastmod>2024-01-01</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-ja.xml</loc>
<lastmod>2024-01-01</lastmod>
</sitemap>
</sitemapindex>
4.3 Generating Sitemap in Next.js App Router
Next.js 13+ provides built-in sitemap generation functionality:
// app/sitemap.ts
import { MetadataRoute } from 'next'
// Define supported languages
const languages = ['en', 'zh', 'ja', 'de']
// Define all routes on the site (without language prefix)
const routes = ['', '/about', '/blog', '/contact']
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://example.com'
const sitemap: MetadataRoute.Sitemap = []
// Generate all language versions for each route
routes.forEach(route => {
// Create an entry for each language
languages.forEach(lang => {
const url = `${baseUrl}/${lang}${route}`
sitemap.push({
url,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: route === '' ? 1 : 0.8,
// Next.js will automatically handle alternateRefs
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}${route}`
return acc
}, {} as Record<string, string>)
}
})
})
})
return sitemap
}
4.4 Sitemap Generation for Dynamic Content
If your website has dynamic content (like blog posts), you need to fetch from database or CMS:
// app/sitemap.ts
import { MetadataRoute } from 'next'
const languages = ['en', 'zh', 'ja']
const baseUrl = 'https://example.com'
// Simulate fetching article list from database
async function getArticles() {
// In actual projects, this should fetch from database or CMS
return [
{ slug: 'getting-started', lastModified: '2024-01-01' },
{ slug: 'advanced-guide', lastModified: '2024-01-15' },
]
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const sitemap: MetadataRoute.Sitemap = []
// 1. Add static pages
const staticPages = ['', '/about', '/contact']
staticPages.forEach(page => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: page === '' ? 1 : 0.8,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}${page}`
return acc
}, {} as Record<string, string>)
}
})
})
})
// 2. Add dynamic content (blog posts)
const articles = await getArticles()
articles.forEach(article => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}/blog/${article.slug}`,
lastModified: new Date(article.lastModified),
changeFrequency: 'weekly',
priority: 0.6,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
return acc
}, {} as Record<string, string>)
}
})
})
})
return sitemap
}
4.5 Generating Sitemap in Pages Router
For Pages Router, you need to manually create an API route:
// pages/api/sitemap.xml.ts
import { NextApiRequest, NextApiResponse } from 'next'
const baseUrl = 'https://example.com'
const languages = ['en', 'zh', 'ja']
function generateSiteMap(pages: string[]) {
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
${pages.map(page => {
return languages.map(lang => {
const url = `${baseUrl}/${lang}${page}`
const alternates = languages.map(l =>
` <xhtml:link rel="alternate" hreflang="${l}" href="${baseUrl}/${l}${page}"/>`
).join('\n')
return ` <url>
<loc>${url}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
${alternates}
<xhtml:link rel="alternate" hreflang="x-default" href="${baseUrl}/en${page}"/>
</url>`
}).join('\n')
}).join('\n')}
</urlset>`
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// Define all page routes
const pages = ['', '/about', '/blog', '/contact']
const sitemap = generateSiteMap(pages)
res.setHeader('Content-Type', 'text/xml')
res.write(sitemap)
res.end()
}
4.6 Submitting Sitemap to Search Engines
After generating the sitemap, you need to submit it to search engines:
Method 1: Declare in robots.txt
# public/robots.txt
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
Method 2: Submit to Google Search Console
- Visit Google Search Console
- Select your site property
- Choose “Sitemaps” from the left menu
- Enter the URL of sitemap.xml
- Click “Submit”
Method 3: Submit to Bing Webmaster Tools
- Visit Bing Webmaster Tools
- Add your site
- Submit sitemap.xml in the “Sitemaps” section
4.7 Validating Sitemap
Use these tools to validate your sitemap:
- XML Sitemap Validator: https://www.xml-sitemaps.com/validate-xml-sitemap.html
- Google Search Console: Check indexing status after submission
- Online XML Validator: Ensure XML format is correct
5. Best Practices and Considerations
5.1 Importance of Translation Quality
Search engines can detect low-quality translations:
Don’t do:
- Use automatic translation tools to directly generate content
- Only translate navigation and headings, leaving body content the same
- Have too much difference in content between language versions
Should do:
- Hire professional translators or native speakers
- Localize content rather than just translate (consider cultural differences)
- Maintain consistency and quality across all language versions
5.2 Avoiding SEO Risks of Automatic Translation
// ❌ Not recommended: Client-side automatic translation
import GoogleTranslate from 'google-translate-api'
export default function Page() {
const [content, setContent] = useState('')
useEffect(() => {
// Client-side automatic translation is ineffective for SEO
GoogleTranslate(originalText, { to: 'zh' })
.then(res => setContent(res.text))
}, [])
return <div>{content}</div>
}
// ✅ Recommended: Server-side rendering of real translated content
export default function Page({ params }: { params: { lang: string } }) {
// Get real translated content from database or files
const content = await getTranslatedContent(params.lang)
return <div>{content}</div>
}
5.3 Performance Optimization Recommendations
1. Use CDN to Accelerate Multi-Region Access
// next.config.js
module.exports = {
images: {
domains: ['cdn.example.com'],
},
// Enable compression
compress: true,
}
2. Load Language Packs on Demand
// Dynamically import language packs
const messages = await import(`@/locales/${lang}.json`)
3. Caching Strategy
// app/[lang]/layout.tsx
export const revalidate = 3600 // Revalidate every 1 hour
5.4 Monitoring and Maintenance
1. Regularly Check hreflang Errors
Use Google Search Console’s “International Targeting” report:
- Check hreflang tag errors
- View indexing status of language versions
- Monitor performance of each language version
2. Recommended Monitoring Tools
- Google Search Console: Official tool, must use
- Ahrefs Site Audit: Professional SEO tool
- Screaming Frog: Crawler tool, can batch check hreflang
- hreflang Tags Testing Tool: https://www.aleydasolis.com/english/international-seo-tools/hreflang-tags-generator/
3. Create Monitoring Scripts
// scripts/check-hreflang.ts
import { JSDOM } from 'jsdom'
async function checkHreflang(url: string) {
const response = await fetch(url)
const html = await response.text()
const dom = new JSDOM(html)
const document = dom.window.document
const hreflangLinks = document.querySelectorAll('link[rel="alternate"][hreflang]')
console.log(`Found ${hreflangLinks.length} hreflang links on ${url}`)
hreflangLinks.forEach(link => {
const hreflang = link.getAttribute('hreflang')
const href = link.getAttribute('href')
console.log(` ${hreflang}: ${href}`)
})
// Check for self-reference
const currentUrl = new URL(url).href
const hasSelfReference = Array.from(hreflangLinks).some(
link => link.getAttribute('href') === currentUrl
)
if (!hasSelfReference) {
console.warn('⚠️ Warning: Missing self-reference hreflang tag')
}
// Check for x-default
const hasXDefault = Array.from(hreflangLinks).some(
link => link.getAttribute('hreflang') === 'x-default'
)
if (!hasXDefault) {
console.warn('⚠️ Warning: Missing x-default hreflang tag')
}
}
// Usage example
checkHreflang('https://example.com/en/about')
checkHreflang('https://example.com/zh/about')
6. Real-World Case: Complete Project Example
Let’s look at a complete example showing how to implement multilingual SEO in a Next.js App Router project.
6.1 Project Structure
my-i18n-site/
├── app/
│ ├── [lang]/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── about/
│ │ │ └── page.tsx
│ │ └── blog/
│ │ ├── page.tsx
│ │ └── [slug]/
│ │ └── page.tsx
│ ├── sitemap.ts
│ └── robots.ts
├── components/
│ └── I18nMetadata.tsx
├── lib/
│ ├── i18n.ts
│ └── articles.ts
├── locales/
│ ├── en.json
│ ├── zh.json
│ └── ja.json
└── next.config.js
6.2 Configuration Files
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Note: App Router doesn't use i18n config
// Need to implement routing manually
}
module.exports = nextConfig
// lib/i18n.ts
export const languages = ['en', 'zh', 'ja'] as const
export type Language = (typeof languages)[number]
export const defaultLanguage: Language = 'en'
export const languageNames: Record<Language, string> = {
en: 'English',
zh: '中文',
ja: '日本語',
}
export function isValidLanguage(lang: string): lang is Language {
return languages.includes(lang as Language)
}
6.3 Layout Component
// app/[lang]/layout.tsx
import { languages, isValidLanguage, defaultLanguage } from '@/lib/i18n'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
return languages.map(lang => ({ lang }))
}
export default function LangLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
if (!isValidLanguage(params.lang)) {
notFound()
}
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
}
6.4 Page Component with Metadata
// app/[lang]/about/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'
type Props = {
params: { lang: Language }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = params
const baseUrl = 'https://example.com'
const pathname = '/about'
// Generate alternates
const alternates = {
canonical: `${baseUrl}/${lang}${pathname}`,
languages: languages.reduce((acc, locale) => {
acc[locale] = `${baseUrl}/${locale}${pathname}`
return acc
}, {} as Record<string, string>)
}
// Return different titles and descriptions based on language
const titles: Record<Language, string> = {
en: 'About Us - Learn More About Our Company',
zh: '关于我们 - 了解更多关于我们公司的信息',
ja: '私たちについて - 当社についてもっと知る',
}
const descriptions: Record<Language, string> = {
en: 'Learn about our mission, values, and the team behind our success.',
zh: '了解我们的使命、价值观以及我们成功背后的团队。',
ja: '私たちの使命、価値観、そして成功を支えるチームについて学びます。',
}
return {
title: titles[lang],
description: descriptions[lang],
alternates,
openGraph: {
title: titles[lang],
description: descriptions[lang],
url: `${baseUrl}/${lang}${pathname}`,
siteName: 'Example Site',
locale: lang,
type: 'website',
},
}
}
export default function AboutPage({ params }: Props) {
const content = {
en: 'About us content in English...',
zh: '关于我们的中文内容...',
ja: '私たちについての日本語コンテンツ...',
}
return (
<div>
<h1>About Us</h1>
<p>{content[params.lang]}</p>
</div>
)
}
6.5 Dynamic Routes with hreflang
// app/[lang]/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'
import { getArticle, getAllArticles } from '@/lib/articles'
import { notFound } from 'next/navigation'
type Props = {
params: { lang: Language; slug: string }
}
export async function generateStaticParams() {
const articles = await getAllArticles()
return languages.flatMap(lang =>
articles.map(article => ({
lang,
slug: article.slug,
}))
)
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang, slug } = params
const article = await getArticle(slug, lang)
if (!article) {
return {}
}
const baseUrl = 'https://example.com'
const pathname = `/blog/${slug}`
const alternates = {
canonical: `${baseUrl}/${lang}${pathname}`,
languages: languages.reduce((acc, locale) => {
acc[locale] = `${baseUrl}/${locale}${pathname}`
return acc
}, {} as Record<string, string>)
}
return {
title: article.title,
description: article.excerpt,
alternates,
openGraph: {
title: article.title,
description: article.excerpt,
url: `${baseUrl}/${lang}${pathname}`,
type: 'article',
publishedTime: article.publishedAt,
authors: [article.author],
},
}
}
export default async function BlogArticle({ params }: Props) {
const { lang, slug } = params
const article = await getArticle(slug, lang)
if (!article) {
notFound()
}
return (
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
</article>
)
}
6.6 Sitemap Generation
// app/sitemap.ts
import { MetadataRoute } from 'next'
import { languages } from '@/lib/i18n'
import { getAllArticles } from '@/lib/articles'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com'
const sitemap: MetadataRoute.Sitemap = []
// Static pages
const staticPages = ['', '/about', '/contact']
staticPages.forEach(page => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: page === '' ? 1 : 0.8,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}${page}`
return acc
}, {} as Record<string, string>)
}
})
})
})
// Blog posts
const articles = await getAllArticles()
articles.forEach(article => {
languages.forEach(lang => {
sitemap.push({
url: `${baseUrl}/${lang}/blog/${article.slug}`,
lastModified: new Date(article.updatedAt),
changeFrequency: 'weekly',
priority: 0.6,
alternates: {
languages: languages.reduce((acc, l) => {
acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
return acc
}, {} as Record<string, string>)
}
})
})
})
return sitemap
}
6.7 Robots.txt
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: 'https://example.com/sitemap.xml',
}
}
7. Verification and Testing
7.1 Local Testing Checklist
Before deploying to production, complete these checks:
- All pages have correct
langattribute - Every page includes complete hreflang tags
- hreflang tags include self-reference
-
x-defaulttag exists - canonical tag points to correct URL
- Sitemap includes all language versions
- robots.txt correctly points to sitemap
- Content quality is consistent across different language versions
7.2 Using Google Rich Results Test
Visit Google Rich Results Test to test your pages:
- Enter page URL
- Check for errors or warnings
- See if hreflang tags are correctly recognized
7.3 Using hreflang Checking Tools
Recommended tools:
- Aleyda Solis hreflang Generator: https://www.aleydasolis.com/english/international-seo-tools/hreflang-tags-generator/
- Merkle hreflang Checker: https://technicalseo.com/tools/hreflang/
7.4 Google Search Console Verification
After deployment:
- Submit sitemap to Google Search Console
- Wait 1-2 weeks for Google to index
- Check “International Targeting” > “Language” report
- Look for hreflang errors
8. Frequently Asked Questions
Q1: What’s the difference between hreflang and canonical?
- canonical: Tells search engines the canonical URL of this page (for handling duplicate content)
- hreflang: Tells search engines what language versions this page has
They can be used together without conflict.
Q2: Must I configure hreflang for every page?
Yes. hreflang tags must exist in every language version and must be symmetric (mutually referenced).
Q3: Which language should x-default point to?
Typically points to your default language or most universal version. For example:
- If the main audience is English users, point to English version
- If it’s a global website, point to international English version
Q4: Subdirectory vs Subdomain - which is better?
Subdirectory (Recommended):
- Concentrated SEO authority
- Simple implementation and maintenance
- Suitable for most projects
Subdomain:
- Can independently deploy to different servers
- Suitable for large international websites
- Requires additional domain management
Q5: How to handle machine-translated content?
Not recommended to use machine translation for SEO:
- Search engines can identify low-quality translations
- May be treated as duplicate content
- Poor user experience
If budget is limited:
- Prioritize translating core pages (homepage, product pages)
- Use human proofreading of machine translations
- Gradually improve translation quality
Q6: How long does it take for a multilingual website to be indexed?
Generally:
- Start indexing 1-2 weeks after submitting sitemap
- Full indexing may take 1-2 months
- Authority building takes 3-6 months
Methods to speed up indexing:
- Ensure sitemap is correct
- Improve content quality
- Acquire external links
- Regularly update content
9. Conclusion
Multilingual SEO optimization is key to the success of internationalized websites. Let’s review the core points:
9.1 Key Takeaways
-
URL Strategy
- Recommended to use subdirectory strategy (example.com/en/)
- Ensure URL structure is clear and consistent
-
hreflang Configuration
- Every page must configure complete hreflang tags
- Include self-reference and x-default
- Use correct language codes (ISO 639-1)
-
Sitemap
- Include URLs for all language versions
- Also add hreflang information in sitemap
- Regularly update and submit to search engines
-
Content Quality
- Avoid using machine translation
- Maintain consistency across all language versions
- Localize rather than just translate
-
Monitoring and Maintenance
- Use Google Search Console for monitoring
- Regularly check hreflang errors
- Track performance of each language version
9.2 Action Checklist
Complete these steps to ensure your multilingual SEO configuration is correct:
- Choose and implement URL strategy (subdirectory/subdomain)
- Add hreflang tags to all pages
- Configure canonical tags
- Generate multilingual sitemap
- Configure robots.txt
- Submit sitemap to Google Search Console
- Use validation tools to check hreflang
- Check translation quality
- Set up monitoring and regular checking process
9.3 Further Reading
- Google Multi-regional and Multilingual Sites Guide
- Complete hreflang Guide
- Next.js Internationalization Routing
- Schema.org Multilingual Markup
Properly implementing multilingual SEO requires time and effort, but the rewards are significant: better search rankings, more precise user matching, and higher conversion rates. Follow the best practices in this article, and your multilingual website will perform better in search engines.
If you have any questions or encounter difficulties, feel free to discuss in the comments!
FAQ
What is hreflang and why is it important?
• Page's target language and region
• Relationships between language versions
• Which version to show users
Importance:
• Prevents duplicate content penalties
• Ensures correct language version shown
• Improves SEO rankings
• Better user experience
Without hreflang, search engines may treat different language versions as duplicate content.
How do I configure hreflang in Next.js?
Example:
export async function generateMetadata({ params }) {
const { locale } = params
return {
alternates: {
languages: {
'en': '/en/about',
'zh': '/zh/about',
'x-default': '/en/about'
}
}
}
}
Or manually add in head:
<link rel="alternate" hreflang="en" href="/en/about" />
<link rel="alternate" hreflang="zh" href="/zh/about" />
<link rel="alternate" hreflang="x-default" href="/en/about" />
What are common hreflang errors?
• Missing hreflang tags
• Incorrect language codes
• Missing x-default
• Not including all language versions
• Circular references
• Wrong URLs
Solutions:
• Use correct language codes (en, zh-CN, etc.)
• Include all language versions
• Add x-default for default language
• Verify URLs are correct
How do I generate multilingual sitemaps?
Example:
export default async function sitemap() {
const locales = ['en', 'zh']
const routes = ['/', '/about']
return locales.flatMap(locale =>
routes.map(route => ({
url: `https://example.com/${locale}${route}`,
alternates: {
languages: Object.fromEntries(
locales.map(l => [l, `https://example.com/${l}${route}`])
)
}
}))
)
}
What URL structure is best for multilingual sites?
1) Sub-path: example.com/en/, example.com/zh/
2) Sub-domain: en.example.com, zh.example.com
3) Top-level domain: example.com, example.cn
Recommendation: Sub-path is easiest to implement and maintain.
Example: /en/about, /zh/about
How do I test hreflang configuration?
1) Google Search Console hreflang report
2) hreflang testing tools
3) Manual HTML inspection
4) Google's Rich Results Test
Check:
• All language versions included
• Correct language codes
• Valid URLs
• No circular references
How do I handle SEO for dynamic routes?
• Generate hreflang for each dynamic page
• Include locale in generateMetadata
• Ensure all language versions have same content structure
• Use generateStaticParams for all locales
Example:
export async function generateStaticParams() {
return locales.flatMap(locale =>
posts.map(post => ({ locale, slug: post.slug }))
)
}
10 min read · Published on: Dec 25, 2025 · Modified on: Jan 22, 2026
Related Posts
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Next.js Unit Testing Guide: Complete Jest + React Testing Library Setup

Comments
Sign in with GitHub to leave a comment