diff --git a/README.md b/README.md index 1928f0d3a..8df53618e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/src/app/(main)/[lang]/[...path]/page.tsx b/src/app/(main)/[lang]/[...path]/page.tsx index fcf222f5f..628c964e0 100644 --- a/src/app/(main)/[lang]/[...path]/page.tsx +++ b/src/app/(main)/[lang]/[...path]/page.tsx @@ -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"; @@ -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) => ( - - ))} - - ); - case "compensations": - return isDraftMode ? ( - - ) : ( - - ); - case "customerCasesPage": - return isDraftMode ? ( - - ) : ( - - ); - case "customerCase": - return ( - // TODO: implement customer case detail page -
-          {JSON.stringify(pageData, null, 2)}
-        
- ); - case "legalDocument": - return isDraftMode ? ( - - ) : ( - - ); - } - - return Page404; + return ( + <> + +
+ {(() => { + switch (docType) { + case "pageBuilder": + return ( + <> + {queryResponse.data?.sections?.map((section, index) => ( + + ))} + + ); + case "compensations": + return isDraftMode ? ( + + ) : ( + + ); + case "customerCasesPage": + return isDraftMode ? ( + + ) : ( + + ); + case "customerCase": + return ( + // TODO: implement customer case detail page +
+                  {JSON.stringify(pageData, null, 2)}
+                
+ ); + case "legalDocument": + return isDraftMode ? ( + + ) : ( + + ); + } + return Page404; + })()} +
+ + ); } export default Page; diff --git a/src/app/(main)/[lang]/layout.tsx b/src/app/(main)/[lang]/layout.tsx index fa07aa0cc..f0b4ad3d7 100644 --- a/src/app/(main)/[lang]/layout.tsx +++ b/src/app/(main)/[lang]/layout.tsx @@ -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"; @@ -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({ @@ -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 ( - - -
- {children} -
- - - ); - } return ( - {hasHeaderData && isDraftMode ? ( - - ) : ( -
- )} -
- {children} -
+ {children} {hasFooterData && isDraftMode ? ( { @@ -53,16 +59,33 @@ const Home = async ({ params }: Props) => { ); } - return initialLandingPage.data.sections.map((section, index) => ( - - )); + const languages = await loadStudioQuery( + LANGUAGES_QUERY, + ); + + const pathTranslations: InternationalizedString = + languages?.data?.map((language) => ({ + _key: language.id, + value: "", + })) ?? []; + + return ( + <> + +
+ {initialLandingPage.data.sections.map((section, index) => ( + + ))} +
+ + ); }; export default Home; diff --git a/src/components/languageSwitcher/LanguageSwitcher.tsx b/src/components/languageSwitcher/LanguageSwitcher.tsx index dfdff5485..c0ddd0654 100644 --- a/src/components/languageSwitcher/LanguageSwitcher.tsx +++ b/src/components/languageSwitcher/LanguageSwitcher.tsx @@ -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 (
    - {sortedTranslations?.map((slugTranslation, index) => { - if (slugTranslation?.language === undefined) { + {pathTranslations?.map((pathTranslation, index) => { + if (pathTranslation._key === undefined) { return null; } const linkText = ( - - {slugTranslation.language.id.toUpperCase()} - + {pathTranslation._key.toUpperCase()} ); return ( - +
  • - {currentLanguage === undefined || - slugTranslation.language.id !== currentLanguage.id ? ( - {linkText} + {pathTranslation._key !== currentLanguage ? ( + + {linkText} + ) : ( linkText )}
  • - {index < sortedTranslations.length - 1 && ( + {index < pathTranslations.length - 1 && ( )}
    diff --git a/src/components/navigation/header/Header.stories.tsx b/src/components/navigation/header/Header.stories.tsx index b9d2f8307..469f0ba7f 100644 --- a/src/components/navigation/header/Header.stories.tsx +++ b/src/components/navigation/header/Header.stories.tsx @@ -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"; @@ -23,7 +28,9 @@ type Story = StoryObj; export const Default: Story = { args: { - data: mockNavigation, + navigation: mockNavigation, assets: mockLogo, + currentLanguage: defaultLanguage?.id ?? "en", + pathTranslations: mockPathTranslations, }, }; diff --git a/src/components/navigation/header/Header.tsx b/src/components/navigation/header/Header.tsx index f9522ab57..857c1a6e4 100644 --- a/src/components/navigation/header/Header.tsx +++ b/src/components/navigation/header/Header.tsx @@ -5,12 +5,14 @@ import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { FocusOn } from "react-focus-on"; +import { defaultLanguage } from "i18n/supportedLanguages"; import { SanityImage } from "src/components/image/SanityImage"; import LanguageSwitcher from "src/components/languageSwitcher/LanguageSwitcher"; import CustomLink from "src/components/link/CustomLink"; import LinkButton from "src/components/linkButton/LinkButton"; import { getHref } from "src/utils/link"; import { BrandAssets } from "studio/lib/interfaces/brandAssets"; +import { InternationalizedString } from "studio/lib/interfaces/global"; import { ILink, Navigation } from "studio/lib/interfaces/navigation"; import { callToActionFieldID } from "studio/schemas/fields/callToActionFields"; import { linkID } from "studio/schemas/objects/link"; @@ -18,21 +20,28 @@ import { linkID } from "studio/schemas/objects/link"; import styles from "./header.module.css"; export interface IHeader { - data: Navigation; + navigation: Navigation; assets: BrandAssets; + currentLanguage: string; + pathTranslations: InternationalizedString; } const filterLinks = (data: ILink[], type: string) => data?.filter((link) => link._type === type); -export const Header = ({ data, assets }: IHeader) => { +export const Header = ({ + navigation, + assets, + currentLanguage, + pathTranslations, +}: IHeader) => { const pathname = usePathname(); const [isOpen, setIsOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); - const sidebarData = data.sidebar || data.main; + const sidebarData = navigation.sidebar || navigation.main; - const links = filterLinks(data.main, linkID); - const ctas = filterLinks(data.main, callToActionFieldID); + const links = filterLinks(navigation.main, linkID); + const ctas = filterLinks(navigation.main, callToActionFieldID); const sidebarLinks = filterLinks(sidebarData, linkID); const sidebarCtas = filterLinks(sidebarData, callToActionFieldID); @@ -86,7 +95,12 @@ export const Header = ({ data, assets }: IHeader) => { {renderPageLinks(links, false, pathname)} {renderPageCTAs(ctas, false)}
    - + {defaultLanguage && ( + + )}