DEV Community

Cover image for SEO and i18n Implementation Guide for Next.js App Router: Dynamic Metadata and Internationalization
Sho Ayuba
Sho Ayuba

Posted on

3 1

SEO and i18n Implementation Guide for Next.js App Router: Dynamic Metadata and Internationalization

This article will provide practical implementation methods for optimizing SEO in internationalized (i18n) web applications using Next.js 14's App Router and Next-intl. We'll focus on metadata configuration for dynamically generated pages and multilingual support, using an e-commerce site as a concrete example.

Target Audience

This guide is for developers who:

  • Have basic understanding of Next.js App Router
  • Know TypeScript fundamentals
  • Have basic SEO knowledge

Development Environment

{
  "next": "^14.2.22",
  "next-intl": "^3.26.3",
  "react": "^18",
  "typescript": "^5"
}
Enter fullscreen mode Exit fullscreen mode

1. Project Structure

1.1 Directory Layout

src/
  ├── app/
  │   └── [locale]/
  │       └── product/
  │           └── [id]/
  │               ├── page.tsx
  │               └── layout.tsx
  ├── lib/
  │   └── metadata/
  │       └── productMetadata.ts
  ├── api/
  │   └── model/
  │       └── Product.ts
  └── i18n/
  │   ├── routing.ts
  │
  └── messages/
      ├── en.json
      └── ja.json
Enter fullscreen mode Exit fullscreen mode

1.2 Implementing Internationalized Routing

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
import { createNavigation } from "next-intl/navigation";

export const routing = defineRouting({
  locales: ["en", "ja"],
  defaultLocale: "en",
});

export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
Enter fullscreen mode Exit fullscreen mode

1.3 Product Data Type Definition

// src/api/model/Product.ts
export interface Product {
  id: string;
  name: string;
  code: string;
  category: {
    id: string;
    name: string;
  };
  manufacturer?: string;
  description: string;
  images: {
    small: string;
    large: string;
  };
  price: number;
  stock: number;
}
Enter fullscreen mode Exit fullscreen mode

2. Dynamic Metadata Generation

2.1 Implementing Metadata Generation Logic

