We’ve noticed that when using the external video field, the thumbnailUrl returned for a YouTube video is not always the best resolution available for a given video. By default, I would expect thumbnailUrl in the Dato response to be the highest resolution thumbnail YouTube offers for the given video. Specifically, if maxresdefault is available, it should use that, and only fall back to hqdefault when necessary.
We have to employ an annoying workaround, something to the effect of…
/**
* Retrieves the URL of the highest available resolution for a YouTube video thumbnail.
*
* @param videoId - The YouTube video ID for which to get the thumbnail URL.
* @returns A promise that resolves to the URL of the highest resolution available thumbnail.
* - Tries to fetch the maximum resolution thumbnail (`maxresdefault.jpg`) first.
* - If the maximum resolution is not available, fetches the high resolution (`hqdefault.jpg`).
* - Defaults to the standard resolution (`default.jpg`) if both higher resolutions are unavailable.
* @throws Error if an invalid or empty video ID is passed.
*/
export async function getBestAvailableYouTubeThumbnailUrl(videoId: string): Promise<string> {
if (!videoId) {
throw new Error('A valid YouTube video ID is required');
}
const reqInit: RequestInit = {
method: 'HEAD',
cache: 'force-cache',
};
const thumbnails: Record<'maxres' | 'high' | 'default', string> = {
maxres: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
high: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
default: `https://img.youtube.com/vi/${videoId}/default.jpg`,
};
return fetch(thumbnails.maxres, reqInit)
.then((maxResResponse) => {
if (maxResResponse.ok) {
return thumbnails.maxres;
}
return fetch(thumbnails.high, reqInit).then((highResResponse) => {
return highResResponse.ok ? thumbnails.high : thumbnails.default;
});
})
.catch(() => thumbnails.default);
}
I really feel this behavior should be handled from within Dato so we can assume we got the best revolution thumbnailUrl available in the response. Requiring us to manually check YouTube for a better-quality thumbnail is bummer.
It’s a good idea, but has some complexity (we currently just use the default YouTube oembed response, which returns only returns the hqdefault).
Can I convert this into a feature request for you?
Also, I think we can get a similar effect using a plugin that does something similar to Computed Fields (e.g. when you load a video, it’ll make that fetch from the plugin and save it into another JSON field for you, then you can query both in the GraphQL response). Can I make you an example for that in the next day or two? It’d work something like this:
It’s not as clean as a true backend implementation, but would be much faster than waiting for the feature request to get enough upvotes? It would still allow you to fetch the maxres thumbnail URL (only if it exists) at the same time you query for the video URL, moving the burden of that check from frontend build time to record editing time (i.e. by the time you save the record, it will have data about whether the maxres thumbnail exists).
import Image from 'next/image';
import { NextImage } from '../next-image';
import { NextImageWithFallback } from '../next-image-with-fallback';
/**
* DatoCMS often returns a lower resolution thumbnailUrl for YouTube videos.
* To address this, we attempt to serve the highest resolution thumbnail (maxresdefault.jpg) directly.
* If the high-resolution image is unavailable, NextImage's onError callback will automatically fall back to the original thumbnailUrl.
* This approach avoids the need for custom plugins or additional fetch calls to YouTube.
*
* For more details, refer to the discussion:
* https://community.datocms.com/t/youtube-thumbnailurl-in-external-video-field-does-not-return-the-highest-quality-image-available/7893/4
*/
interface DatoYouTubeImageProps extends React.ComponentPropsWithoutRef<typeof Image> {
src: string;
}
export function DatoYouTubeImage({ src, alt, ...props }: DatoYouTubeImageProps) {
const filename = src.split('/').pop() ?? '';
const isMaxres = filename === 'maxresdefault.jpg';
if (isMaxres) {
return <NextImage src={src} alt={alt} {...props} />;
}
return (
<NextImageWithFallback
src={src.replace(filename, 'maxresdefault.jpg')}
fallbackSrc={src}
alt={alt}
{...props}
/>
);
}
Do you know if it causes a bunch of 404s on initial page load, then? On their example site, it looks like my browser has to first try to hit the invalid image before loading the correct one: https://solutions-image-fallback.vercel.app. Probably fine for a few videos, but if you have a whole gallery of them, might impact performance a bit?
I’m also not sure if that override breaks static caching of the thumbnail using NextImage…? If the error handling is only clientside, the server version of it will just be a broken image:
Certainly a lot easier to implement than a Dato-side plugin. But if you’re going to do it on the frontend anyway, maybe an API route / route handler would be cleaner, since you can proxy all the images through that func but still do all the thumbnail checking (and more importantly, request caching) on the server rather than offloading it to the client?
I like the API route / route handler notion! I could pass that right to the src prop of the image and it would guarantee to return the best res thumbnail available. Gonna mess with that now.
/**
* DatoCMS often returns a lower resolution thumbnailUrl for YouTube videos.
* This approach avoids the need for custom plugins or additional fetch calls to YouTube.
*
* For more details, refer to the discussion:
* https://community.datocms.com/t/youtube-thumbnailurl-in-external-video-field-does-not-return-the-highest-quality-image-available/7893/4
*/
interface YouTubeImageProps extends Omit<React.ComponentPropsWithoutRef<typeof Image>, 'src'> {
videoId: string;
}
export function YouTubeImage({ videoId, alt, ...props }: YouTubeImageProps) {
return <NextImage src={`/assets/youtube/${videoId}/thumbnail.jpg`} alt={alt} {...props} />;
}
I take it that async func is a route handler for everything under /assets/youtube/ and getBestAvailableYouTubeThumbnailUrl() just sees if the HQ one is available, and either way, returns either a thumbnail jpeg or a 404?