DEV Community

Cover image for How to Implement Dynamic Multilingual Sitemap with 40,000 URLs in Next.js App Router
Yuba
Yuba

Posted on

3

How to Implement Dynamic Multilingual Sitemap with 40,000 URLs in Next.js App Router

Introduction

This article explains implementing a dynamic sitemap for a large-scale website using Next.js App Router. I'll share the implementation method for a multilingual site containing over 40,000 content items.

TL;DR

By combining Next.js sitemap API with 24-hour caching, we implemented a practical dynamic sitemap solution for large-scale sites. This approach minimizes the impact on frontend build time while enabling daily data updates.

Development Environment

  • Frontend: Next.js (App Router)
  • Backend: Express + MongoDB
  • Infrastructure: Vercel (Frontend), Heroku (Backend)
  • Languages: 2 (bilingual)

Context

  • Over 20,000 unique pages
  • Each page has 2 language variations (total: 40,000+ URLs)
  • Daily data updates
  • Sitemap required for SEO requirements

Understanding Sitemap Generation

There are three main approaches to sitemap generation:

  1. Static Generation

    • Fast response time
    • Low server load
    • Longer build times
    • Difficult to handle immediate data updates
  2. Dynamic Generation

    • Easy reflection of latest data
    • No impact on build time
    • Higher server load
    • Potential slower response times
  3. ISR (Incremental Static Regeneration)

    • Balanced approach between static and dynamic
    • Easy cache control
    • Slightly more complex implementation

I had written about basic sitemap generation in the following document:
Implementing Multilingual Sitemap with next-intl in Next.js App Router

Implementation

We adopted an implementation combining Next.js sitemap API with 24-hour caching:

// app/sitemap.ts
import { MetadataRoute } from "next";

const BASE_URL = process.env.NODE_ENV === "production" 
  ? "https://example.com"
  : "localhost:3000";

const API_BASE_URL = process.env.NODE_ENV === "production"
  ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/util`
  : "http://localhost:3500/api/util";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const locales = ["en", "ja"];
  const routes = ["/", "/about", "/products", "/contact"];

  // Generate static routes
  const staticRoutes = routes.flatMap((route) =>
    locales.map((locale) => ({
      url: `${BASE_URL}/${locale}${route === "/" ? "" : route}`,
      lastModified: new Date(),
      changeFrequency: "monthly" as const,
      priority: route === "/" ? 1 : 0.8,
    }))
  );

  try {
    // Fetch product data with caching
    const response = await fetch(`${API_BASE_URL}/products`, {
      next: { revalidate: 86400 }, // Cache for 24 hours
      headers: { "Cache-Control": "public, max-age=86400" },
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch products: ${response.statusText}`);
    }

    const products = await response.json();

    // Generate product URLs
    const productRoutes = products.flatMap((product: any) =>
      locales.map((locale) => ({
        url: `${BASE_URL}/${locale}/product/${product.id}`,
        lastModified: new Date(product.updatedAt),
        changeFrequency: "daily" as const,
        priority: 0.7,
      }))
    );

    return [...staticRoutes, ...productRoutes];
  } catch (error) {
    console.error("Error generating sitemap:", error);
    return staticRoutes; // Fallback to static routes on error
  }
}
Enter fullscreen mode Exit fullscreen mode

Backend API implementation:

// Express route handler
router.get("/products", async (req, res) => {
  try {
    // Fetch minimal required data from MongoDB
    const products = await Product.find(
      {},
      {
        id: 1,
        lastUpdateDate: 1,
      }
    ).lean();
    res.json(products);
  } catch (error) {
    console.error("Failed to fetch products:", error);
    res.status(500).json({
      message: "Error fetching products",
      error: error instanceof Error ? error.message : String(error),
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Implementation Results

  • Total URLs: 42,000 (21,000 for en, ja)
  • File size: 4.6MB
  • Generation time: Under 5 seconds
  • Impact on build time: Minimal
  • Cache duration: 24 hours

Checking results

You can check the generated sitemap in dev environment, localhost:3000/sitemap.xml.

Key Implementation Considerations

  1. Caching Strategy

    • Use both Next.js revalidation and HTTP caching
    • 24-hour cache balances freshness and performance
    • Implement fallback to static routes on errors
  2. Error Handling

    • Graceful degradation to static routes
    • Structured error logging
    • Monitoring integration recommended
  3. SEO Optimization

    • Proper lastModified dates
    • Appropriate priority settings
    • Consistent language codes

Conclusion

Next.js Sitemap API proves capable of handling large-scale multilingual sites effectively. The key to success lies in implementing proper caching strategies and error handling, enabling a balance between performance and maintainability.

Resources

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay