From e15ea411583318693555091dc646721fb8992d50 Mon Sep 17 00:00:00 2001 From: Archana Agivale Date: Thu, 26 Sep 2024 19:32:56 +0530 Subject: [PATCH] Sitemap video tag support (#69349) Co-authored-by: Sam Ko Co-authored-by: Jiachi Liu --- .../01-metadata/sitemap.mdx | 48 +++++++++++++ .../metadata/resolve-route-data.test.ts | 72 +++++++++++++++++++ .../loaders/metadata/resolve-route-data.ts | 41 +++++++++++ .../lib/metadata/types/metadata-interface.ts | 2 + .../src/lib/metadata/types/metadata-types.ts | 28 ++++++++ .../app/sitemap-video/sitemap.ts | 39 ++++++++++ .../metadata-dynamic-routes/index.test.ts | 32 +++++++++ test/turbopack-build-tests-manifest.json | 2 + 8 files changed, 264 insertions(+) create mode 100644 test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-video/sitemap.ts 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 32342d172f8c22..f7e0cba5539f32 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 @@ -160,6 +160,54 @@ Output: ``` +### Video Sitemaps + +You can use `videos` property to create video sitemaps. Learn more details in the [Google Developer Docs](https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-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, + videos: [ + { + title: 'example', + thumbnail_loc: 'https://example.com/image.jpg', + description: 'this is the description', + }, + ], + }, + ] +} +``` + +Output: + +```xml filename="acme.com/sitemap.xml" + + + + https://example.com + + example + https://example.com/image.jpg + this is the description + + 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 82e686afd6707f..dd163ecd3c8463 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 @@ -153,5 +153,77 @@ describe('resolveRouteData', () => { " `) }) + it('should resolve sitemap.xml with videos', () => { + expect( + resolveSitemap([ + { + url: 'https://example.com', + lastModified: '2021-01-01', + changeFrequency: 'weekly', + priority: 0.5, + videos: [ + { + title: 'example', + thumbnail_loc: 'https://example.com/image.jpg', + description: 'this is the description', + content_loc: 'http://streamserver.example.com/video123.mp4', + player_loc: 'https://www.example.com/videoplayer.php?video=123', + duration: 2, + view_count: 50, + tag: 'summer', + rating: 4, + expiration_date: '2025-09-16', + publication_date: '2024-09-16', + family_friendly: 'yes', + requires_subscription: 'no', + live: 'no', + restriction: { + relationship: 'allow', + content: 'IE GB US CA', + }, + platform: { + relationship: 'allow', + content: 'web', + }, + uploader: { + info: 'https://www.example.com/users/grillymcgrillerson', + content: 'GrillyMcGrillerson', + }, + }, + ], + }, + ]) + ).toMatchInlineSnapshot(` + " + + + https://example.com + + example + https://example.com/image.jpg + this is the description + http://streamserver.example.com/video123.mp4 + https://www.example.com/videoplayer.php?video=123 + 2 + 50 + summer + 4 + 2025-09-16 + 2024-09-16 + yes + no + no + IE GB US CA + web + GrillyMcGrillerson + + 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 4c10b73bb98811..ca8a28720f82c6 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 @@ -48,6 +48,7 @@ export function resolveSitemap(data: MetadataRoute.Sitemap): string { (item) => Object.keys(item.alternates ?? {}).length > 0 ) const hasImages = data.some((item) => Boolean(item.images?.length)) + const hasVideos = data.some((item) => Boolean(item.videos?.length)) let content = '' content += '\n' @@ -55,6 +56,9 @@ export function resolveSitemap(data: MetadataRoute.Sitemap): string { if (hasImages) { content += ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' } + if (hasVideos) { + content += ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"' + } if (hasAlternates) { content += ' xmlns:xhtml="http://www.w3.org/1999/xhtml">\n' } else { @@ -79,6 +83,43 @@ export function resolveSitemap(data: MetadataRoute.Sitemap): string { content += `\n${image}\n\n` } } + if (item.videos?.length) { + for (const video of item.videos) { + let videoFields = [ + ``, + `${video.title}`, + `${video.thumbnail_loc}`, + `${video.description}`, + video.content_loc && + `${video.content_loc}`, + video.player_loc && + `${video.player_loc}`, + video.duration && + `${video.duration}`, + video.view_count && + `${video.view_count}`, + video.tag && `${video.tag}`, + video.rating && `${video.rating}`, + video.expiration_date && + `${video.expiration_date}`, + video.publication_date && + `${video.publication_date}`, + video.family_friendly && + `${video.family_friendly}`, + video.requires_subscription && + `${video.requires_subscription}`, + video.live && `${video.live}`, + video.restriction && + `${video.restriction.content}`, + video.platform && + `${video.platform.content}`, + video.uploader && + `${video.uploader.content}`, + `\n`, + ].filter(Boolean) + content += videoFields.join('\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 eece6b99a43a36..ac919b2096b0b9 100644 --- a/packages/next/src/lib/metadata/types/metadata-interface.ts +++ b/packages/next/src/lib/metadata/types/metadata-interface.ts @@ -30,6 +30,7 @@ import type { TemplateString, Verification, ThemeColorDescriptor, + Videos, } from './metadata-types' import type { Manifest as ManifestFile } from './manifest-types' import type { OpenGraph, ResolvedOpenGraph } from './opengraph-types' @@ -607,6 +608,7 @@ type SitemapFile = Array<{ languages?: Languages } images?: string[] + videos?: Videos[] }> type ResolvingMetadata = Promise diff --git a/packages/next/src/lib/metadata/types/metadata-types.ts b/packages/next/src/lib/metadata/types/metadata-types.ts index e8be1feeeb9d08..ae54c8677e48ee 100644 --- a/packages/next/src/lib/metadata/types/metadata-types.ts +++ b/packages/next/src/lib/metadata/types/metadata-types.ts @@ -156,3 +156,31 @@ export type ThemeColorDescriptor = { color: string media?: string } + +export type Restriction = { + relationship: 'allow' | 'deny' + content: string +} + +export type Videos = { + title: string + thumbnail_loc: string + description: string + content_loc?: string + player_loc?: string + duration?: number + expiration_date?: Date | string + rating?: number + view_count?: number + publication_date?: Date | string + family_friendly?: 'yes' | 'no' + restriction?: Restriction + platform?: Restriction + requires_subscription?: 'yes' | 'no' + uploader?: { + info?: string + content?: string + } + live?: 'yes' | 'no' + tag?: string +} diff --git a/test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-video/sitemap.ts b/test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-video/sitemap.ts new file mode 100644 index 00000000000000..94cf6319b0e54d --- /dev/null +++ b/test/e2e/app-dir/metadata-dynamic-routes/app/sitemap-video/sitemap.ts @@ -0,0 +1,39 @@ +import { MetadataRoute } from 'next' + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + url: 'https://example.com/about', + videos: [ + { + title: 'example', + thumbnail_loc: 'https://example.com/image.jpg', + description: 'this is the description', + content_loc: 'http://streamserver.example.com/video123.mp4', + player_loc: 'https://www.example.com/videoplayer.php?video=123', + duration: 2, + view_count: 50, + tag: 'summer', + rating: 4, + expiration_date: '2025-09-16', + publication_date: '2024-09-16', + family_friendly: 'yes', + requires_subscription: 'no', + live: 'no', + restriction: { + relationship: 'allow', + content: 'IE GB US CA', + }, + platform: { + relationship: 'allow', + content: 'web', + }, + uploader: { + info: 'https://www.example.com/users/grillymcgrillerson', + content: 'GrillyMcGrillerson', + }, + }, + ], + }, + ] +} 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 f1613464ac1237..f8953261d9f20c 100644 --- a/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts +++ b/test/e2e/app-dir/metadata-dynamic-routes/index.test.ts @@ -115,6 +115,38 @@ describe('app dir - metadata dynamic routes', () => { ) }) + it('should support videos in sitemap', async () => { + const xml = await (await next.fetch('/sitemap-video/sitemap.xml')).text() + expect(xml).toMatchInlineSnapshot(` + " + + + https://example.com/about + + example + https://example.com/image.jpg + this is the description + http://streamserver.example.com/video123.mp4 + https://www.example.com/videoplayer.php?video=123 + 2 + 50 + summer + 4 + 2025-09-16 + 2024-09-16 + yes + no + no + IE GB US CA + web + GrillyMcGrillerson + + + + " + `) + }) + 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 0e4ba5ab383e2e..ac91c72ed04e01 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -2100,6 +2100,8 @@ "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 videos in sitemap", "app dir - metadata dynamic routes sitemap should support generate multi sitemaps with generateSitemaps", "app dir - metadata dynamic routes sitemap should support images in sitemap", "app dir - metadata dynamic routes social image routes should fill params into dynamic routes url of metadata images",