Learn how to build SEO-optimized multilingual websites using next-intl library instead of hardcoding metadata in Next.js App Router.
One of the most common mistakes when building multilingual websites is hardcoding metadata in a single language. Today, I'll share how we solved this problem elegantly using the next-intl library.
The Problem: Metadata Stuck in English
hafuture is a global service supporting Korean, English, and Japanese. While checking Google Search Console one day, we noticed something strange. Even when accessing the Korean page (/ko), the HTML <title> and <meta description> were displayed in English.
<!-- Why is /ko showing English? -->
<title>Free Online Tools for Text, PDF & Images | Hafuture</title>
<meta name="description" content="Fast, private, browser-based tools..." />
This clearly hurts SEO and can result in penalties for Korean search results.
Root Cause: Hardcoded generateMetadata
The problem was in the generateMetadata function in layout.tsx:
// ❌ Wrong approach: Hardcoding
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
default: "Free Online Tools for Text, PDF & Images | Hafuture",
template: "%s | Hafuture",
},
description: "Fast, private, browser-based tools...",
};
}
Even with next-intl configured, if you don't call translation functions in the metadata section, static English text will be output as-is.
Solution: Using getTranslations
next-intl provides the getTranslations function for server components. With this, you can dynamically generate translated metadata in generateMetadata.
Step 1: Add Metadata Section to Message Files
// messages/en.json
{
"Metadata": {
"title": "Free Online Tools for Text, PDF & Images | Hafuture",
"description": "Fast, private, browser-based tools...",
"ogTitle": "Free Online Tools for Text, PDF & Images | Hafuture"
}
}
Step 2: Call getTranslations in generateMetadata
// ✅ Correct approach: Using next-intl
import { getTranslations } from "next-intl/server";
export async function generateMetadata(
{ params }: { params: Promise<{ locale: string }> }
): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Metadata" });
return {
title: {
default: t("title"),
template: t("titleTemplate"),
},
description: t("description"),
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
},
};
}
Hardcoding vs next-intl Comparison
| Aspect | Hardcoding (Custom) | next-intl Library |
|---|---|---|
| Final HTML Result | Varies by skill level | Perfect results guaranteed |
| Implementation Difficulty | High (server/client sync needed) | Low (designed for Next.js) |
| Error Probability | High (metadata omission, etc.) | Low (established patterns) |
| SEO Impact | Unstable | Consistently high scores |
| Maintenance | Difficult | Easy (just edit JSON files) |
Verification
After the fix, checking the HTML for each language page shows:
Korean (/ko):
<title>텍스트, PDF, 이미지를 위한 무료 온라인 도구 | Hafuture</title>
<meta property="og:locale" content="ko_KR" />
Japanese (/ja):
<title>テキスト、PDF、画像のための無料オンラインツール | Hafuture</title>
<meta property="og:locale" content="ja_JP" />
Now search engines can accurately recognize the metadata for each language!
Try our Tools
Discover Hafuture's tools that are now smarter with multilingual support:
- Text Tools: Text editing that works perfectly in every language.
- Image Tools: Image resizing according to global standards.
Conclusion
If you're using Next.js App Router, I strongly recommend adopting next-intl to prevent SEO mistakes and simplify maintenance. Using getTranslations in generateMetadata is the cleanest way to solve the "title/description not translated" problem.
Multilingual support isn't just about changing text. It's the first step in respecting your users' language and culture!
Thank you. 🌏