From 4f3704c669f0493a32d323ac6835858de4751361 Mon Sep 17 00:00:00 2001 From: Mike Wheeler Date: Thu, 16 Nov 2023 10:48:14 -0800 Subject: [PATCH] Add support for video sitemaps --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++ sitemap.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b5f6e6d3..4e4947d12 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ These parameters mean: * @property {Object} [alternates] alternative versions of this link (useful for multi-language) * @property {Array} [alternates.languages] list of languages that are enabled * @property {Array} [images] list of images links related to the hit +* @property {Array} [videos] list of videos links related to the hit * @property {function} [alternates.hitToURL] function to transform a language into a url of this object */ ``` @@ -138,6 +139,60 @@ function hitToParams({ For more information, see https://support.google.com/webmasters/answer/178636?hl=en +### Video Sitemaps + +If you want your sitemap to include [Google video extensions](https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps), return an array for each hit containing objects with the following keys: + +```js +/** + * @typedef {Object} Video + * @property {string} [title] Video title + * @property {string} [description] Video description + * @property {string} [thumbnail_loc] location of video thumbnail image + * @property {string} [content_loc] location of video file + * @property {string} [player_loc] location of video player + */ +``` + +For example: + +```js +function hitToParams({ + objectID, + modified, + downloadsRatio, + videoFile, + videoThumbnail, + videoDescription + name, +}) { + const url = ({ lang, objectID }) => + `https://${lang}.yoursite.com/${lang}/detail/${objectID}`; + const loc = url({ lang: 'en', objectID }); + const lastmod = new Date().toISOString(); + const priority = Math.random(); + return { + loc, + lastmod, + priority, + videos: [ + { + title: name, + description: videoDescription, + thumbnail_loc: videoThumbnail, + content_loc: videoFile + }, + ], + alternates: { + languages: ['fr', 'pt-BR', 'zh-Hans'], + hitToURL: lang => url({ lang, objectID }), + }, + }; +} +``` + +For more information, see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps + ## Custom queries You can pass a `params` parameter to `algoliaSitemap`. This allows you to narrow down the returned results. For instance, in order to have `hitToParams` called for every products in the `phone` category, we could do: diff --git a/sitemap.js b/sitemap.js index 613ee47c3..3d8a6c0d6 100644 --- a/sitemap.js +++ b/sitemap.js @@ -31,6 +31,7 @@ const isValidURL = ({ priority, alternates, images, + videos, }) => { // loc // eslint-disable-next-line camelcase @@ -118,6 +119,46 @@ see https://support.google.com/webmasters/answer/178636?hl=en for more informati }); } + // videos + const _videosError = `videos ${JSON.stringify(videos)} was not valid. An array with video locations like + +[{ + thumbnail_loc: 'https://example.com/test/my-video.jpg', + title: 'Video title', + description: 'Video description', + content_loc: 'https://example.com/test/my-video.mp4' +}] + +was expected. + +see https://support.google.com/webmasters/answer/178636?hl=en for more information.`; + + if (videos !== undefined) { + if (!(videos instanceof Array)) { + throw new Error(_videosError); + } + videos.forEach(vid => { + if (typeof vid.thumbnail_loc !== 'string') { + throw new Error(_videosError); + } + if (!isURL(vid.thumbnail_loc)) { + throw new Error(_videosError); + } + if (typeof vid.title !== 'string') { + throw new Error(_videosError); + } + if (typeof vid.description !== 'string') { + throw new Error(_videosError); + } + if (typeof vid.content_loc !== 'string' && typeof vid.player_loc !== 'string') { + throw new Error(_videosError); + } + if (!isURL(vid.content_loc) && !isURL(vid.player_loc)) { + throw new Error(_videosError); + } + }); + } + // alternates const _alternatesError = `alternates ${JSON.stringify( alternates @@ -151,6 +192,7 @@ was expected.`; priority: priority === undefined ? undefined : priority.toFixed(1), alternates, images, + videos, }; }; @@ -173,6 +215,17 @@ function createSitemap(entries = []) { {img.license && {img.license}} ); + + const _videos = vid => ( + + {vid.title} + {vid.description} + {vid.thumbnail_loc} + {vid.content_loc && {vid.content_loc}} + {vid.player_loc && {vid.player_loc}} + + ); + const url = args => { const { loc = undefined, @@ -181,6 +234,7 @@ function createSitemap(entries = []) { priority = undefined, alternates = undefined, images = undefined, + videos = undefined, } = isValidURL(args); return ( @@ -193,6 +247,7 @@ function createSitemap(entries = []) { {priority && {priority}} {alternates && _alternates(alternates)} {images && images.length > 0 ? images.map(_images) : null} + {videos && videos.length > 0 ? videos.map(_videos) : null} ); }; @@ -203,6 +258,7 @@ function createSitemap(entries = []) { {entries.map(url)} @@ -222,8 +278,18 @@ function createSitemap(entries = []) { .replace(/<\/imagetitle>/g, ']]>') .replace(//g, '/g, ']]>') + .replace(//g, '/g, ']]>') + .replace(//g, '/g, ']]>') // ➡️ - .replace(/<\/?image/g, '$&:'), + .replace(/<\/?image/g, '$&:') + // ➡️ + .replace(/<\/?video/g, '$&:') + // Video sitemaps use underscores in attribute names + .replace(/<(\/?)video:thumbnailLoc/g, '<$1video:thumbnail_loc') + .replace(/<(\/?)video:contentLoc/g, '<$1video:content_loc') + .replace(/<(\/?)video:playerLoc/g, '<$1video:player_loc') }; }