Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Improvements for domain-based routing docs and add MDX docs #1176

Merged
merged 9 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/pages/docs/environments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The `next-intl` APIs are available in the following environments:
title="Error files (e.g. not-found)"
href="/docs/environments/error-files"
/>
<Card title="Markdown (MDX)" href="/docs/environments/mdx" />
<Card
title="Core library (agnostic)"
href="/docs/environments/core-library"
Expand Down
1 change: 1 addition & 0 deletions docs/pages/docs/environments/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"server-client-components": "Server & Client Components",
"actions-metadata-route-handlers": "Server Actions, Metadata & Route Handlers",
"error-files": "Error files (e.g. not-found)",
"mdx": "Markdown (MDX)",
"core-library": "Core library",
"runtime-requirements": "Runtime requirements"
}
60 changes: 60 additions & 0 deletions docs/pages/docs/environments/mdx.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Details from 'components/Details';
import PartnerContentLink from 'components/PartnerContentLink';

# Markdown (MDX)

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.

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.

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:

```
src
└── app
└── [locale]
├── page.tsx
├── en.mdx
└── de.mdx
```

Now, in `page.tsx`, you can import the MDX content based on the user's locale:

```tsx filename="src/app/[locale]/page.tsx"
export default async function HomePage({params}) {
const Content = (await import(`./${params.locale}.mdx`)).default;
return <Content />;
}
```

In this example, an MDX file might look like this:

```mdx filename="src/app/[locale]/en.mdx"
import Portrait from '@/components/Portrait';

# Home

Welcome to my site!

<Portrait />
```

Components that invoke hooks from `next-intl` like `useTranslations` can naturally be used in MDX content and will respect the user's locale.

<Details id="rich-text">
<summary>Is MDX required to format rich text?</summary>

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.

</Details>

<Details id="remote-files">
<summary>Can I load MDX content from a remote source?</summary>

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>.

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).

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).

</Details>
28 changes: 15 additions & 13 deletions docs/pages/docs/routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ src
└── navigation.ts
```

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

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

</Tab>
<Tab>
… and imported into both `middleware.ts` and `navigation.ts`:

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

</Tab>
<Tab>

```tsx filename="src/navigation.ts"
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
import {locales, /* ... */} from './config';
Expand All @@ -71,9 +66,6 @@ export const {Link, redirect, usePathname, useRouter} =
createSharedPathnamesNavigation({locales, /* ... */});
```

</Tab>
</Tabs>

### Locale prefix

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.
Expand Down Expand Up @@ -139,7 +131,7 @@ In this case, requests for all locales will be rewritten to have the locale only

1. If you use this strategy, you should make sure that your matcher detects [unprefixed pathnames](/docs/routing/middleware#matcher-no-prefix).
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).
3. [Alternate links](/docs/routing/middleware#alternate-links) are disabled in this mode since URLs might not be unique per locale.
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`.

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

Expand Down Expand Up @@ -242,7 +234,10 @@ export const pathnames = {
**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]]`.

<Callout>
If you're using localized pathnames, you should use `createLocalizedPathnamesNavigation` instead of `createSharedPathnamesNavigation` for your [navigation APIs](/docs/routing/navigation).
If you're using localized pathnames, you should use
`createLocalizedPathnamesNavigation` instead of
`createSharedPathnamesNavigation` for your [navigation
APIs](/docs/routing/navigation).
</Callout>

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

**Note that:**

1. You can optionally remove the locale prefix in pathnames by changing the [`localePrefix`](#locale-prefix) setting.
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.
2. If no domain matches, the middleware will fall back to the [`defaultLocale`](/docs/routing/middleware#default-locale) (e.g. on `localhost`).

<Details id="domains-testing">
<summary>How can I locally test if my setup is working?</summary>

Learn more about this in the [locale detection for domain-based routing](/docs/routing/middleware#location-detection-domain) docs.

</Details>
17 changes: 15 additions & 2 deletions docs/pages/docs/routing/middleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ In contrast, the "best fit" algorithm compares the _distance_ between the user's

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

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`.
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).

The locale is detected based on these priorities:

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

</Details>

<Details id="domain-local-testing">
<summary>How can I locally test if my setup is working?</summary>

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:

```
X-Forwarded-Host: example.com
```

With this, your domain config for this particular domain will be used.

</Details>

