diff --git a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx
index 8a10c89579c09..d361a2bec8afb 100644
--- a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx
+++ b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/sitemap.mdx
@@ -120,6 +120,46 @@ Output:
```
+### Image Sitemaps
+
+You can use `images` property to create image sitemaps. Learn more details in the [Google Developer Docs](https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps).
+
+```ts filename="app/sitemap.ts" switcher
+import type { MetadataRoute } from 'next'
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [
+ {
+ url: 'https://example.com',
+ lastModified: '2021-01-01',
+ changeFrequency: 'weekly',
+ priority: 0.5,
+ images: ['https://example.com/image.jpg'],
+ },
+ ]
+}
+```
+
+Output:
+
+```xml filename="acme.com/sitemap.xml"
+
+
+
+ https://example.com
+
+ https://example.com/image.jpg
+
+ 2021-01-01
+ weekly
+ 0.5
+
+
+```
+
### Generate a localized Sitemap
```ts filename="app/sitemap.ts" switcher
diff --git a/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.test.ts b/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.test.ts
index 7c0b22de50c71..82e686afd6707 100644
--- a/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.test.ts
+++ b/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.test.ts
@@ -126,5 +126,32 @@ describe('resolveRouteData', () => {
"
`)
})
+ it('should resolve sitemap.xml with images', () => {
+ expect(
+ resolveSitemap([
+ {
+ url: 'https://example.com',
+ lastModified: '2021-01-01',
+ changeFrequency: 'weekly',
+ priority: 0.5,
+ images: ['https://example.com/image.jpg'],
+ },
+ ])
+ ).toMatchInlineSnapshot(`
+ "
+
+
+ https://example.com
+
+ https://example.com/image.jpg
+
+ 2021-01-01
+ weekly
+ 0.5
+
+
+ "
+ `)
+ })
})
})
diff --git a/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts b/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts
index 368fdbaebbdb7..4c10b73bb9881 100644
--- a/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts
+++ b/packages/next/src/build/webpack/loaders/metadata/resolve-route-data.ts
@@ -47,10 +47,14 @@ export function resolveSitemap(data: MetadataRoute.Sitemap): string {
const hasAlternates = data.some(
(item) => Object.keys(item.alternates ?? {}).length > 0
)
+ const hasImages = data.some((item) => Boolean(item.images?.length))
let content = ''
content += '\n'
content += '\n'
} else {
@@ -70,6 +74,11 @@ export function resolveSitemap(data: MetadataRoute.Sitemap): string {
}" />\n`
}
}
+ if (item.images?.length) {
+ for (const image of item.images) {
+ content += `\n${image}\n\n`
+ }
+ }
if (item.lastModified) {
const serializedDate =
item.lastModified instanceof Date
diff --git a/packages/next/src/lib/metadata/types/metadata-interface.ts b/packages/next/src/lib/metadata/types/metadata-interface.ts
index a8bd0d9fc426c..6b0aa16cdc4ad 100644
--- a/packages/next/src/lib/metadata/types/metadata-interface.ts
+++ b/packages/next/src/lib/metadata/types/metadata-interface.ts
@@ -607,6 +607,7 @@ type SitemapFile = Array<{
alternates?: {
languages?: Languages
}
+ images?: string[]
}>
type ResolvingMetadata = Promise
diff --git a/test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-image/sitemap.ts b/test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-image/sitemap.ts
new file mode 100644
index 0000000000000..5ff6e59228463
--- /dev/null
+++ b/test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-image/sitemap.ts
@@ -0,0 +1,20 @@
+import { MetadataRoute } from 'next'
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [
+ {
+ url: 'https://example.com',
+ lastModified: '2024-01-01',
+ changeFrequency: 'daily',
+ priority: 0.5,
+ },
+ {
+ url: 'https://example.com/about',
+ lastModified: '2024-01-01',
+ images: [
+ 'https://example.com/image1.jpg',
+ 'https://example.com/image2.jpg',
+ ],
+ },
+ ]
+}
diff --git a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
index 2dca3ba618dbc..f1613464ac123 100644
--- a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
+++ b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
@@ -103,6 +103,18 @@ describe('app dir - metadata dynamic routes', () => {
``
)
})
+
+ it('should support images in sitemap', async () => {
+ const xml = await (await next.fetch('/sitemap-image/sitemap.xml')).text()
+
+ expect(xml).toContain(
+ `\nhttps://example.com/image1.jpg\n`
+ )
+ expect(xml).toContain(
+ `\nhttps://example.com/image2.jpg\n`
+ )
+ })
+
if (isNextStart) {
it('should optimize routes without multiple generation API as static routes', async () => {
const appPathsManifest = JSON.parse(
diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json
index 0c6f256dbd155..06d59e33bf405 100644
--- a/test/turbopack-build-tests-manifest.json
+++ b/test/turbopack-build-tests-manifest.json
@@ -2063,6 +2063,7 @@
"app dir - metadata dynamic routes sitemap should not throw if client components are imported but not used in sitemap",
"app dir - metadata dynamic routes sitemap should optimize routes without multiple generation API as static routes",
"app dir - metadata dynamic routes sitemap should support alternate.languages in sitemap",
+ "app dir - metadata dynamic routes sitemap should support images in sitemap",
"app dir - metadata dynamic routes sitemap should support generate multi sitemaps with generateSitemaps",
"app dir - metadata dynamic routes social image routes should fill params into dynamic routes url of metadata images",
"app dir - metadata dynamic routes social image routes should fill params into routes groups url of static images",