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
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(),
}
}
}
Technical Issues Encountered
- 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>
- 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>
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
Attempted Solutions
- 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'
}]
}
}
}
This approach failed to resolve the issue of redundant paths in alternateRefs URLs.
- 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;
}
}
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}`])
),
},
}))
);
}
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.
Top comments (0)