## Configuration

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.
Expand Down Expand Up @@ -526,7 +539,7 @@ export const config = {

<Callout>

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

</Callout>

Expand Down
7 changes: 7 additions & 0 deletions examples/example-app-router-playground/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {MDXComponents} from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components
};
}
1 change: 1 addition & 0 deletions examples/example-app-router-playground/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"switchLocale": "Zu {locale, select, de {Deutsch} en {Englisch} other {Unbekannt}} wechseln"
},
"Navigation": {
"about": "Über (MDX)",
"client": "Client-Seite",
"home": "Start",
"nested": "Verschachtelte Seite",
Expand Down
1 change: 1 addition & 0 deletions examples/example-app-router-playground/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"switchLocale": "Switch to {locale, select, de {German} en {English} other {Unknown}}"
},
"Navigation": {
"about": "About (MDX)",
"client": "Client page",
"home": "Home",
"nested": "Nested page",
Expand Down
1 change: 1 addition & 0 deletions examples/example-app-router-playground/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"switchLocale": "Cambiar a {locale, select, de {Alemán} en {Inglés} other {Desconocido}}"
},
"Navigation": {
"about": "Acerca de (MDX)",
"client": "Página del cliente",
"home": "Inicio",
"nested": "Página anidada",
Expand Down
11 changes: 6 additions & 5 deletions examples/example-app-router-playground/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
"switchLocale": ""
},
"Navigation": {
"client": "",
"home": "",
"nested": "",
"newsArticle": ""
"about": "(MDX) について",
"client": "クライアントページ",
"home": "家",
"nested": "ネストされたページ",
"newsArticle": "ニュース記事"
},
"Nested": {
"description": "これはネストされたページです。",
Expand All @@ -49,7 +50,7 @@
"title": ""
},
"NotFound": {
"title": ""
"title": "このページは見つかりませんでした (404)"
},
"OpenGraph": {
"title": ""
Expand Down
23 changes: 14 additions & 9 deletions examples/example-app-router-playground/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
// @ts-check

import mdxPlugin from '@next/mdx';
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin('./src/i18n.tsx');
export default withNextIntl({
trailingSlash: process.env.TRAILING_SLASH === 'true',
experimental: {
staleTimes: {
// Next.js 14.2 broke `locale-prefix-never.spec.ts`.
// This is a workaround for the time being.
dynamic: 0
const withMdx = mdxPlugin();

export default withMdx(
withNextIntl({
trailingSlash: process.env.TRAILING_SLASH === 'true',
experimental: {
staleTimes: {
// Next.js 14.2 broke `locale-prefix-never.spec.ts`.
// This is a workaround for the time being.
dynamic: 0
}
}
}
});
})
);
4 changes: 4 additions & 0 deletions examples/example-app-router-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"start": "next start"
},
"dependencies": {
"@mdx-js/react": "^3.0.1",
"lodash": "^4.17.21",
"ms": "2.1.3",
"next": "^14.2.4",
Expand All @@ -23,10 +24,13 @@
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@mdx-js/loader": "^3.0.1",
"@next/mdx": "^14.2.5",
"@playwright/test": "^1.44.1",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.5",
"@types/mdx": "^2.0.13",
"@types/node": "^20.14.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AsyncComponent from '@/components/AsyncComponent';
import Counter from '@/components/client/02-MessagesOnClientCounter/Counter';

# Über uns

Ein bisschen Text …

<hr />

<AsyncComponent />
<Counter />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AsyncComponent from '@/components/AsyncComponent';
import Counter from '@/components/client/02-MessagesOnClientCounter/Counter';

# About

Some text …

<hr />

<AsyncComponent />
<Counter />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AsyncComponent from '@/components/AsyncComponent';
import Counter from '@/components/client/02-MessagesOnClientCounter/Counter';

# Acerca de

Algun texto ...

<hr />

<AsyncComponent />
<Counter />
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
type Props = {
params: {
locale: string;
};
};

export default async function AboutPage({params}: Props) {
const Content = (await import(`./${params.locale}.mdx`)).default;
return <Content />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default function Navigation() {
<nav style={{display: 'flex', gap: 10}}>
<NavigationLink href="/">{t('home')}</NavigationLink>
<NavigationLink href="/client">{t('client')}</NavigationLink>
<NavigationLink href="/about">{t('about')}</NavigationLink>
<NavigationLink href="/nested">{t('nested')}</NavigationLink>
<NavigationLink
href={{pathname: '/news/[articleId]', params: {articleId: 3}}}
Expand Down
1 change: 1 addition & 0 deletions examples/example-app-router-playground/src/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const localePrefix = (
export const pathnames = {
'/': '/',
'/client': '/client',
'/about': '/about',
'/client/redirect': '/client/redirect',
'/nested': {
en: '/nested',
Expand Down
5 changes: 4 additions & 1 deletion examples/example-app-router-playground/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
{
"name": "next"
}
]
],
"paths": {
"@/components/*": ["./src/components/*"]
}
},
"include": [
"next-env.d.ts",
Expand Down
Loading
Loading