// src/lib/metadata/productMetadata.ts
import { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import type { Product } from "@/api/model/Product";
import { routing } from "@/i18n/routing";

export async function generateProductMetadata(
  productDataPromise: Promise<Product>,
  locale: string
): Promise<Metadata> {
  const [productData, t] = await Promise.all([
    productDataPromise,
    getTranslations({ locale, namespace: "metadataProduct" }),
  ]);

  // SEO optimization: Information structuring
  const descriptionParts = [
    productData.name,
    `${productData.category.name} #${productData.code}`,
    productData.manufacturer ? `${t("manufacturedBy")}${productData.manufacturer}` : "",
    t("checkPrice")
  ].filter(Boolean);

  const description = descriptionParts.join(" | ");

  // Dynamic keyword generation
  const productSpecificKeywords = [
    productData.name,
    productData.category.name,
    productData.manufacturer,
    `${productData.name} ${t("price")}`,
    `${productData.name} ${t("stock")}`,
    `${productData.category.name} ${t("products")}`
  ].filter(Boolean);

  const baseKeywords = t("keywords").split(", ");
  const allKeywords = [...new Set([...productSpecificKeywords, ...baseKeywords])];

  return {
    title: `${productData.name} | ${productData.category.name} | ${t("siteTitle")}`,
    description,
    keywords: allKeywords.join(", "),
    metadataBase: new URL("https://example.com"),

    alternates: {
      canonical: `/${locale}/product/${productData.id}`,
      languages: {
        "x-default": `/en/product/${productData.id}`,
        ...Object.fromEntries(
          routing.locales.map((l) => [l, `/${l}/product/${productData.id}`])
        ),
      },
    },

    openGraph: {
      type: "website",
      siteName: t("siteTitle"),
      title: `${productData.name} | ${productData.category.name}`,
      description,
      locale,
      alternateLocale: routing.locales.filter((l) => l !== locale),
      images: productData.images.large ? [
        {
          url: productData.images.large,
          width: 1200,
          height: 630,
          alt: productData.name,
        },
      ] : undefined,
    },
    robots: {
      index: true,
      follow: true,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

2.2 Translation File Setup

// messages/en.json
{
  "metadataProduct": {
    "siteTitle": "Example Store",
    "manufacturedBy": "Manufactured by",
    "checkPrice": "Check price and availability",
    "price": "price",
    "stock": "stock",
    "products": "products",
    "keywords": "online store, ecommerce, shopping, price comparison, stock check, product reviews, online shopping"
  }
}

// messages/ja.json
{
  "metadataProduct": {
    "siteTitle": "Example Store",
    "manufacturedBy": "製造元",
    "checkPrice": "価格と在庫状況をチェック",
    "price": "価格",
    "stock": "在庫",
    "products": "商品",
    "keywords": "オンラインストア, eコマース, 通販, 価格比較, 在庫確認, 商品レビュー, オンラインショッピング"
  }
}
Enter fullscreen mode Exit fullscreen mode

2.3 Page Component Implementation

// app/[locale]/product/[id]/page.tsx
import { generateProductMetadata } from "@/lib/metadata/productMetadata";
import { cache } from 'react';
import { notFound } from 'next/navigation';

const getProductDataCached = cache(async (id: string) => {
  const response = await fetch(`/api/products/${id}`);
  if (!response.ok) {
    notFound();
  }
  return response.json();
});

export async function generateMetadata({ 
  params 
}: { 
  params: { id: string; locale: string } 
}) {
  const productDataPromise = getProductDataCached(params.id);
  return generateProductMetadata(productDataPromise, params.locale);
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const productData = await getProductDataCached(params.id);
  // Page content implementation
}
Enter fullscreen mode Exit fullscreen mode

3. SEO Optimization Key Points

3.1 Metadata Structuring

When structuring metadata, consider:

  1. Title Optimization

    • Order information by importance
    • Use pipes (|) for proper separation
    • Include site name for brand recognition
  2. Description Structuring

    • Clear information segmentation
    • Include only necessary information
    • Match search intent
  3. Keyword Optimization

    • Combine dynamic and static keywords
    • Consider search patterns
    • Natural keyword placement

3.2 Multilingual Considerations

  1. URL Structure Design

    • Include language codes
    • Set proper canonical URLs
    • Configure x-default
  2. Content Optimization

    • Language-appropriate descriptions
    • Cultural considerations
    • Proper keyword translation

3.3 OpenGraph Configuration

For OpenGraph settings:

  1. Basic Configuration

    • Choose appropriate type settings
    • Maintain siteName and title consistency
    • Optimize descriptions
  2. Image Handling

    • Set appropriate sizes and ratios
    • Configure alt attributes
    • Handle missing images gracefully

4. Performance Optimization

4.1 Data Fetching Optimization

  1. Utilizing Cache
   const getProductDataCached = cache(async (id: string) => {
     // Data fetching logic
   });
Enter fullscreen mode Exit fullscreen mode
  • Prevent duplicate requests
  • Improve response time
  • Ensure consistent data
  1. Error Handling
    • Display 404 pages appropriately
    • Maintain user experience
    • Ensure crawlability

4.2 Rendering Optimization

  1. Separate Metadata Generation

    • Clear logic separation
    • Improve reusability
    • Enhance maintainability
  2. Optimize Async Processing

    • Parallel processing for speed
    • Efficient resource usage

Summary

We've covered key implementation points for SEO and internationalization in Next.js App Router:

  • Dynamic metadata generation implementation
  • Multilingual support considerations and optimization
  • SEO best practices
  • Performance optimization techniques

These implementations enable building SEO-optimized multilingual web applications effectively.

References

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (1)

Collapse
 
mehmetakar profile image
mehmet akar

SEO Optimization in Next.js is crucial but unfortunately it is often overlooked. Thanks, Great Tips!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more