Multilingual SEO for Next.js: Arabic, English, Turkish Done Right (2026)
A multilingual site that ranks is not three translated copies of the same content. It's three independent sites linked by hreflang, with localized slugs, native metadata per locale, and a sitemap that tells Google exactly which version to serve to which user.
We run this stack on every CloudTopia bilingual/trilingual project. Here's how to do it in Next.js 14 App Router without leaking ranking authority across locales.
The architecture that ranks
| Approach | Setup effort | SEO quality | Arabic/Turkish ranking |
|---|---|---|---|
What we implement [locale] segment, localized slugs, canonical id linking, full hreflang, per-locale OG images, per-locale structured data. | Clean | ★★★★★ | Ranks natively in both scripts |
English slug + ?lang=ar param Worst pattern. Google may index the param, AR content often gets swallowed by EN ranking. | Low | ★ | Barely ranks in AR |
Same English slug across all locales Common but suboptimal — Arabic/Turkish keywords in URL are a strong local ranking signal you're throwing away. | Low | ★★★ | OK but misses easy wins |
Subdomain per locale (ar.site.com) Works but harder to manage, splits domain authority unless well-linked. | High | ★★★★ | Ranks but needs more internal linking |
1. The [locale] segment
In Next.js 14, nest all routes under app/[locale]/. Middleware detects the request locale (from path, cookie, or Accept-Language) and rewrites/redirects as needed.
app/
[locale]/
layout.tsx
page.tsx
blog/
[slug]/
page.tsx
pricing/page.tsx
In your root layout, set <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>. Never set dir on body — form controls and scrollbars inherit from html.
2. Localized slugs with canonical IDs
The key trick: every translated post shares a canonical id in its frontmatter, but each locale has its own slug in the native script or Turkish orthography.
# blog-posts/en/gulf-payment-gateways.mdx
slug: gulf-payment-gateways-mada-stc-pay-tabby
id: gulf-payment-gateways-guide
lang: en
# blog-posts/ar/بوابات-الدفع-الخليجية.mdx
slug: بوابات-الدفع-الخليجية-مدى-اس-تي-سي-باي-تابي
id: gulf-payment-gateways-guide
lang: ar
Then a helper resolves translations by id:
// lib/blog.ts
export function getSlugById(id: string, locale: 'en'|'ar'|'tr') {
return allPosts.find(p => p.id === id && p.lang === locale)?.slug
}
This unlocks clean hreflang and proper language switchers that don't drop users back to the homepage.
3. hreflang tags per page
In each page.tsx's generateMetadata, emit hreflang for every available locale:
export async function generateMetadata({ params }) {
const post = await getPostBySlug(params.slug, params.locale)
const languages: Record<string, string> = {}
for (const locale of ['en','ar','tr']) {
const slug = getSlugById(post.id, locale)
if (slug) languages[locale] = `/${locale}/blog/${slug}`
}
return {
alternates: {
canonical: `/${params.locale}/blog/${post.slug}`,
languages,
},
}
}
Next.js Metadata API serializes alternates.languages into <link rel="alternate" hreflang="..."> tags automatically.
4. The sitemap
app/sitemap.ts must list every URL for every locale, with correct lastModified:
export default async function sitemap() {
const posts = await getAllPostsAllLocales()
return posts.map(post => ({
url: `https://cloudtopia.net/${post.lang}/blog/${post.slug}`,
lastModified: post.updated ?? post.date,
alternates: {
languages: buildLanguageMap(post.id),
},
}))
}
Submit the sitemap in Google Search Console under each locale property, or (simpler) use a single domain property and let Google discover locales from hreflang.
5. Per-locale metadata — not translated metadata
Translate titles, descriptions, and OG images natively per locale. A translated English title often becomes a 70-character Arabic title (Arabic is denser). Rewrite it:
- EN:
Gulf Payment Gateways: Mada, Apple Pay, STC Pay, Tabby & Tamara (2026 Guide) - AR:
بوابات الدفع الخليجية: مدى وApple Pay وSTC Pay وتابي وتمارا (دليل 2026) - TR:
Körfez Ödeme Geçitleri: Mada, Apple Pay, STC Pay, Tabby ve Tamara (2026 Rehberi)
Same intent, native phrasing, optimal length for each SERP.
6. Structured data per locale
BlogPosting, FAQPage, BreadcrumbList, Person — all should emit with inLanguage: 'ar' (or en-SA, tr-TR) matching the page locale. Machine translation of structured data is fine; wrong inLanguage tags confuse Google.
7. Internal linking across locales
Never link from an Arabic blog post to an English service page. Always link within locale. Use a helper:
function localeLink(path: string, locale: string) {
return `/${locale}${path}`
}
// In MDX/JSX: <Link href={localeLink('/pricing', locale)}>
This keeps crawl paths clean and each locale fully self-contained from a discovery perspective.
Checklist
- [ ]
app/[locale]/segment with middleware - [ ]
<html lang dir>set fromparams.locale - [ ] Frontmatter uses canonical
id+ locale-specificslug - [ ]
getSlugByIdhelper used in metadata and language switcher - [ ]
generateMetadataemitsalternates.languages - [ ]
sitemap.tsincludes every locale × slug withlastModified - [ ] OG images localized (Arabic OG should be in Arabic)
- [ ] Structured data
inLanguagematches page locale - [ ] Internal links stay within current locale
Common questions about multilingual SEO in Next.js
Native script. Google rankings in Arabic markets reward Arabic keywords in the URL. Same for Turkish — use Turkish orthography, not English transliteration.
The honest summary
Multilingual SEO in Next.js isn't hard, but it's unforgiving. Get the [locale] segment, native-script slugs, canonical IDs, hreflang, and per-locale metadata right on day one. Retrofitting these later requires URL redirects and ranking recovery that takes months.
The teams that treat each locale as a first-class site — native URLs, native copy, native metadata — outrank competitors who shipped a translation-layer site, every single time.
