diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5499ba87d6..2cbe67eb63 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,7 @@ Our versioning strategy is as follows:
* `[templates/nextjs-sxa]` The background image in the Container component was being generated from the image ID instead of the mediaUrl parameter. This fix changes that behavior. ([#1879](https://github.com/Sitecore/jss/pull/1879))
* `[templates/nextjs-sxa]` The caption of image component has been fixed. ([#1874](https://github.com/Sitecore/jss/pull/1874))
* `[sitecore-jss-nextjs]` A bug has been fixed in the redirect middleware that occurred when a user clicked on a link rendered by the Link component from the Next.js library(next/link). ([#1876](https://github.com/Sitecore/jss/pull/1876))
+* `[sitecore-jss-nextjs]` Disable nextjs image optimization in edit and preview modes. This prevents rendering issues in XM Cloud Pages Edit and Preview.
### 🎉 New Features & Improvements
diff --git a/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx b/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
index e39b28c793..21c2a2bfc3 100644
--- a/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
@@ -7,14 +7,23 @@ import { NextImage } from './NextImage';
import {
ImageField,
DefaultEmptyFieldEditingComponentImage,
+ LayoutServicePageState,
+ SitecoreContextReactContext,
} from '@sitecore-jss/sitecore-jss-react';
-import { ImageLoader } from 'next/image';
+import Image, { ImageLoader } from 'next/image';
import { spy, match } from 'sinon';
import sinonChai from 'sinon-chai';
import { SinonSpy } from 'sinon';
use(sinonChai);
+const setContext = spy();
const expect = chai.use(chaiString).expect;
+const testContextProps = {
+ context: {
+ pageState: LayoutServicePageState.Normal,
+ },
+ setContext,
+};
describe('', () => {
const HOSTNAME = 'https://cm.jss.localhost';
@@ -39,7 +48,11 @@ describe('', () => {
height: 10,
};
- const mounted = mount();
+ const mounted = mount(
+
+
+
+ );
const rendered = mounted.find('img');
it('should render image with url', () => {
expect(rendered).to.have.lengthOf(1);
@@ -64,7 +77,11 @@ describe('', () => {
className: 'the-dude-abides',
};
- const rendered = mount().find('img');
+ const rendered = mount(
+
+
+
+ ).find('img');
it('should render image with needed props', () => {
expect(rendered).to.have.length(1);
@@ -88,9 +105,11 @@ describe('', () => {
const field = {
value: { src: '/assets/img/test0.png', alt: 'my image', width: 200, height: 400 },
};
- const rendered = mount().find(
- 'img'
- );
+ const rendered = mount(
+
+
+
+ ).find('img');
expect(rendered).to.have.length(1);
expect(rendered.prop('src')).to.equal(`${HOSTNAME}${props.field.value.src}?w=${props.width}`);
@@ -112,7 +131,11 @@ describe('', () => {
id: 'some-id',
className: 'the-dude-abides',
};
- const rendered = mount().find('img');
+ const rendered = mount(
+
+
+
+ ).find('img');
it('should render image component with "value" properties', () => {
expect(rendered).to.have.length(1);
@@ -141,7 +164,11 @@ describe('', () => {
className: 'the-dude-abides',
};
- const rendered = mount().find('img');
+ const rendered = mount(
+
+
+
+ ).find('img');
expect(rendered).to.have.length(1);
expect(rendered.prop('src')).to.eql(`${HOSTNAME}${props.field.value.src}?w=${props.width}`);
@@ -162,7 +189,11 @@ describe('', () => {
editable: false,
className: 'the-dude-abides w-100',
};
- const rendered = mount().find('img');
+ const rendered = mount(
+
+
+
+ ).find('img');
it('should render image component with "value" properties', () => {
expect(rendered).to.have.length(1);
@@ -191,16 +222,25 @@ describe('', () => {
imageParams: { foo: 'bar' },
mediaUrlPrefix: /\/([-~]{1})assets\//i,
};
- const rendered = mount();
+ const rendered = mount(
+
+
+
+ );
expect(rendered.find('img').prop('src')).to.equal(
`${HOSTNAME}/~/jssmedia/img/test0.png?foo=bar&w=8`
);
- rendered.setProps({
+ const props2 = {
...props,
field: { src: '/-assets/img/test0.png' },
- });
- expect(rendered.find('img').prop('src')).to.equal(
+ };
+ const rendered2 = mount(
+
+
+
+ );
+ expect(rendered2.find('img').prop('src')).to.equal(
`${HOSTNAME}/-/jssmedia/img/test0.png?foo=bar&w=8`
);
expect(mockLoader.called).to.be.true;
@@ -223,17 +263,26 @@ describe('', () => {
imageParams: { foo: 'bar' },
mediaUrlPrefix: /\/([-~]{1})assets\//i,
};
- const rendered = mount();
+ const rendered = mount(
+
+
+
+ );
expect(rendered.find('img').prop('src')).to.equal(
`${HOSTNAME}/~/jssmedia/img/test0.png?foo=bar&w=8`
);
- rendered.setProps({
+ const props2 = {
...props,
field: { src: '/-assets/img/test0.png' },
width,
height: 10,
- });
- expect(rendered.find('img').prop('src')).to.equal(
+ };
+ const rendered2 = mount(
+
+
+
+ );
+ expect(rendered2.find('img').prop('src')).to.equal(
`${HOSTNAME}/-/jssmedia/img/test0.png?foo=bar&w=8`
);
expect(mockLoader.called).to.be.true;
@@ -247,7 +296,11 @@ describe('', () => {
it('should render no image when field prop is empty', () => {
const img = '' as ImageField;
- const rendered = mount().find('img');
+ const rendered = mount(
+
+
+
+ ).find('img');
expect(rendered).to.have.length(0);
});
});
@@ -258,9 +311,13 @@ describe('', () => {
const field = {
src: '/assets/img/test0.png',
};
- expect(() => mount()).to.throw(
- 'Detected src prop. If you wish to use src, use next/image directly.'
- );
+ expect(() =>
+ mount(
+
+
+
+ )
+ ).to.throw('Detected src prop. If you wish to use src, use next/image directly.');
});
});
@@ -276,7 +333,11 @@ describe('', () => {
loader: userMockLoader,
};
- const rendered = mount().find('img');
+ const rendered = mount(
+
+
+
+ ).find('img');
it('should render image with url', () => {
expect(rendered).to.have.lengthOf(1);
@@ -291,6 +352,12 @@ describe('', () => {
});
describe('editMode metadata', () => {
+ const testEditingContext = {
+ ...testContextProps,
+ context: {
+ pageState: LayoutServicePageState.Edit,
+ },
+ };
const testMetadata = {
contextItem: {
id: '{09A07660-6834-476C-B93B-584248D3003B}',
@@ -309,14 +376,18 @@ describe('', () => {
metadata: testMetadata,
};
- const rendered = mount();
-
+ const rendered = mount(
+
+
+
+ );
+ // we expect imgSrc from nextjs optimizations to be absent in editing/metadata mode
expect(rendered.html()).to.equal(
[
`${JSON.stringify(
testMetadata
)}
`,
- '',
+ '',
'
',
].join('')
);
@@ -328,7 +399,11 @@ describe('', () => {
metadata: testMetadata,
};
- const rendered = mount();
+ const rendered = mount(
+
+
+
+ );
const defaultEmptyImagePlaceholder = mount();
expect(rendered.html()).to.equal(
[
@@ -347,7 +422,11 @@ describe('', () => {
metadata: testMetadata,
};
- const rendered = mount();
+ const rendered = mount(
+
+
+
+ );
const defaultEmptyImagePlaceholder = mount();
expect(rendered.html()).to.equal(
[
@@ -371,7 +450,9 @@ describe('', () => {
);
const rendered = mount(
-
+
+
+
);
expect(rendered.html()).to.equal(
@@ -396,7 +477,9 @@ describe('', () => {
);
const rendered = mount(
-
+
+
+
);
expect(rendered.html()).to.equal(
@@ -416,7 +499,11 @@ describe('', () => {
metadata: testMetadata,
};
- const rendered = mount();
+ const rendered = mount(
+
+
+
+ );
expect(rendered.html()).to.equal('');
});
@@ -427,9 +514,66 @@ describe('', () => {
metadata: testMetadata,
};
- const rendered = mount();
+ const rendered = mount(
+
+
+
+ );
expect(rendered.html()).to.equal('');
});
});
+
+ describe('unoptimized property manipulation', () => {
+ const props = {
+ field: { value: { src: '/assets/img/test0.png' } },
+ width,
+ height: 10,
+ id: 'some-id',
+ className: 'the-dude-abides',
+ };
+
+ it('should render unoptimized image in edit mode', () => {
+ const testEditingContext = {
+ ...testContextProps,
+ context: {
+ pageState: LayoutServicePageState.Edit,
+ },
+ };
+ const rendered = mount(
+
+
+
+ ).find(Image);
+ expect(rendered.prop('unoptimized')).to.equal(true);
+ });
+
+ it('should render unoptimized image in preview mode', () => {
+ const testEditingContext = {
+ ...testContextProps,
+ context: {
+ pageState: LayoutServicePageState.Preview,
+ },
+ };
+ const rendered = mount(
+
+
+
+ ).find(Image);
+ expect(rendered.prop('unoptimized')).to.equal(true);
+ });
+
+ it('should render respect original unoptimized value in normal mode', () => {
+ const modifiedProps = {
+ ...props,
+ unoptimized: true,
+ };
+ const rendered = mount(
+
+
+
+ ).find(Image);
+ expect(rendered.prop('unoptimized')).to.equal(true);
+ });
+ });
});
diff --git a/packages/sitecore-jss-nextjs/src/components/NextImage.tsx b/packages/sitecore-jss-nextjs/src/components/NextImage.tsx
index 96b4a31c13..9723645e44 100644
--- a/packages/sitecore-jss-nextjs/src/components/NextImage.tsx
+++ b/packages/sitecore-jss-nextjs/src/components/NextImage.tsx
@@ -7,16 +7,18 @@ import {
ImageField,
ImageFieldValue,
withFieldMetadata,
+ SitecoreContextReactContext,
} from '@sitecore-jss/sitecore-jss-react';
import Image, { ImageProps as NextImageProperties } from 'next/image';
import { withEmptyFieldEditingComponent } from '@sitecore-jss/sitecore-jss-react';
import { DefaultEmptyFieldEditingComponentImage } from '@sitecore-jss/sitecore-jss-react';
-import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout';
+import { isFieldValueEmpty, LayoutServicePageState } from '@sitecore-jss/sitecore-jss/layout';
type NextImageProps = ImageProps & Partial;
export const NextImage: React.FC = withFieldMetadata(
withEmptyFieldEditingComponent(
({ editable = true, imageParams, field, mediaUrlPrefix, fill, priority, ...otherProps }) => {
+ const sitecoreContext = React.useContext(SitecoreContextReactContext);
// next handles src and we use a custom loader,
// throw error if these are present
if (otherProps.src) {
@@ -49,6 +51,11 @@ export const NextImage: React.FC = withFieldMetadata = withFieldMetadata