diff --git a/CHANGELOG.md b/CHANGELOG.md index 0635486090..e4ffae839f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Our versioning strategy is as follows: ### 🐛 Bug Fixes +* `[sitecore-jss-nextjs]` Fix Link component locale handling to support both `languageEmbedding="always"` and `languageEmbedding="asNeeded"` Sitecore configurations. The component now auto-detects if the href already contains a locale prefix, preventing both double-prefixing and missing locale issues. * `[sitecore-jss-nextjs]` Preserve default locale in external absolute urls ([#2142](https://github.com/Sitecore/jss/pull/2142)) * `[React]` Custom properties are not applied to empty field in editing metadata mode ([#2141](https://github.com/Sitecore/jss/pull/2141)) * `[sitecore-jss-nextjs]` Add regex variable substitution for absolute and external URL redirects. ([#2159](https://github.com/Sitecore/jss/pull/2159)) diff --git a/packages/sitecore-jss-nextjs/src/components/Link.test.tsx b/packages/sitecore-jss-nextjs/src/components/Link.test.tsx index ab390e7ef5..ffd0e4d331 100644 --- a/packages/sitecore-jss-nextjs/src/components/Link.test.tsx +++ b/packages/sitecore-jss-nextjs/src/components/Link.test.tsx @@ -7,7 +7,7 @@ import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtim import { Link } from './Link'; import { spy } from 'sinon'; -const Router = (): NextRouter => ({ +const Router = (locales?: string[]): NextRouter => ({ pathname: '/', route: '/', query: {}, @@ -17,6 +17,7 @@ const Router = (): NextRouter => ({ isFallback: false, isPreview: false, isReady: false, + locales: locales, events: { emit: spy(), off: spy(), on: spy() }, push: spy(() => Promise.resolve(true)), replace: spy(() => Promise.resolve(true)), @@ -27,8 +28,8 @@ const Router = (): NextRouter => ({ }); // Should provide RouterContext in case if we render Link from next/link -const Page = ({ children }: { children: ReactNode }) => ( - {children} +const Page = ({ children, locales }: { children: ReactNode; locales?: string[] }) => ( + {children} ); describe('', () => { @@ -616,4 +617,88 @@ describe('', () => { expect(rendered.container.innerHTML).to.equal(''); }); }); + + describe('locale handling', () => { + it('should not double-prefix locale when href already contains locale', () => { + const field = { + value: { + href: '/sv/about', + text: 'About', + }, + }; + + const rendered = render( + + + + ); + + const link = rendered.container.querySelector('a'); + + // Href should remain unchanged (no double prefix) + expect(link?.getAttribute('href')).to.equal('/sv/about'); + expect(link?.getAttribute('data-nextjs-link')).to.equal('true'); + }); + + it('should allow Next.js to add locale when href does not contain locale', () => { + const field = { + value: { + href: '/about', + text: 'About', + }, + }; + + const rendered = render( + + + + ); + + const link = rendered.container.querySelector('a'); + + // Link should render with NextLink (locale handling enabled) + expect(link?.getAttribute('data-nextjs-link')).to.equal('true'); + }); + + it('should handle locale-only href paths', () => { + const field = { + value: { + href: '/sv', + text: 'Swedish Home', + }, + }; + + const rendered = render( + + + + ); + + const link = rendered.container.querySelector('a'); + + // Href should remain unchanged + expect(link?.getAttribute('href')).to.equal('/sv'); + expect(link?.getAttribute('data-nextjs-link')).to.equal('true'); + }); + + it('should work when no locales are configured', () => { + const field = { + value: { + href: '/about', + text: 'About', + }, + }; + + const rendered = render( + + + + ); + + const link = rendered.container.querySelector('a'); + + expect(link?.getAttribute('href')).to.equal('/about'); + expect(link?.getAttribute('data-nextjs-link')).to.equal('true'); + }); + }); }); diff --git a/packages/sitecore-jss-nextjs/src/components/Link.tsx b/packages/sitecore-jss-nextjs/src/components/Link.tsx index 5383b716a7..3112f416e6 100644 --- a/packages/sitecore-jss-nextjs/src/components/Link.tsx +++ b/packages/sitecore-jss-nextjs/src/components/Link.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, JSX } from 'react'; import NextLink from 'next/link'; import { LinkProps as NextLinkProps } from 'next/link'; +import { useRouter } from 'next/router'; import { Link as ReactLink, LinkFieldValue, @@ -37,6 +38,9 @@ export const Link = forwardRef( ...htmlLinkProps } = props; + // Get configured locales from Next.js router for locale detection + const router = useRouter(); + if ( !field || (!(field as LinkFieldValue).editable && @@ -65,11 +69,19 @@ export const Link = forwardRef( // determine if a link is a route or not. File extensions are not routes and should not be pre-fetched. if (isMatching && !isFileUrl) { delete htmlLinkProps.emptyFieldEditingComponent; + + // Check if href already contains a locale prefix to avoid double-prefixing. + // This supports both languageEmbedding="always" (locale in URL from Sitecore) and + // languageEmbedding="asNeeded" (locale may not be in URL, let Next.js handle it). + const hrefHasLocale = router.locales?.some( + (locale) => href.startsWith(`/${locale}/`) || href === `/${locale}` + ); + return (