Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
91 changes: 88 additions & 3 deletions packages/sitecore-jss-nextjs/src/components/Link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand All @@ -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)),
Expand All @@ -27,8 +28,8 @@ const Router = (): NextRouter => ({
});

// Should provide RouterContext in case if we render Link from next/link
const Page = ({ children }: { children: ReactNode }) => (
<RouterContext.Provider value={Router()}>{children}</RouterContext.Provider>
const Page = ({ children, locales }: { children: ReactNode; locales?: string[] }) => (
<RouterContext.Provider value={Router(locales)}>{children}</RouterContext.Provider>
);

describe('<Link />', () => {
Expand Down Expand Up @@ -616,4 +617,88 @@ describe('<Link />', () => {
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(
<Page locales={['en', 'sv', 'de']}>
<Link field={field} />
</Page>
);

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(
<Page locales={['en', 'sv', 'de']}>
<Link field={field} />
</Page>
);

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(
<Page locales={['en', 'sv', 'de']}>
<Link field={field} />
</Page>
);

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(
<Page>
<Link field={field} />
</Page>
);

const link = rendered.container.querySelector('a');

expect(link?.getAttribute('href')).to.equal('/about');
expect(link?.getAttribute('data-nextjs-link')).to.equal('true');
});
});
});
14 changes: 13 additions & 1 deletion packages/sitecore-jss-nextjs/src/components/Link.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -37,6 +38,9 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
...htmlLinkProps
} = props;

// Get configured locales from Next.js router for locale detection
const router = useRouter();

if (
!field ||
(!(field as LinkFieldValue).editable &&
Expand Down Expand Up @@ -65,11 +69,19 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
// 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 (
<NextLink
href={{ pathname: href, query: querystring, hash: anchor }}
key="link"
locale={false}
locale={hrefHasLocale ? false : undefined}
title={value.title}
target={value.target}
className={value.class}
Expand Down
Loading