Manual i18n with Static Export
Next.js’s standard Internationalized Routing does not support output: 'export', which is required for hosting on Cloudflare Pages (Static). This guide explains how to implement multilingual support manually.
Directory Structure
Instead of relying on Next.js routing, we physically separate content into language folders.
pages/
├── index.mdx # Root redirect
├── _meta.js # Root navigation config
├── en/ # English content root
│ ├── index.mdx
│ └── ...
└── ja/ # Japanese content root
├── index.mdx
└── ...1. Root Configuration
Root Redirect (pages/index.mdx)
Create a root page that immediately redirects to your default language (e.g., English).
pages/index.mdx
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function Index() {
const router = useRouter()
useEffect(() => {
router.replace('/en')
}, [])
return null
}Root Meta (pages/_meta.js)
Define your language folders as top-level pages. Do not use display: 'hidden' here, as it may hide the sidebar navigation entirely.
pages/_meta.js
export default {
"index": {
"display": "hidden" // Hide the redirect page itself
},
"en": {
"title": "English",
"type": "page"
},
"ja": {
"title": "日本語",
"type": "page"
}
}2. Language Switcher
Since we are using custom paths, we need a custom language switcher component in theme.config.tsx.
theme.config.tsx
import { useRouter } from 'next/router'
const LanguageSwitch = () => {
const { asPath } = useRouter()
const isJa = asPath.startsWith('/ja')
// Toggle logic: /ja/foo -> /en/foo, /en/foo -> /ja/foo
// Handle root case defaulting to /en
const currentPath = asPath === '/' ? '/en' : asPath
const targetPath = isJa
? currentPath.replace('/ja', '/en')
: currentPath.replace('/en', '/ja')
return (
<a href={targetPath}>
{isJa ? 'English' : '日本語'}
</a>
)
}
const config = {
// ...
navbar: {
extraContent: <LanguageSwitch />
}
}Benefits
- Fully Static: Works perfectly with Cloudflare Pages and
next export. - Isolated Navigation: Users viewing English docs won’t see Japanese page titles in the sidebar, and vice versa.
- Simple: No complex middleware or edge functions required.