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 & and < as <.
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
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 &. 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; 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
- Next.js RSS feed in the App Router: what I tested
- Next.js sitemap checker
- Next.js sitemap.ts: revalidate, lastmod and splitting, tested
- Next.js trailingSlash: why your sitemap URLs redirect
- Next.js robots.txt checker
- Next.js development