I18n Implementation - NextJS v14 404 & Middleware Errors

Describe your question/issue in detail

We're working with a project which requires internationalisation and have been looking through a variety of documents to determine how this works in Prismic (sources in next section).

After enabling en-gb and a custom locale value we've got the locales configured in nextjs.config.mjs as expected. When running locally if we call http://localhost:3000/ we get a scenario where a 404 is thrown by NextJS before it hits the middleware and subsequently seems to terminate any remaining processes.

[next]  GET / 404 in 237ms
[next] CONSOLE LOG: MIDDLEWARE TEST
[next]  GET /favicon.ico 200 in 5ms

If we call http://localhost:3000/en-gb the same issue occurs and it never reaches the console logs in the page.tsx files. The only time it does seem to trigger semi-correctly is when it's called as http://localhost:3000/en_gb which is obviously incorrect and results in this expected error.

[next] MIDDLEWARE TEST
[next] CONSOLE LOG: Page Content
[next] { lang: 'en_gb' }
[next] CONSOLE LOG: Page Meta Data
[next] { lang: 'en_gb' }
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Index (./src/app/[lang]/page.tsx:47:18)
[next] digest: "1732355935"
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Index (./src/app/[lang]/page.tsx:47:18)
[next] digest: "1732355935"
[next] CONSOLE LOG: Page Meta Data
[next] { lang: 'en_gb' }
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Module.generateMetadata (./src/app/[lang]/page.tsx:27:18)
[next] digest: "3879409013"
[next]  ⨯ Error: Some(en_gb) is not a valid language code
[next]     at async Module.generateMetadata (./src/app/[lang]/page.tsx:27:18)
[next] digest: "3879409013"
[next]  GET /en_gb 500 in 948ms
[next] MIDDLEWARE TEST
[next]  GET /favicon.ico 200 in 4ms

When we've added in a work around for the above example (.replace on the underscore to convert it to a hyphen) we know the languages are available as we've got them embedded on the page. These link to the correct places as expected but clicking on any of the links results in a 404 being thrown.

Any advice on fixing this issue would be greatly appreciated.

What steps have you taken to resolve this issue already?

Investigated the following articles and forum posts (amongst others):

Code

I'm unable to provide you with a link to a GitHub repository as the code is sat behind a private repo so the following information is the best I can provide.

Directory Structure

  • src
    • app
      • [lang]
        • [uid]
          • page.tsx
        • article
          • page.tsx
        • page.tsx
        • slice-simulator
      • api
      • favicon.ico
      • icon.png
      • layout.tsx

next.config.mjs

/** @type {Promise<import('next').NextConfig>} */
const nextConfig = async () => {
  const client = createClient(sm.repositoryName);

  const { languages } = await client.getRepository();
  const locales = languages.map((lang) => lang.id);

  const defaultLocale = languages.find(({ is_master }) => is_master).id;

  return {
    i18n: { locales, defaultLocale: defaultLocale, localeDetection: false },
    // Other configuration
  };
};

export default nextConfig;

prismic.ts

// Out of the box imports and exports

const routes: prismic.ClientConfig['routes'] = [
  { type: 'home', path: '/:lang?', uid: 'home' },
  { type: 'page', path: '/:lang?/:uid' },
  { type: 'article', path: '/:lang?/article/:uid' },
];

// Out of the box createClient function 

middleware.ts

import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/prismicio';

export async function middleware(request: NextRequest) {
  const client = createClient();
  const repository = await client.getRepository();

  const locales = repository.languages.map((lang) => lang.id);
  const defaultLocale = locales[0];

  // Check if there is any supported locale in the pathname
  const { pathname } = request.nextUrl;

  const pathnameIsMissingLocale = locales.every(
    (locale) =>
      !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
  );

  // Redirect to default locale if there is no supported locale prefix
  if (pathnameIsMissingLocale) {
    return NextResponse.rewrite(
      new URL(`/${defaultLocale}${pathname}`, request.url),
    );
  }
}

export const config = {
  matcher: ['/((?!_next).*)'],
};

app/page.tsx

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  console.log('Language Meta Data');
  console.log(params);
  const client = createClient();
  const home = await client.getSingle('home', { lang: params.lang });

  return {
    title: prismic.asText(home.data.title),
    description: home.data.meta_description,
    openGraph: {
      title: home.data.meta_title ?? undefined,
      images: [{ url: home.data.meta_image.url ?? '' }],
    },
  };
}

