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

v3 - translate full path #781

Merged
merged 4 commits into from
Oct 16, 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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Pages](#pages)
- [Development](#development)
- [Using `fetchWithToken` for Custom Components in Sanity](#using-fetchwithtoken-for-custom-components-in-sanity)
- [Nested Path Translation](#nested-path-translation)
- [Steganography in Presentation](#steganography-in-presentation)
- [Serving files from `/public`](#serving-files-from-public)
- [OpenGraph image customization](#opengraph-image-customization)
Expand Down Expand Up @@ -188,6 +189,10 @@ export default MyCustomComponent;

By using fetchWithToken, you ensure that all data fetching happens securely, with the server-side API route handling the sensitive token.

### Nested Path Translation

Some extra handling is required for translating paths with multiple slugs/segments, e.g. `/kunder/fram`. In these cases, the `translatePath` function in [`languageMiddleware.tsx`](src/middlewares/languageMiddleware.ts) must be extended to handle the specific path structure. This usually involves checking which document type corresponds to the first path segment (e.g. `kunder` corresponds to the `customerCase` document type), and then finding the document for the second segment (e.g. customer case with slug `fram`).

### Steganography in Presentation

To enable preview functionality in the Presentation view, Sanity applies [steganography](https://www.sanity.io/docs/stega) to the string data. This manipulates the data to include invisible HTML entities to store various metadata. If the strings are used in business logic, that logic will likely break in the Presentation view. To fix this, Sanity provides the `stegaClean` utility to remove this extra metadata. An example of this in action can be found in [CompensationsPreview.tsx](src/compensations/CompensationsPreview.tsx), where JSON parsing of salary data fails without stega cleaning.
Expand Down
115 changes: 62 additions & 53 deletions src/app/(main)/[lang]/[...path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CustomerCasesPreview from "src/components/customerCases/CustomerCasesPrev
import CustomErrorMessage from "src/components/customErrorMessage/CustomErrorMessage";
import Legal from "src/components/legal/Legal";
import LegalPreview from "src/components/legal/LegalPreview";
import PageHeader from "src/components/navigation/header/PageHeader";
import { homeLink } from "src/components/utils/linkTypes";
import { getDraftModeInfo } from "src/utils/draftmode";
import { fetchPageDataFromParams } from "src/utils/pageData";
Expand Down Expand Up @@ -77,60 +78,68 @@ async function Page({ params }: Props) {
return Page404;
}

const { queryResponse, docType } = pageData;
const { queryResponse, docType, pathTranslations } = pageData;

switch (docType) {
case "pageBuilder":
return (
<>
{queryResponse.data?.sections?.map((section, index) => (
<SectionRenderer
key={section._key}
section={section}
isDraftMode={isDraftMode}
initialData={queryResponse}
isLandingPage={false}
sectionIndex={index}
/>
))}
</>
);
case "compensations":
return isDraftMode ? (
<CompensationsPreview
initialCompensations={queryResponse.compensationsPage}
initialLocations={queryResponse.companyLocations}
initialLocale={queryResponse.locale}
/>
) : (
<Compensations
compensations={queryResponse.compensationsPage.data}
locations={queryResponse.companyLocations.data}
locale={queryResponse.locale.data}
/>
);
case "customerCasesPage":
return isDraftMode ? (
<CustomerCasesPreview initialCustomerCases={queryResponse} />
) : (
<CustomerCases customerCasesPage={queryResponse.data} />
);
case "customerCase":
return (
// TODO: implement customer case detail page
<pre style={{ background: "hotpink", marginTop: "8rem" }}>
{JSON.stringify(pageData, null, 2)}
</pre>
);
case "legalDocument":
return isDraftMode ? (
<LegalPreview initialDocument={queryResponse} />
) : (
<Legal document={queryResponse.data} />
);
}

return Page404;
return (
<>
<PageHeader language={lang} pathTranslations={pathTranslations} />
<main id={"main"} tabIndex={-1}>
{(() => {
switch (docType) {
case "pageBuilder":
return (
<>
{queryResponse.data?.sections?.map((section, index) => (
<SectionRenderer
key={section._key}
section={section}
isDraftMode={isDraftMode}
initialData={queryResponse}
isLandingPage={false}
sectionIndex={index}
/>
))}
</>
);
case "compensations":
return isDraftMode ? (
<CompensationsPreview
initialCompensations={queryResponse.compensationsPage}
initialLocations={queryResponse.companyLocations}
initialLocale={queryResponse.locale}
/>
) : (
<Compensations
compensations={queryResponse.compensationsPage.data}
locations={queryResponse.companyLocations.data}
locale={queryResponse.locale.data}
/>
);
case "customerCasesPage":
return isDraftMode ? (
<CustomerCasesPreview initialCustomerCases={queryResponse} />
) : (
<CustomerCases customerCasesPage={queryResponse.data} />
);
case "customerCase":
return (
// TODO: implement customer case detail page
<pre style={{ background: "hotpink", marginTop: "8rem" }}>
{JSON.stringify(pageData, null, 2)}
</pre>
);
case "legalDocument":
return isDraftMode ? (
<LegalPreview initialDocument={queryResponse} />
) : (
<Legal document={queryResponse.data} />
);
}
return Page404;
})()}
</main>
</>
);
}

export default Page;
37 changes: 1 addition & 36 deletions src/app/(main)/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { draftMode } from "next/headers";

import Footer from "src/components/navigation/footer/Footer";
import FooterPreview from "src/components/navigation/footer/FooterPreview";
import { Header } from "src/components/navigation/header/Header";
import HeaderPreview from "src/components/navigation/header/HeaderPreview";
import SkipToMain from "src/components/skipToMain/SkipToMain";
import { getDraftModeInfo } from "src/utils/draftmode";
import { BrandAssets } from "studio/lib/interfaces/brandAssets";
Expand All @@ -24,8 +22,6 @@ import {
} from "studio/lib/queries/siteSettings";
import { loadStudioQuery } from "studio/lib/store";

import styles from "./layout.module.css";

import "src/styles/global.css";

const fontBrittiSans = localFont({
Expand Down Expand Up @@ -73,45 +69,14 @@ export default async function Layout({
]);

const hasNavData = hasValidData(initialNav.data);
const hasCompanyInfoData = hasValidData(initialCompanyInfo.data);

const hasHeaderData =
hasNavData && (initialNav.data.main || initialNav.data.sidebar);

const hasFooterData = hasNavData && initialNav.data.footer;
const hasMenuData = hasCompanyInfoData && (hasHeaderData || hasFooterData);

if (!hasMenuData) {
return (
<html lang={params.lang}>
<body className={fontBrittiSans.variable}>
<main
id="main"
tabIndex={-1}
className={styles.offsetForStickyHeader}
>
{children}
</main>
</body>
</html>
);
}

return (
<html lang={params.lang}>
<body className={fontBrittiSans.variable}>
<SkipToMain />
{hasHeaderData && isDraftMode ? (
<HeaderPreview
initialNav={initialNav}
initialBrandAssets={initialBrandAssets}
/>
) : (
<Header data={initialNav.data} assets={initialBrandAssets.data} />
)}
<main id="main" tabIndex={-1}>
{children}
</main>
{children}
{hasFooterData && isDraftMode ? (
<FooterPreview
initialNav={initialNav}
Expand Down
45 changes: 34 additions & 11 deletions src/app/(main)/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Metadata } from "next";

import InformationSection from "src/components/informationSection/InformationSection";
import PageHeader from "src/components/navigation/header/PageHeader";
import { getDraftModeInfo } from "src/utils/draftmode";
import { isNonNullQueryResponse } from "src/utils/queryResponse";
import SectionRenderer from "src/utils/renderSection";
import { generateMetadataFromSeo } from "src/utils/seo";
import { InternationalizedString } from "studio/lib/interfaces/global";
import { LinkType } from "studio/lib/interfaces/navigation";
import { PageBuilder } from "studio/lib/interfaces/pages";
import { LANDING_PAGE_QUERY } from "studio/lib/queries/siteSettings";
import { LanguageObject } from "studio/lib/interfaces/supportedLanguages";
import {
LANDING_PAGE_QUERY,
LANGUAGES_QUERY,
} from "studio/lib/queries/siteSettings";
import { loadStudioQuery } from "studio/lib/store";

export async function generateMetadata({ params }: Props): Promise<Metadata> {
Expand Down Expand Up @@ -53,16 +59,33 @@ const Home = async ({ params }: Props) => {
);
}

return initialLandingPage.data.sections.map((section, index) => (
<SectionRenderer
key={section._key}
section={section}
isDraftMode={isDraftMode}
initialData={initialLandingPage}
isLandingPage={true}
sectionIndex={index}
/>
));
const languages = await loadStudioQuery<LanguageObject[] | null>(
LANGUAGES_QUERY,
);

const pathTranslations: InternationalizedString =
languages?.data?.map((language) => ({
_key: language.id,
value: "",
})) ?? [];

return (
<>
<PageHeader language={params.lang} pathTranslations={pathTranslations} />
<main id={"main"} tabIndex={-1}>
{initialLandingPage.data.sections.map((section, index) => (
<SectionRenderer
key={section._key}
section={section}
isDraftMode={isDraftMode}
initialData={initialLandingPage}
isLandingPage={true}
sectionIndex={index}
/>
))}
</main>
</>
);
};

export default Home;
44 changes: 20 additions & 24 deletions src/components/languageSwitcher/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,42 @@ import Link from "next/link";
import { Fragment } from "react";

import Text from "src/components/text/Text";
import useLanguage from "src/utils/hooks/useLanguage";
import { InternationalizedString } from "studio/lib/interfaces/global";

import styles from "./languageSwitcher.module.css";

export default function LanguageSwitcher() {
const { slugTranslations, language, defaultLanguage } = useLanguage();

const currentLanguage = language ?? defaultLanguage;

// make sure the current language is the first item in the languages list
const sortedTranslations = slugTranslations?.toSorted((a, b) =>
a?.language?.id === currentLanguage?.id
? -1
: b?.language?.id === currentLanguage?.id
? 1
: 0,
);
export interface LanguageSwitcherProps {
currentLanguage: string;
pathTranslations: InternationalizedString;
}

export default function LanguageSwitcher({
currentLanguage,
pathTranslations,
}: LanguageSwitcherProps) {
return (
<ul className={styles.list}>
{sortedTranslations?.map((slugTranslation, index) => {
if (slugTranslation?.language === undefined) {
{pathTranslations?.map((pathTranslation, index) => {
if (pathTranslation._key === undefined) {
return null;
}
const linkText = (
<Text type={"small"}>
{slugTranslation.language.id.toUpperCase()}
</Text>
<Text type={"small"}>{pathTranslation._key.toUpperCase()}</Text>
);
return (
<Fragment key={slugTranslation.language.id}>
<Fragment key={pathTranslation._key}>
<li>
{currentLanguage === undefined ||
slugTranslation.language.id !== currentLanguage.id ? (
<Link href={slugTranslation.slug}>{linkText}</Link>
{pathTranslation._key !== currentLanguage ? (
<Link
href={`/${pathTranslation._key}/${pathTranslation.value}`}
>
{linkText}
</Link>
) : (
linkText
)}
</li>
{index < sortedTranslations.length - 1 && (
{index < pathTranslations.length - 1 && (
<span className={styles.divider}></span>
)}
</Fragment>
Expand Down
11 changes: 9 additions & 2 deletions src/components/navigation/header/Header.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Meta, StoryObj } from "@storybook/react";

import { mockLogo, mockNavigation } from "src/components/navigation/mockData";
import { defaultLanguage } from "i18n/supportedLanguages";
import {
mockLogo,
mockNavigation,
mockPathTranslations,
} from "src/components/navigation/mockData";

import { Header } from "./Header";

Expand All @@ -23,7 +28,9 @@ type Story = StoryObj<typeof Header>;

export const Default: Story = {
args: {
data: mockNavigation,
navigation: mockNavigation,
assets: mockLogo,
currentLanguage: defaultLanguage?.id ?? "en",
pathTranslations: mockPathTranslations,
},
};
Loading