DEV Community

Cover image for Implementing Multilingual Sitemap with next-intl in Next.js App Router
Sho Ayuba
Sho Ayuba

Posted on

Implementing Multilingual Sitemap with next-intl in Next.js App Router

Introduction

This article describes the journey of implementing a multilingual sitemap in a Next.js App Router project. Initially attempting to use next-sitemap, we ultimately switched to the App Router's built-in sitemap generation functionality. Here's what we learned along the way.

Development Environment

- Next.js 14.x (App Router)
- TypeScript 5.x
- next-intl 3.x
- next-sitemap 4.x
Enter fullscreen mode Exit fullscreen mode

Requirements

  • Sitemap generation supporting multiple languages (/en, /ja, etc.)
  • Proper alternate links configuration for each language
  • SEO-compliant XML format
  • Appropriate priority and update frequency settings for pages

Initial Implementation with next-sitemap and Its Challenges

Initial Setup

// next-sitemap.config.js
module.exports = {
  siteUrl: 'https://example.com',
  generateRobotsTxt: true,
  exclude: ['/_not-found', '/404', '/500'],
  generateIndexSitemap: false,

  transform: async (config, path) => {
    return {
      loc: path,
      changefreq: 'daily',
      priority: 0.7,
      lastmod: new Date().toISOString(),
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Technical Issues Encountered

  1. Alternate Links Generation Error

The issue where /en/about incorrectly follows /ja/about:

<!-- Incorrect generation example -->
<url>
  <loc>https://example.com/en/about</loc>
  <xhtml:link rel="alternate" hreflang="ja" 
    href="https://example.com/ja/about/en/about"/>
</url>
Enter fullscreen mode Exit fullscreen mode
  1. Sitemap Index Structure Issues
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Empty index generation -->
</sitemapindex>
Enter fullscreen mode Exit fullscreen mode

Troubleshooting Process

During npm run build, we can observe static (SSG) and dynamic page generation:

Route (app)
┌ ○ /_not-found 
├ ● /[locale]   
├   ├ /en
├   └ /ja
├ ● /[locale]/about   
├   ├ /en/about
├   └ /ja/about
├ ƒ /[locale]/blog/[id]
├ ● /[locale]/contact
├   ├ /en/contact
├   └ /ja/contact
└ ● /[locale]/products
    ├ /en/products
    └ /ja/products

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
ƒ  (Dynamic)  server-rendered on demand
Enter fullscreen mode Exit fullscreen mode

Attempted Solutions

  1. Transform Approach
// next-sitemap.config.js
module.exports = {
  transform: async (config, path) => {
    const parts = path.split('/').filter(Boolean);
    const locale = parts[0];

    if (!['en', 'ja'].includes(locale)) {
      return null;
    }

    return {
      loc: `${config.siteUrl}${path}`,
      changefreq: 'weekly',
      priority: 0.7,
      alternateRefs: [{
        href: `${config.siteUrl}/${locale === 'en' ? 'ja' : 'en'}${path}`,
        hreflang: locale === 'en' ? 'ja' : 'en'
      }]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach failed to resolve the issue of redundant paths in alternateRefs URLs.

  1. Additional Paths Approach
module.exports = {
  additionalPaths: async (config) => {
    const urls = [];
    const locales = ['en', 'ja'];
    const routes = [
      { path: '', priority: 1.0 },
      { path: '/about', priority: 0.8 },
      { path: '/contact', priority: 0.8 },
      { path: '/products', priority: 0.8 }
    ];

    locales.forEach(locale => {
      routes.forEach(({ path, priority }) => {
        urls.push({
          loc: `/${locale}${path}`,
          changefreq: 'weekly',
          priority,
          alternateRefs: [/* ... */]
        });
      });
    });
    return urls;
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach required manual URL management, leading to maintenance issues.

Solution Using App Router's Built-in Functionality

Inspired by next-intl's example implementation:

// app/sitemap.ts
import { MetadataRoute } from 'next'
import { routing } from "@/i18n/routing";

const host = process.env.NEXT_PUBLIC_HOST || "http://localhost:3000";

export default function sitemap(): MetadataRoute.Sitemap {
    const baseUrl = 'https://example.com'

    const routes = [
        { path: '', priority: 1.0 },
        { path: '/about', priority: 0.8 },
        { path: '/contact', priority: 0.8 },
        { path: '/products', priority: 0.8 }
    ]

    return routes.flatMap((route) =>
        routing.locales.map((locale) => ({
            url: `${host}/${locale}${route === "/" ? "" : route}`,
            alternates: {
                languages: Object.fromEntries(
                    routing.locales.map((altLocale) => [altLocale, `${host}/${altLocale}${route === "/" ? "" : route}`])
                ),
            },
        }))
    );
}
Enter fullscreen mode Exit fullscreen mode

Place this in src/app/sitemap.ts, and Next.js will automatically generate the sitemap.xml. You can verify the generated sitemap by accessing localhost:3000/sitemap.xml in development.

Comparison of Implementation Approaches

next-sitemap

Advantages:

  • Automatic robots.txt generation
  • Rich configuration options
  • Sitemap index generation

Disadvantages:

  • Complex configuration for multilingual support
  • Compatibility issues with App Router
  • Difficult debugging process

App Router Built-in Functionality

Advantages:

  • Full TypeScript support
  • Easy integration with App Router
  • Simple implementation structure
  • Straightforward debugging

Disadvantages:

  • Requires separate robots.txt implementation
  • Limited configuration options
  • Lacks sitemap index functionality

Implementation Decision Criteria

Choose next-sitemap when:

  • Using Pages Router
  • Complex sitemap requirements
  • Single language site

Choose App Router Built-in when:

  • Using App Router
  • Multilingual site requirements
  • TypeScript-focused development

Conclusion

For multilingual sitemap implementation in Next.js App Router, the built-in functionality proves to be the optimal solution. The TypeScript support and implementation simplicity significantly enhance development efficiency. For SEO requirements in multilingual sites, the App Router's built-in functionality is recommended.

References

Top comments (0)