Next.js Image Wrapper for Sitecore SVGs

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:

  1. 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.
  2. Add Types & Helpers: Ensure you have the necessary types and the useIsEditing hook.
  3. Update your components: Find and replace all instances of <NextImage ... /> with <NextImageExtended ... />.
  4. Sanitize: Integrate a sanitation library like dompurify into the fetchSvg.ts route for security.
  5. 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.

Related Posts

From Excel to Sitecore - My Streamlined Migration Workflow via PowerShell

Content migration projects are notoriously painful Excel files flying around email threads, editors losing track of versions, and developers drowning in manual imports. I recently implemented a workf

Read More

Exposing Site Settings from the Settings item in a Headless JSS Next.js Application

In this blog post, we will explore a common requirement in Sitecore Headless SXA, exposing site-specific settings and global data to the front end when working with a JSS headless site. ## Problem S

Read More

Handling Sitecore CM and IdentityServer Behind a Proxy or CDN in an Azure Environment

Recently, while working with a Sitecore 10.4 deployment in an Azure environment, we encountered an interesting challenge: handling Sitecore Content Management (CM) and IdentityServer (SI) behind a pr

Read More