Our team is consistently hitting a wall with next/image component. When a designer hands off a vector graphic, it gets flattened into a static img tag, completely stripping it of its inherent scalability and styling benefits. This isn't just a minor issue; it's a performance bottleneck, a hit to our dynamic styling capabilities, and a constant source of technical debt.
This post will walk through a robust, production-ready solution: a custom Next.js Image Wrapper that not only gracefully handles all media from Sitecore but also intelligently renders SVGs as true inline svg markup.
Executive Summary
- The Problem: The default
next/image
component treats SVGs as raster images, limiting styling and DOM manipulation. We also need a bulletproof way to handle missing width
, height
, and alt
attributes from Sitecore.
- Inline SVG vs.
next/image
: By rendering SVGs as inline <svg>
markup, we gain full CSS control (e.g., currentColor
for theming), enable accessibility hooks, and simplify DOM manipulation. For all other image types, we leverage the power of next/image
for automatic optimization.
- Bulletproof Fallbacks: Our wrapper component provides sensible defaults for
width
and height
if they are missing from the Sitecore Media Library, preventing Cumulative Layout Shift (CLS) and ensuring consistent layouts.
- Key Outcomes: This approach leads to superior performance (no extra network requests for styling), enhanced accessibility, and a more maintainable, consistent frontend architecture. It’s about building a predictable and resilient component system.
The Problem
Sitecore’s Media Library is a fantastic tool for managing digital assets. It provides URLs for images, but when it comes to SVGs, a URL is often not enough. A standard <img>
tag or next/image
component treats the SVG file just like a PNG or JPG. We lose:
- Styling with CSS: Can’t use
fill
, stroke
, or color
to change the icon's appearance based on state or theme. This is critical for design systems and Dark Mode.
- DOM Interaction: No way to programmatically manipulate the SVG's internal paths or elements with JavaScript.
- Accessibility: The
alt
text is on the wrapper <img>
tag, but you can’t add rich accessibility attributes like aria-labelledby
or role="img"
directly to the <svg>
itself.
- Performance: The browser makes an extra network request to download the SVG, even if it's a tiny icon, instead of having the markup directly in the DOM.
On top of this, CMS authors often forget or don't know to fill in the width
, height
, or alt
fields for an image, leading to layout shifts and accessibility violations. We need a way to enforce consistency without manual intervention.
The Solution: Architecture Overview
Our solution is an intelligent wrapper component that sits in front of the standard next/image
. Its primary job is to inspect the src
URL and make a critical decision: should this be a flexible, inline <svg>
or a performant, optimized <Image>
?
Here’s the decision flow:
props (field, width, height)
└─> isSVG? (check file extension)
| └─> is in Experience Editor or Preview mode? -> <NextImage />
| └─> else -> fetch SVG content on the server -> render <SVGInline />
|
└─> else (e.g., .jpg, .png)
└─> has all dimensions? -> <NextImage />
└─> else (missing dimensions)
└─> is in Experience Editor? -> render red warning + <NextImage />
└─> else -> render empty tag to prevent layout shift
This architecture ensures we get the best of both worlds: the power of inline SVGs where we need it and the performance benefits of next/image
for everything else. The fallback logic for missing dimensions is a critical safety net that prevents layout shifts (CLS) and provides clear feedback to content authors in the editor.
Code Walkthrough
Let's dissect the code files to see how this all comes together.
NextImageExtended.tsx
This is our main wrapper component. It's the "brain" that makes all the decisions.
File Name & Role: NextImageExtended.tsx
. This component serves as a drop-in replacement for the default Sitecore JSS NextImage
component. Its primary responsibility is to orchestrate the rendering logic based on the image's properties.
Key Exports:
NextImageExtended
: The core component that handles the rendering logic.
NextImageProps
: A TypeScript interface to define the expected props, ensuring type safety.
Code:
import useIsEditing from '@/hooks/useIsEditing';
import {
ImageField,
LayoutServicePageState,
NextImage,
useSitecoreContext,
} from '@sitecore-jss/sitecore-jss-nextjs';
import SVGInline from './SVGInline';
type NextImageProps {
field?: ImageField;
width?: number;
height?: number;
}
const NextImageExtended = (props: NextImageProps): JSX.Element => {
const { sitecoreContext } = useSitecoreContext();
const isEditing = useIsEditing();
const isPreview = sitecoreContext.pageState == LayoutServicePageState.Preview;
if (!!props.width && props.field?.value) {
props.field.value.width = props.width;
}
if (!!props.height && props.field?.value) {
props.field.value.height = props.height;
}
const hasEmptyDimension =
props?.field?.value?.src !== undefined &&
(props.field?.value?.width === undefined ||
props.field?.value?.width === '' ||
props.field?.value?.height === undefined ||
props.field?.value?.height == '');
if (props.field?.value?.src?.includes('.svg') && !(isEditing || isPreview)) {
let src = props.field?.value?.src;
src = '/api/fetchSvg?url=${encodeURIComponent(src)}';
return <SVGInline src={src} />;
} else if (props.field?.value?.src?.includes('.svg') && hasEmptyDimension) {
props.field.value.width = 50;
props.field.value.height = 50;
return <NextImage {...props} />;
} else if (hasEmptyDimension) {
if (isEditing) {
return (
<>
<span style={{ color: 'red' }}>Fill in image dimensions</span>
<NextImage {...props} />
</>
);
} else {
return <></>;
}
} else {
return <NextImage {...props} />;
}
};
export default NextImageExtended;
Why this matters:
-
Experience Editor Exemption: We intentionally don't render inline SVGs in the Experience Editor. This is a crucial design choice. The editor often requires a specific <img>
tag structure to enable its rich-text and image editing capabilities. The useIsEditing()
and useSitecoreContext()
hooks are the perfect way to make this distinction, ensuring the authoring experience is not broken.
-
Server-Side Fetching: The src
is rewritten to /api/fetchSvg
. This is a powerful move. By fetching the SVG content on the server side (via a Next.js API route), we can get the raw markup before the component is rendered. This is essential for server-side rendering (SSR) and Incremental Static Regeneration (ISR). The <SVGInline>
component then renders this already-fetched and sanitized markup. This avoids a client-side fetch, improving performance.
-
Handling Missing Dimensions: The hasEmptyDimension
check is our fallback logic.
-
Preventing CLS: Setting default dimensions prevents a massive layout shift. next/image
requires these values to calculate the aspect ratio, which is key to preventing CLS.
-
Content Author Feedback: In editing mode, we show a red warning. This isn't just a band-aid; it's a user experience improvement for the content author, teaching them that dimensions are important.
-
Graceful Degradation: In production, if dimensions are still missing, we just render an empty <></>
fragment. This is an opinionated choice: better to render nothing than to cause a massive layout shift or a broken layout.
fetchSvg.ts
This is our Next.js API route. It's a simple, single-purpose utility.
File Name & Role: fetchSvg.ts
. This file defines a serverless function that lives in the /pages/api
directory (or /app/api
in Next.js 13+). Its sole purpose is to securely fetch the raw content of a given SVG URL.
Key Exports:
handler
: The default export that serves as the API route.
Critical Code Paths:
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { url } = req.query;
if (!url) {
res.status(400).json({ error: 'Missing URL parameter' });
return;
}
try {
const response = await fetch(url as string);
if (!response.ok) {
res.status(response.status).json({ error: 'Failed to fetch SVG' });
return;
}
const data = await response.text();
res.setHeader('Content-Type', 'image/svg+xml');
res.status(200).send(data);
} catch (error) {
res.status(500).json({ error: 'Internal Server Error' });
}
}
Why this matters:
- Security First: By using an API route as a proxy, we can sanitize the SVG content before it reaches the client. While this code doesn't show it, a real-world implementation should use a library like
dompurify
on the data
to strip out any malicious <script>
tags or on*
attributes. Never render raw SVG markup from an untrusted source.
- Caching and Optimization: This API route can be cached by a CDN or Next.js, meaning the SVG content is only fetched from Sitecore once. This is a huge performance win for repeat visits.
- Cross-Origin Headers: This route handles any potential cross-origin (CORS) issues between your Next.js application and the Sitecore Media Library, as it acts as a server-side intermediary.
Migration Checklist
Ready to adopt this into your project? Here's a quick checklist:
- Create the files: Add
NextImageExtended.tsx
, SVGInline.tsx
(a simple component that takes src
as props and renders the inner HTML), and the fetchSvg.ts
API route.
- Add Types & Helpers: Ensure you have the necessary types and the
useIsEditing
hook.
- Update your components: Find and replace all instances of
<NextImage ... />
with <NextImageExtended ... />
.
- Sanitize: Integrate a sanitation library like
dompurify
into the fetchSvg.ts
route for security.
- Test: Verify that images and SVGs render correctly in both production and Experience Editor modes. Ensure missing dimensions are handled gracefully.
By following this approach, you can deliver a more performant, accessible, and maintainable frontend experience for your Sitecore solution. It’s about being deliberate with our design choices and creating a component system that is both intelligent and resilient.