Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sitemap video tag support #69349

Merged
merged 12 commits into from
Sep 26, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,54 @@ Output:
</urlset>
```

### 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"
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
>
<url>
<loc>https://example.com</loc>
<video:video>
<video:title>example</video:title>
<video:thumbnail_loc>https://example.com/image.jpg</video:thumbnail_loc>
<video:description>this is the description</video:description>
</video:video>
<lastmod>2021-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
```

### Generate a localized Sitemap

```ts filename="app/sitemap.ts" switcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
"<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>https://example.com</loc>
<video:video>
<video:title>example</video:title>
<video:thumbnail_loc>https://example.com/image.jpg</video:thumbnail_loc>
<video:description>this is the description</video:description>
<video:content_loc>http://streamserver.example.com/video123.mp4</video:content_loc>
<video:player_loc>https://www.example.com/videoplayer.php?video=123</video:player_loc>
<video:duration>2</video:duration>
<video:view_count>50</video:view_count>
<video:tag>summer</video:tag>
<video:rating>4</video:rating>
<video:expiration_date>2025-09-16</video:expiration_date>
<video:publication_date>2024-09-16</video:publication_date>
<video:family_friendly>yes</video:family_friendly>
<video:requires_subscription>no</video:requires_subscription>
<video:live>no</video:live>
<video:restriction relationship="allow">IE GB US CA</video:restriction>
<video:platform relationship="allow">web</video:platform>
<video:uploader info="https://www.example.com/users/grillymcgrillerson">GrillyMcGrillerson</video:uploader>
</video:video>
<lastmod>2021-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
"
`)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,17 @@ 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 += '<?xml version="1.0" encoding="UTF-8"?>\n'
content += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'
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 {
Expand All @@ -79,6 +83,43 @@ export function resolveSitemap(data: MetadataRoute.Sitemap): string {
content += `<image:image>\n<image:loc>${image}</image:loc>\n</image:image>\n`
}
}
if (item.videos?.length) {
for (const video of item.videos) {
let videoFields = [
`<video:video>`,
`<video:title>${video.title}</video:title>`,
`<video:thumbnail_loc>${video.thumbnail_loc}</video:thumbnail_loc>`,
`<video:description>${video.description}</video:description>`,
video.content_loc &&
`<video:content_loc>${video.content_loc}</video:content_loc>`,
video.player_loc &&
`<video:player_loc>${video.player_loc}</video:player_loc>`,
video.duration &&
`<video:duration>${video.duration}</video:duration>`,
video.view_count &&
`<video:view_count>${video.view_count}</video:view_count>`,
video.tag && `<video:tag>${video.tag}</video:tag>`,
video.rating && `<video:rating>${video.rating}</video:rating>`,
video.expiration_date &&
`<video:expiration_date>${video.expiration_date}</video:expiration_date>`,
video.publication_date &&
`<video:publication_date>${video.publication_date}</video:publication_date>`,
video.family_friendly &&
`<video:family_friendly>${video.family_friendly}</video:family_friendly>`,
video.requires_subscription &&
`<video:requires_subscription>${video.requires_subscription}</video:requires_subscription>`,
video.live && `<video:live>${video.live}</video:live>`,
video.restriction &&
`<video:restriction relationship="${video.restriction.relationship}">${video.restriction.content}</video:restriction>`,
video.platform &&
`<video:platform relationship="${video.platform.relationship}">${video.platform.content}</video:platform>`,
video.uploader &&
`<video:uploader${video.uploader.info && ` info="${video.uploader.info}"`}>${video.uploader.content}</video:uploader>`,
`</video:video>\n`,
].filter(Boolean)
content += videoFields.join('\n')
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}
}
if (item.lastModified) {
const serializedDate =
item.lastModified instanceof Date
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/lib/metadata/types/metadata-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -607,6 +608,7 @@ type SitemapFile = Array<{
languages?: Languages<string>
}
images?: string[]
videos?: Videos[]
}>

type ResolvingMetadata = Promise<ResolvedMetadata>
Expand Down
28 changes: 28 additions & 0 deletions packages/next/src/lib/metadata/types/metadata-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com/about',
videos: [
{
archanaagivale30 marked this conversation as resolved.
Show resolved Hide resolved
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',
},
},
],
},
]
}
32 changes: 32 additions & 0 deletions test/e2e/app-dir/metadata-dynamic-routes/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
"<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>https://example.com/about</loc>
<video:video>
<video:title>example</video:title>
<video:thumbnail_loc>https://example.com/image.jpg</video:thumbnail_loc>
<video:description>this is the description</video:description>
<video:content_loc>http://streamserver.example.com/video123.mp4</video:content_loc>
<video:player_loc>https://www.example.com/videoplayer.php?video=123</video:player_loc>
<video:duration>2</video:duration>
<video:view_count>50</video:view_count>
<video:tag>summer</video:tag>
<video:rating>4</video:rating>
<video:expiration_date>2025-09-16</video:expiration_date>
<video:publication_date>2024-09-16</video:publication_date>
<video:family_friendly>yes</video:family_friendly>
<video:requires_subscription>no</video:requires_subscription>
<video:live>no</video:live>
<video:restriction relationship="allow">IE GB US CA</video:restriction>
<video:platform relationship="allow">web</video:platform>
<video:uploader info="https://www.example.com/users/grillymcgrillerson">GrillyMcGrillerson</video:uploader>
</video:video>
</url>
</urlset>
"
`)
})

if (isNextStart) {
it('should optimize routes without multiple generation API as static routes', async () => {
const appPathsManifest = JSON.parse(
Expand Down
2 changes: 2 additions & 0 deletions test/turbopack-build-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading