Skip to content

Commit

Permalink
feat(i18n): language routing
Browse files Browse the repository at this point in the history
introduces a new level in the Next.js dynamic routing: [lang] to specify the desired display language

supported by a middleware to rewrite paths to include language code
  • Loading branch information
mathiazom committed Sep 26, 2024
1 parent 7bf52a8 commit 04b7fbf
Show file tree
Hide file tree
Showing 15 changed files with 334 additions and 104 deletions.
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"@sanity/preview-url-secret": "^1.6.11",
"@sanity/react-loader": "^1.9.15",
"@sanity/vision": "^3.39.1",
"@types/negotiator": "^0.6.3",
"negotiator": "^0.6.3",
"next": "^14.2.13",
"next-sanity": "^7.1.4",
"next-sanity-image": "^6.1.1",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Metadata } from "next";
import { Blog } from "src/blog/Blog";
import BlogPreview from "src/blog/BlogPreview";
import CustomErrorMessage from "src/blog/components/customErrorMessage/CustomErrorMessage";
import Legal from "src/blog/components/legal/Legal";
import LegalPreview from "src/blog/components/legal/LegalPreview";
import { homeLink } from "src/blog/components/utils/linkTypes";
import Compensations from "src/compensations/Compensations";
import CompensationsPreview from "src/compensations/CompensationsPreview";
Expand All @@ -13,9 +15,11 @@ import SectionRenderer from "src/utils/renderSection";
import { fetchSeoData, generateMetadataFromSeo } from "src/utils/seo";
import { CompanyLocation } from "studio/lib/interfaces/companyDetails";
import { CompensationsPage } from "studio/lib/interfaces/compensations";
import { LegalDocument } from "studio/lib/interfaces/legalDocuments";
import { BlogPage, PageBuilder, Post } from "studio/lib/interfaces/pages";
import { CustomerCasePage } from "studio/lib/interfaces/specialPages";
import { COMPANY_LOCATIONS_QUERY } from "studio/lib/queries/companyDetails";
import { LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY } from "studio/lib/queries/legalDocuments";
import {
BLOG_PAGE_QUERY,
POSTS_QUERY,
Expand All @@ -31,9 +35,7 @@ import { loadStudioQuery } from "studio/lib/store";
export const dynamic = "force-dynamic";

type Props = {
params: {
slug: string;
};
params: { lang: string; slug: string };
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
Expand All @@ -51,7 +53,8 @@ const Page404 = (
);

async function Page({ params }: Props) {
const { slug } = params;
const { lang, slug } = params;

const { perspective, isDraftMode } = getDraftModeInfo();

const [
Expand All @@ -60,6 +63,7 @@ async function Page({ params }: Props) {
initialCompensationsPage,
initialLocationsData,
initialCustomerCases,
initialLegalDocument,
] = await Promise.all([
loadStudioQuery<PageBuilder>(SLUG_QUERY, { slug }, { perspective }),
loadStudioQuery<BlogPage>(BLOG_PAGE_QUERY, { slug }, { perspective }),
Expand All @@ -78,6 +82,11 @@ async function Page({ params }: Props) {
{ slug },
{ perspective },
),
loadStudioQuery<LegalDocument>(
LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY,
{ slug, language: lang },
{ perspective },
),
]);

if (initialPage.data) {
Expand Down Expand Up @@ -145,6 +154,14 @@ async function Page({ params }: Props) {
);
}

if (initialLegalDocument.data) {
return isDraftMode ? (
<LegalPreview initialDocument={initialLegalDocument} />
) : (
<Legal document={initialLegalDocument.data} />
);
}

return Page404;
}

Expand Down
27 changes: 26 additions & 1 deletion src/app/(main)/page.tsx → src/app/(main)/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Metadata } from "next";

import CustomErrorMessage from "src/blog/components/customErrorMessage/CustomErrorMessage";
import InformationSection from "src/blog/components/informationSection/InformationSection";
import { homeLink } from "src/blog/components/utils/linkTypes";
import { getDraftModeInfo } from "src/utils/draftmode";
import SectionRenderer from "src/utils/renderSection";
import { fetchSeoData, generateMetadataFromSeo } from "src/utils/seo";
import { client } from "studio/lib/client";
import { LinkType } from "studio/lib/interfaces/navigation";
import { PageBuilder } from "studio/lib/interfaces/pages";
import { LanguageObject } from "studio/lib/interfaces/supportedLanguages";
import { LANGUAGES_QUERY } from "studio/lib/queries/languages";
import { LANDING_PAGE_REF_QUERY } from "studio/lib/queries/navigation";
import { PAGE_QUERY, SEO_PAGE_QUERY } from "studio/lib/queries/page";
import { loadStudioQuery } from "studio/lib/store";
Expand Down Expand Up @@ -34,9 +39,29 @@ const pagesLink = {
internalLink: { _ref: "studio/structure/pages" },
};

const Home = async () => {
type Props = {
params: { lang: string; slug: string };
};

const Page404 = (
<CustomErrorMessage
title="404 — Something went wrong"
body="The page you are looking for does not exist. There may be an error in the URL, or the page may have been moved or deleted."
link={homeLink}
/>
);

const Home = async ({ params }: Props) => {
const { perspective, isDraftMode } = getDraftModeInfo();

const language = (
await client.fetch<LanguageObject[] | null>(LANGUAGES_QUERY)
)?.find((l) => l.id === params.lang);

if (language === undefined) {
return Page404;
}

const { data: landingId } = await loadStudioQuery<string>(
LANDING_PAGE_REF_QUERY,
{},
Expand Down
41 changes: 0 additions & 41 deletions src/app/(main)/legal/[id]/page.tsx

This file was deleted.

3 changes: 1 addition & 2 deletions src/components/navigation/footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,13 @@ const Footer = ({
</Text>
</li>
{legalData?.map((legal) => {
const path = `legal/${legal.slug.current}`;
const link = {
_key: legal._id,
_type: legal._type,
linkTitle: legal.basicTitle,
linkType: LinkType.Internal,
internalLink: {
_ref: path,
_ref: legal.slug.current,
},
};
return (
Expand Down
47 changes: 7 additions & 40 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { NextRequest } from "next/server";

import { RedirectDestinationSlugPage } from "studio/lib/interfaces/redirect";
import { REDIRECT_BY_SOURCE_SLUG_QUERY } from "studio/lib/queries/redirects";

import { readBaseUrl } from "./app/env";
import { HTTP_STATUSES } from "./utils/http";
import { languageMiddleware } from "./middlewares/languageMiddleware";
import { redirectMiddleware } from "./middlewares/redirectMiddleware";

export async function middleware(request: NextRequest) {
const baseUrlResult = readBaseUrl();
if (!baseUrlResult.ok) {
console.error(
"Failed to run middleware, missing base url:",
baseUrlResult.error,
);
return;
}
const baseUrl = baseUrlResult.value;
const slug = request.nextUrl.pathname;
const slugQueryParam = slug.replace(/^\/+/, "");
/*
fetching redirect data via API route to avoid token leaking to client
(middleware should run on server, but `experimental_taintUniqueValue` begs to differ...)
*/
const redirectRes = await fetch(new URL("/api/fetchData", baseUrl), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: REDIRECT_BY_SOURCE_SLUG_QUERY,
params: { slug: slugQueryParam },
}),
});
if (redirectRes.ok) {
const redirect: RedirectDestinationSlugPage | null =
await redirectRes.json();
if (redirect?.destination) {
return NextResponse.redirect(
new URL(redirect.destination, request.url),
HTTP_STATUSES.TEMPORARY_REDIRECT,
);
}
const res = await redirectMiddleware(request);
if (res !== undefined) {
return res;
}
return languageMiddleware(request);
}

export const config = {
Expand Down
Loading

0 comments on commit 04b7fbf

Please sign in to comment.