DEV Community

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

Posted on • Edited on

2 1 1 1

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

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay