afaqrashid.com  ·  portfolio · 2026 ed.

Next.js Image Sitemap: images Field and the & That Breaks It

Next.js sitemap.ts can add images and videos to each URL, and the output looks right at a glance. But Next.js writes every value into the XML exactly as you return it. An & in a URL, an image path or a video title makes the file invalid XML, and a sitemap that doesn't parse can't be read.

I tested this on Next.js 16.4.4 with next build and next start, fetched /sitemap.xml with curl, and parsed it with Python's XML parser.

Images and videos in sitemap.ts

// app/sitemap.ts
import type { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://example.com/work/a",
      images: ["https://example.com/a.jpg"],
    },
    {
      url: "https://example.com/video",
      videos: [
        {
          title: "Product demo",
          thumbnail_loc: "https://example.com/t.jpg",
          description: "A short demo",
        },
      ],
    },
  ];
}

Next.js added the image and video namespaces to <urlset> and wrote one <image:image> per image and a <video:video> block per video. With plain values like these, the file parsed fine.

One & and the file stops parsing

I changed three values: an image file named a & b.jpg, a video titled Demo & intro, and a description containing <short>. Next.js wrote them as they were:

<image:loc>https://example.com/a & b.jpg</image:loc>
<video:title>Demo & intro</video:title>
<video:description>A <short> demo</video:description>

Python's parser rejected the file: not well-formed (invalid token). The sitemap protocol says all data values in a sitemap must be entity-escaped, so & has to be written as &amp; and < as &lt;.

The page URL itself isn't escaped either. A URL with a query string:

{ url: "https://example.com/search?a=1&b=2" }

came out as <loc>https://example.com/search?a=1&b=2</loc>, and that alone made the whole file fail to parse, every other URL included.

Relative image paths pass through

An image given as "/relative.jpg" came out as <image:loc>/relative.jpg</image:loc>. Next.js doesn't apply metadataBase here the way it does for canonical tags. Use full URLs for every image, as Google's image sitemap guide shows.

The fix: escape values before returning them

// app/sitemap.ts
import type { MetadataRoute } from "next";

// Next.js writes these values into the XML as they are, so escape them here.
const text = (s: string) =>
  s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");

const url = (u: string) => text(encodeURI(u));

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    { url: url("https://example.com/search?a=1&b=2") },
    {
      url: url("https://example.com/work/a"),
      images: [url("https://example.com/a & b.jpg")],
      videos: [
        {
          title: text("Demo & intro"),
          thumbnail_loc: url("https://example.com/t.jpg"),
          description: text("A <short> demo"),
        },
      ],
    },
  ];
}

encodeURI turns the spaces into %20, and text() turns & into &amp;. The rebuilt sitemap parsed, and the parser read the values back correctly: the loc was https://example.com/search?a=1&b=2, the image https://example.com/a%20&%20b.jpg and the title Demo & intro.

Use url() for every URL field (url, images, thumbnail_loc, content_loc) and text() for titles and descriptions. Values from a CMS or database are the ones to worry about, since you don't control what an editor types.

Run the check below again after each Next.js upgrade. If a future version starts escaping values itself, this helper would escape them twice, and you'd see &amp;amp; in the file.

Check your own sitemap

curl -s https://your-site.com/sitemap.xml | python3 -c "import sys, xml.dom.minidom as m; m.parseString(sys.stdin.read()); print('valid XML')"

It prints valid XML, or an error with the line and column of the first bad character. Search Console's Sitemaps report also shows a sitemap it couldn't read.

Related

Sources