Skip to content

Commit

Permalink
Sitemap video tag support (vercel#69349)
Browse files Browse the repository at this point in the history
Co-authored-by: Sam Ko <sam@vercel.com>
Co-authored-by: Jiachi Liu <inbox@huozhi.im>
  • Loading branch information
3 people authored and abhi12299 committed Sep 29, 2024
1 parent 0ec1d27 commit e15ea41
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 0 deletions.
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')
}
}
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: [
{
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

0 comments on commit e15ea41

Please sign in to comment.