Skip to content

Commit 98f1d2d

Browse files
authored
docs: Improvements for domain-based routing docs and add MDX docs (#1176)
1 parent 805cfae commit 98f1d2d

File tree

20 files changed

+857
-176
lines changed

20 files changed

+857
-176
lines changed

docs/pages/docs/environments.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The `next-intl` APIs are available in the following environments:
1818
title="Error files (e.g. not-found)"
1919
href="/docs/environments/error-files"
2020
/>
21+
<Card title="Markdown (MDX)" href="/docs/environments/mdx" />
2122
<Card
2223
title="Core library (agnostic)"
2324
href="/docs/environments/core-library"

docs/pages/docs/environments/_meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"server-client-components": "Server & Client Components",
33
"actions-metadata-route-handlers": "Server Actions, Metadata & Route Handlers",
44
"error-files": "Error files (e.g. not-found)",
5+
"mdx": "Markdown (MDX)",
56
"core-library": "Core library",
67
"runtime-requirements": "Runtime requirements"
78
}

docs/pages/docs/environments/mdx.mdx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Details from 'components/Details';
2+
import PartnerContentLink from 'components/PartnerContentLink';
3+
4+
# Markdown (MDX)
5+
6+
Especially for sites where the content varies significantly by locale and may require a different structure, it can be helpful to use Markdown or [MDX](https://mdxjs.com) to provide your localized content. To consume this content in a Next.js app, you can use the [`@next/mdx`](https://nextjs.org/docs/app/building-your-application/configuring/mdx) package, which allows you to import and render MDX content.
7+
8+
While you can create entire pages using `page.mdx` files, in an app that uses [the `[locale]` segment](/docs/getting-started/app-router), it can be beneficial to import localized MDX content based on the user's locale into a single `page.tsx` file.
9+
10+
After following the [setup instructions for `@next/mdx`](https://nextjs.org/docs/app/building-your-application/configuring/mdx), you can consider placing your localized MDX files next to a page that will render them:
11+
12+
```
13+
src
14+
└── app
15+
└── [locale]
16+
├── page.tsx
17+
├── en.mdx
18+
└── de.mdx
19+
```
20+
21+
Now, in `page.tsx`, you can import the MDX content based on the user's locale:
22+
23+
```tsx filename="src/app/[locale]/page.tsx"
24+
export default async function HomePage({params}) {
25+
const Content = (await import(`./${params.locale}.mdx`)).default;
26+
return <Content />;
27+
}
28+
```
29+
30+
In this example, an MDX file might look like this:
31+
32+
```mdx filename="src/app/[locale]/en.mdx"
33+
import Portrait from '@/components/Portrait';
34+
35+
# Home
36+
37+
Welcome to my site!
38+
39+
<Portrait />
40+
```
41+
42+
Components that invoke hooks from `next-intl` like `useTranslations` can naturally be used in MDX content and will respect the user's locale.
43+
44+
<Details id="rich-text">
45+
<summary>Is MDX required to format rich text?</summary>
46+
47+
Not at all! Messages support [rich text syntax](/docs/usage/messages#rich-text), which can be used to provide formatting, structure and embedding of components.
48+
49+
</Details>
50+
51+
<Details id="remote-files">
52+
<summary>Can I load MDX content from a remote source?</summary>
53+
54+
Especially if you'd like to allow translators to collaborate on MDX files, you can consider uploading them to a translation management system like <PartnerContentLink href="https://crowdin.com/">Crowdin</PartnerContentLink>.
55+
56+
In this case, you can fetch the MDX content dynamically from within a page and parse it using a package like [`next-mdx-remote`](https://nextjs.org/docs/app/building-your-application/configuring/mdx#remote-mdx).
57+
58+
Note that MDX compiles to JavaScript and is dynamically evaluated. You should be sure to only load MDX content from a trusted source, otherwise this can lead to [arbitrary code execution](https://en.wikipedia.org/wiki/Arbitrary_code_execution).
59+
60+
</Details>

docs/pages/docs/routing.mdx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ src
2929
└── navigation.ts
3030
```
3131

32-
<Tabs items={['config.ts', 'middleware.ts', 'navigation.ts']}>
33-
<Tab>
32+
This shared module can be set up like this:
3433

3534
```tsx filename="config.ts"
3635
// A list of all locales that are supported
@@ -39,8 +38,7 @@ export const locales = ['en', 'de'] as const;
3938
// ...
4039
```
4140

42-
</Tab>
43-
<Tab>
41+
… and imported into both `middleware.ts` and `navigation.ts`:
4442

4543
```tsx filename="middleware.ts"
4644
import createMiddleware from 'next-intl/middleware';
@@ -60,9 +58,6 @@ export const config = {
6058
};
6159
```
6260

63-
</Tab>
64-
<Tab>
65-
6661
```tsx filename="src/navigation.ts"
6762
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
6863
import {locales, /* ... */} from './config';
@@ -71,9 +66,6 @@ export const {Link, redirect, usePathname, useRouter} =
7166
createSharedPathnamesNavigation({locales, /* ... */});
7267
```
7368

74-
</Tab>
75-
</Tabs>
76-
7769
### Locale prefix
7870

7971
By default, the pathnames of your app will be available under a prefix that matches your directory structure (e.g. `app/[locale]/about/page.tsx``/en/about`). You can however adapt the routing to optionally remove the prefix or customize it per locale by configuring the `localePrefix` setting.
@@ -139,7 +131,7 @@ In this case, requests for all locales will be rewritten to have the locale only
139131

140132
1. If you use this strategy, you should make sure that your matcher detects [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix).
141133
2. If you don't use domain-based routing, the cookie is now the source of truth for determining the locale in the middleware. Make sure that your hosting solution reliably returns the `set-cookie` header from the middleware (e.g. Vercel and Cloudflare are known to potentially [strip this header](https://developers.cloudflare.com/cache/concepts/cache-behavior/#interaction-of-set-cookie-response-header-with-cache) for cacheable requests).
142-
3. [Alternate links](/docs/routing/middleware#alternate-links) are disabled in this mode since URLs might not be unique per locale.
134+
3. [Alternate links](/docs/routing/middleware#alternate-links) are disabled in this mode since URLs might not be unique per locale. Due to this, consider including these yourself, or set up a [sitemap](/docs/environments/actions-metadata-route-handlers#sitemap) that links localized pages via `alternates`.
143135

144136
#### Custom prefixes [#locale-prefix-custom]
145137

@@ -242,7 +234,10 @@ export const pathnames = {
242234
**Note:** Localized pathnames map to a single internal pathname that is created via the file-system based routing in Next.js. If you're using an external system like a CMS to localize pathnames, you'll typically implement this with a catch-all route like `[locale]/[[...slug]]`.
243235

244236
<Callout>
245-
If you're using localized pathnames, you should use `createLocalizedPathnamesNavigation` instead of `createSharedPathnamesNavigation` for your [navigation APIs](/docs/routing/navigation).
237+
If you're using localized pathnames, you should use
238+
`createLocalizedPathnamesNavigation` instead of
239+
`createSharedPathnamesNavigation` for your [navigation
240+
APIs](/docs/routing/navigation).
246241
</Callout>
247242

248243
<Details id="localized-pathnames-revalidation">
@@ -343,5 +338,12 @@ export const domains: DomainsConfig<typeof locales> = [
343338

344339
**Note that:**
345340

346-
1. You can optionally remove the locale prefix in pathnames by changing the [`localePrefix`](#locale-prefix) setting.
341+
1. You can optionally remove the locale prefix in pathnames by changing the [`localePrefix`](#locale-prefix) setting. E.g. [`localePrefix: 'never'`](/docs/routing#locale-prefix-never) can be helpful in case you have unique domains per locale.
347342
2. If no domain matches, the middleware will fall back to the [`defaultLocale`](/docs/routing/middleware#default-locale) (e.g. on `localhost`).
343+
344+
<Details id="domains-testing">
345+
<summary>How can I locally test if my setup is working?</summary>
346+
347+
Learn more about this in the [locale detection for domain-based routing](/docs/routing/middleware#location-detection-domain) docs.
348+
349+
</Details>

docs/pages/docs/routing/middleware.mdx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ In contrast, the "best fit" algorithm compares the _distance_ between the user's
7373

7474
### Domain-based routing [#location-detection-domain]
7575

76-
If you're using [domain-based routing](/docs/routing#domains), the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the `x-forwarded-host` header, with a fallback to `host`.
76+
If you're using [domain-based routing](/docs/routing#domains), the middleware will match the request against the available domains to determine the best-matching locale. To retrieve the domain, the host is read from the `x-forwarded-host` header, with a fallback to `host` (hosting platforms typically provide these headers out-of-the-box).
7777

7878
The locale is detected based on these priorities:
7979

@@ -103,6 +103,19 @@ The bestmatching domain is detected based on these priorities:
103103

104104
</Details>
105105

106+
<Details id="domain-local-testing">
107+
<summary>How can I locally test if my setup is working?</summary>
108+
109+
Since the negotiated locale depends on the host of the request, you can test your setup by attaching a corresponding `x-forwarded-host` header. To achieve this in the browser, you can use a browser extension like [ModHeader in Chrome](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj) and add a setting like:
110+
111+
```
112+
X-Forwarded-Host: example.com
113+
```
114+
115+
With this, your domain config for this particular domain will be used.
116+
117+
</Details>
118+
106119
## Configuration
107120

108121
The middleware accepts a number of configuration options that are [shared](/docs/routing#shared-configuration) with the [navigation APIs](/docs/routing/navigation). This list contains all options that are specific to the middleware.
@@ -526,7 +539,7 @@ export const config = {
526539

527540
<Callout>
528541

529-
There's a working [example that combines `next-intl` with Auth.js](/examples#app-router-next-auth) on GitHub.
542+
Have a look at the [`next-intl` with NextAuth.js example](/examples#app-router-next-auth) to explore a working setup.
530543

531544
</Callout>
532545

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type {MDXComponents} from 'mdx/types';
2+
3+
export function useMDXComponents(components: MDXComponents): MDXComponents {
4+
return {
5+
...components
6+
};
7+
}

examples/example-app-router-playground/messages/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"switchLocale": "Zu {locale, select, de {Deutsch} en {Englisch} other {Unbekannt}} wechseln"
3737
},
3838
"Navigation": {
39+
"about": "Über (MDX)",
3940
"client": "Client-Seite",
4041
"home": "Start",
4142
"nested": "Verschachtelte Seite",

examples/example-app-router-playground/messages/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"switchLocale": "Switch to {locale, select, de {German} en {English} other {Unknown}}"
3737
},
3838
"Navigation": {
39+
"about": "About (MDX)",
3940
"client": "Client page",
4041
"home": "Home",
4142
"nested": "Nested page",

examples/example-app-router-playground/messages/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"switchLocale": "Cambiar a {locale, select, de {Alemán} en {Inglés} other {Desconocido}}"
3737
},
3838
"Navigation": {
39+
"about": "Acerca de (MDX)",
3940
"client": "Página del cliente",
4041
"home": "Inicio",
4142
"nested": "Página anidada",

examples/example-app-router-playground/messages/ja.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@
3636
"switchLocale": ""
3737
},
3838
"Navigation": {
39-
"client": "",
40-
"home": "",
41-
"nested": "",
42-
"newsArticle": ""
39+
"about": "(MDX) について",
40+
"client": "クライアントページ",
41+
"home": "",
42+
"nested": "ネストされたページ",
43+
"newsArticle": "ニュース記事"
4344
},
4445
"Nested": {
4546
"description": "これはネストされたページです。",
@@ -49,7 +50,7 @@
4950
"title": ""
5051
},
5152
"NotFound": {
52-
"title": ""
53+
"title": "このページは見つかりませんでした (404)"
5354
},
5455
"OpenGraph": {
5556
"title": ""
Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
// @ts-check
22

3+
import mdxPlugin from '@next/mdx';
34
import createNextIntlPlugin from 'next-intl/plugin';
45

56
const withNextIntl = createNextIntlPlugin('./src/i18n.tsx');
6-
export default withNextIntl({
7-
trailingSlash: process.env.TRAILING_SLASH === 'true',
8-
experimental: {
9-
staleTimes: {
10-
// Next.js 14.2 broke `locale-prefix-never.spec.ts`.
11-
// This is a workaround for the time being.
12-
dynamic: 0
7+
const withMdx = mdxPlugin();
8+
9+
export default withMdx(
10+
withNextIntl({
11+
trailingSlash: process.env.TRAILING_SLASH === 'true',
12+
experimental: {
13+
staleTimes: {
14+
// Next.js 14.2 broke `locale-prefix-never.spec.ts`.
15+
// This is a workaround for the time being.
16+
dynamic: 0
17+
}
1318
}
14-
}
15-
});
19+
})
20+
);

examples/example-app-router-playground/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"start": "next start"
1414
},
1515
"dependencies": {
16+
"@mdx-js/react": "^3.0.1",
1617
"lodash": "^4.17.21",
1718
"ms": "2.1.3",
1819
"next": "^14.2.4",
@@ -23,10 +24,13 @@
2324
},
2425
"devDependencies": {
2526
"@jest/globals": "^29.7.0",
27+
"@mdx-js/loader": "^3.0.1",
28+
"@next/mdx": "^14.2.5",
2629
"@playwright/test": "^1.44.1",
2730
"@testing-library/react": "^16.0.0",
2831
"@types/jest": "^29.5.12",
2932
"@types/lodash": "^4.17.5",
33+
"@types/mdx": "^2.0.13",
3034
"@types/node": "^20.14.5",
3135
"@types/react": "^18.3.3",
3236
"@types/react-dom": "^18.3.0",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import AsyncComponent from '@/components/AsyncComponent';
2+
import Counter from '@/components/client/02-MessagesOnClientCounter/Counter';
3+
4+
# Über uns
5+
6+
Ein bisschen Text …
7+
8+
<hr />
9+
10+
<AsyncComponent />
11+
<Counter />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import AsyncComponent from '@/components/AsyncComponent';
2+
import Counter from '@/components/client/02-MessagesOnClientCounter/Counter';
3+
4+
# About
5+
6+
Some text …
7+
8+
<hr />
9+
10+
<AsyncComponent />
11+
<Counter />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import AsyncComponent from '@/components/AsyncComponent';
2+
import Counter from '@/components/client/02-MessagesOnClientCounter/Counter';
3+
4+
# Acerca de
5+
6+
Algun texto ...
7+
8+
<hr />
9+
10+
<AsyncComponent />
11+
<Counter />
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Props = {
2+
params: {
3+
locale: string;
4+
};
5+
};
6+
7+
export default async function AboutPage({params}: Props) {
8+
const Content = (await import(`./${params.locale}.mdx`)).default;
9+
return <Content />;
10+
}

examples/example-app-router-playground/src/components/Navigation.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default function Navigation() {
88
<nav style={{display: 'flex', gap: 10}}>
99
<NavigationLink href="/">{t('home')}</NavigationLink>
1010
<NavigationLink href="/client">{t('client')}</NavigationLink>
11+
<NavigationLink href="/about">{t('about')}</NavigationLink>
1112
<NavigationLink href="/nested">{t('nested')}</NavigationLink>
1213
<NavigationLink
1314
href={{pathname: '/news/[articleId]', params: {articleId: 3}}}

examples/example-app-router-playground/src/navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const localePrefix = (
1717
export const pathnames = {
1818
'/': '/',
1919
'/client': '/client',
20+
'/about': '/about',
2021
'/client/redirect': '/client/redirect',
2122
'/nested': {
2223
en: '/nested',

examples/example-app-router-playground/tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
{
2222
"name": "next"
2323
}
24-
]
24+
],
25+
"paths": {
26+
"@/components/*": ["./src/components/*"]
27+
}
2528
},
2629
"include": [
2730
"next-env.d.ts",

0 commit comments

Comments
 (0)