Switch Language
Toggle Theme

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:

  1. Prevent duplicate content penalties: Tell search engines that different language versions are translations of the same content, not duplicate content
  2. Precise user matching: Display the most appropriate page version based on users’ language and region settings
  3. 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:

  1. Detect the page’s language (through HTML lang attribute, hreflang tags, page content)
  2. Look for hreflang tags to understand relationships between pages
  3. Display the corresponding version in search results based on users’ language preferences
  4. 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

StrategyExampleSEO ImpactImplementation DifficultyRecommendation
Subdirectoryexample.com/en/
example.com/zh/
⭐⭐⭐⭐⭐ Best⭐⭐⭐ Medium⭐⭐⭐⭐⭐
Subdomainen.example.com
zh.example.com
⭐⭐⭐ Average⭐⭐⭐⭐ Complex⭐⭐⭐
URL Parameterexample.com?lang=en⭐⭐ Poor⭐⭐⭐⭐⭐ Simple⭐⭐

2.2 Detailed Analysis of Each Strategy

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

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:

  1. What language versions this page has
  2. What the URL is for each version
  3. The language and region corresponding to each version

3.2 Configuring hreflang in Next.js App Router

// 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

  1. Speed up indexing: Proactively tell search engines about all language version pages
  2. Ensure completeness: Avoid missing certain language versions
  3. Convey hreflang information: You can also configure hreflang in the sitemap

4.2 Sitemap Strategy Selection

There are two mainstream approaches:

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>

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

  1. Visit Google Search Console
  2. Select your site property
  3. Choose “Sitemaps” from the left menu
  4. Enter the URL of sitemap.xml
  5. Click “Submit”

Method 3: Submit to Bing Webmaster Tools

  1. Visit Bing Webmaster Tools
  2. Add your site
  3. Submit sitemap.xml in the “Sitemaps” section

4.7 Validating Sitemap

Use these tools to validate your sitemap:

  1. XML Sitemap Validator: https://www.xml-sitemaps.com/validate-xml-sitemap.html
  2. Google Search Console: Check indexing status after submission
  3. 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

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 lang attribute
  • Every page includes complete hreflang tags
  • hreflang tags include self-reference
  • x-default tag 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:

  1. Enter page URL
  2. Check for errors or warnings
  3. See if hreflang tags are correctly recognized

7.3 Using hreflang Checking Tools

Recommended tools:

7.4 Google Search Console Verification

After deployment:

  1. Submit sitemap to Google Search Console
  2. Wait 1-2 weeks for Google to index
  3. Check “International Targeting” > “Language” report
  4. 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:

  1. Prioritize translating core pages (homepage, product pages)
  2. Use human proofreading of machine translations
  3. 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

  1. URL Strategy

    • Recommended to use subdirectory strategy (example.com/en/)
    • Ensure URL structure is clear and consistent
  2. hreflang Configuration

    • Every page must configure complete hreflang tags
    • Include self-reference and x-default
    • Use correct language codes (ISO 639-1)
  3. Sitemap

    • Include URLs for all language versions
    • Also add hreflang information in sitemap
    • Regularly update and submit to search engines
  4. Content Quality

    • Avoid using machine translation
    • Maintain consistency across all language versions
    • Localize rather than just translate
  5. 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

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?
hreflang is an HTML attribute that tells search engines:
• 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?
In Metadata API:

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?
Common 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?
In sitemap.ts:

Example:
export default async function sitemap() {
const locales = ['en', 'zh']
const routes = ['/', '/about']

return locales.flatMap(locale =>
routes.map(route => ({
url: `https://example.com/$&#123;locale&#125;$&#123;route&#125;`,
alternates: {
languages: Object.fromEntries(
locales.map(l => [l, `https://example.com/$&#123;l&#125;$&#123;route&#125;`])
)
}
}))
)
}
What URL structure is best for multilingual sites?
Options:
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?
Testing methods:
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?
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

Comments

Sign in with GitHub to leave a comment

Related Posts