export default async function Index({ params }: PageProps) {
  console.log('Language Page');
  console.log(params);
  const client = createClient();
  const page = await client.getSingle('home', { lang: params.lang });

  const { slices } = page.data;

  return (
    <Layout language={params.lang} page={page}>
      <SliceZone slices={slices} components={components} />
    </Layout>
  );
}

[uid]/page.tsx

export async function generateMetadata({
  params: { lang, uid },
}: PageProps): Promise<Metadata> {
  const client = createClient();

  const page = await client
    .getByUID('page', uid, { lang: lang })
    .catch(() => notFound());

  return {
    title: prismic.asText(page.data.title),
    description: page.data.meta_description,
    openGraph: {
      title: page.data.meta_title || undefined,
      images: [
        {
          url: page.data.meta_image.url || '',
        },
      ],
    },
  };
}

export default async function Page({ params: { lang, uid } }: PageProps) {
  const client = createClient();

  const page = await client
    .getByUID('page', uid, { lang: lang })
    .catch(() => notFound());

  return (
    <Layout language={lang} page={page}>
      <SliceZone slices={page.data.slices} components={components} />
    </Layout>
  );
}

export async function generateStaticParams() {
  const client = createClient();

  const pages = await client.getAllByType('page', { lang: '*' });

  return pages.map(({ uid, lang }) => ({ uid: uid, lang: lang }));
}

package.json

{
  "name": "nextjs-starter-prismic-minimal-ts",
  "version": "0.1.0",
  "private": true,
  "license": "Apache-2.0",
  "author": "Prismic <contact@prismic.io> (https://prismic.io)",
  "scripts": {
    "dev": "concurrently \"npm:next:dev\" \"npm:slicemachine\" --names \"next,slicemachine\" --prefix-colors blue,magenta",
    "next:dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "slicemachine": "start-slicemachine",
    "format": "prettier --write ."
  },
  "dependencies": {
    "@prismicio/client": "^7.5.0",
    "@prismicio/next": "^1.5.0",
    "@prismicio/react": "^2.7.4",
    "classnames": "^2.5.1",
    "next": "^14.2.3",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "uuid": "^9.0.1"
  },
  "devDependencies": {
    "@slicemachine/adapter-next": "^0.3.38",
    "@svgr/webpack": "^8.1.0",
    "@types/node": "^20.12.11",
    "@types/react": "^18.3.1",
    "@types/react-dom": "^18.3.0",
    "@types/uuid": "^9.0.8",
    "concurrently": "^8.2.2",
    "eslint": "^8",
    "eslint-config-next": "^14.2.3",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-react-hooks": "^4.6.2",
    "prettier": "^3.2.5",
    "sass": "^1.77.1",
    "slice-machine-ui": "^1.26.0",
    "typescript": "^5.4.5"
  }
}
2 Likes

Hi @damian.connolly, thank you for providing lots of details. :slight_smile:

Could you take a look at this thread and give the recommended solution a try?

Our i18n documentation and start is out dated as we did not decide on a final direction internally, but what is described in that thread can be used today.

Notably, you can make the following changes to your setup:

  • Remove the i18n property and the local-related lines from next.config.js. The i18n property is designed for the Pages Router, which your website does not seem to use.
  • Remove the createClient() and client.getRepository() lines from middleware.ts. Fetching a repository's locale in middleware will cause every page to be delayed by ~300 ms, which should be avoided if possible.

I hope that helps! We are aware that we need to update our i18n and clarify the setup.

Apologies for the late reply, I hadn't seen a response come through.

Managed to resolve this issue not long after posting. The issue was with the next.config.mjs file and I've simplified the approach based on a GitHub application we were linked to by a person at Prismic.

Out of curiosity, is there a plan for the Developer documentation to be updated to support NextJS 14 and more specifically the App Router as everything right now points at the Page Router?

Hi Damian,

Most of the dev docs for Next.js feature tabs for both the app router and the page router.

I found these examples in the 'How to' section which we need to update:

Everything in the 'Guide' section should be good.

We'll prioritise this, and if you have any other examples that we need to update, let me know.

Thanks.

1 Like

Thanks for identifying those articles to be updated.

I've been actively avoiding the Developer Documentation for Prismic due to inconsistencies between figuring which are App Router vs Prismic Router. If I find any further inconsistencies I'll tag them onto this post.

1 Like