Generate Simple YouTube Thumbnails

Ifeoma Imoh

With more and more success stories emerging daily, content creation has more than proven itself as a viable means of revenue generation. This is mainly due to YouTube's vast viewership of 300 million daily active users (Q3 2021). A key factor in taking advantage of this audience is a captivating thumbnail that attracts the potential viewer's attention. YouTube has the following recommendations regarding thumbnails.

An ideal thumbnail should:

  1. Have a resolution of 1280x720 (with a minimum width of 640 pixels).
  2. Be uploaded in image formats such as JPG, GIF, or PNG.
  3. Remain under the 2MB limit.
  4. Try to use a 16:9 aspect ratio as it's the most used in YouTube players and previews.

In this article, we will take advantage of Cloudinary to automatically generate simple thumbnails that meet YouTube’s recommendations. By taking advantage of the Transformation URL API, we can specify a URL containing parameters that Cloudinary uses to perform on-the-fly transformations. This provides massive savings as we don’t have to write any code for image manipulation, yet we get a seamless experience that allows us to generate a compliant thumbnail in seconds. You will need a Cloudinary account for this tutorial. If you don't already have one, create one here.

For UI components in our application, we will use antd while axios will be used to upload our videos to our Cloudinary store.

Our application will take the video title and an image for the thumbnail in a form. On form submission, the image will be uploaded to Cloudinary via an unsigned POST request. From the response, the application will retrieve the stored version and public ID with which it will build a URL and download the image returned by the URL.

Here is a link to the demo on CodeSandbox.

Project Setup

Create a new React app using the following command:

1npx create-react-app youtube-thumbnail-generator

Next, add the project dependencies using the following command:

1npm install antd @ant-design/icons axios

Next, we need to import the antd CSS. To do this, open src/App.css and edit its content to match the following:

1@import '~antd/dist/antd.css';

Next, we need to create some environment variables to hold our Cloudinary details. For this tutorial, we will be sending images to Cloudinary via unsigned POST requests. To do this, we need our account cloud name and an unsigned upload preset. At the root of the project, create a new file called .env and add the following to it:

1REACT_APP_CLOUD_NAME = 'ADD YOUR CLOUD NAME HERE'
2 REACT_APP_UPLOAD_PRESET = 'ADD YOUR UNSIGNED UPLOAD PRESET KEY HERE'

This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command:

1cp .env .env.local

By default, this local file is added to .gitignore and mitigates the security risk of inadvertently exposing secret credentials to the public. You can update .env.local with your Cloudinary cloud name and the generated upload preset.

Create a new folder named utility in the project's src directory. This folder will hold all the helper classes we need in our components. In the utility folder, create a file called cloudinaryConfig.js. This file will give access to the environment variables and prevent repeated process.env. calls throughout the project. Add the following to the cloudinaryConfig.js file:

1export const cloudName = process.env.REACT_APP_CLOUD_NAME;
2 export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;

Create Helper Class for External Requests

We need to make two requests for this application - one to upload a new image to Cloudinary and the other to download the image from a URL our application will build. In the utility folder, create a new file named api.js and add the following code to it.

1import axios from 'axios';
2 import { cloudName, uploadPreset } from './cloudinaryConfig';
3 export const uploadImage = ({ file, successCallback }) => {
4 const url = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
5 const data = new FormData();
6 data.append('file', file);
7 data.append('upload_preset', uploadPreset);
8 axios
9 .post(url, data, {
10 headers: {
11 'Content-Type': 'multipart/form-data',
12 },
13 })
14 .then((response) => successCallback(response.data));
15 };
16 export const downloadImage = (imageUrl) => {
17 fetch(imageUrl)
18 .then((response) => response.blob())
19 .then((blob) => {
20 const link = document.createElement('a');
21 link.href = URL.createObjectURL(blob);
22 link.download = 'youtube_thumbnail';
23 link.click();
24 });
25 };

The uploadVideo function takes a file and a callback function that will be executed when the request is handled successfully. The file and upload preset are passed in a formData request to Cloudinary. The provided success callback is used to trigger further action in the component once a successful response is received. In our case, this will be to download the image from a URL. The downloadImage function takes the image URL and downloads the image from the URL using the Fetch API. We exported both functions to be used in other parts of our application.

Creating a URL Builder

The last utility helper we need is to build an image URL with the relevant parameters for our image transformation. In the utility folder, create a new file named urlBuilder.js and add the following code to it:

1import { cloudName } from './cloudinaryConfig';
2 const baseUrl = `https://res.cloudinary.com/${cloudName}/image/upload`;
3 const size = 'c_scale,w_1280,h_720,ar_16:9,e_sharpen';
4 const titleOverlay = (title) =>
5 `b_rgb:9994,co_white,l_text:Helvetica_100:${title}`;
6 const titleLocation = 'fl_layer_apply,g_south';
7 const format = 'jpg';
8 export const buildThumbnailUrl = ({ publicId, version, title }) =>
9 `${baseUrl}/${size}/${titleOverlay(
10 title
11 )}/${titleLocation}/v${version}/${publicId}.${format}`;

The default Cloudinary asset delivery URL has the following structure:

1https://res.cloudinary.com/<cloud_name>/<asset_type>/<delivery_type>/<transformations>/<version>/<public_id_full_path>.<extension>

Since we’re dealing with images, <asset_type> is image and <delivery_type> is upload since we’re working with uploaded images. This is how the baseUrl is generated.

Next, we define the size transformation. Using c_scale, we scale the image to match the new height and width we specify instead of stretching in one direction. Additionally, we set the width and height using the w_ and h parameters accordingly. Finally, we use the e_ parameter to apply a sharpen effect on the image.

Next, we define the text overlay transformation. We want the video's title to be on the image in our generated thumbnail. We start by adding a transparent background using the b_rgb parameter - the first three digits for the rgb specification of the color and the last digit for the alpha (transparency) value. Next, we specify the color of the text using the co_ parameter. Finally, we specify the font using the l_text parameter. For the font, we can either specify the name of any universally available font or a custom font set as the public ID of a raw, authenticated font in your account. You can get more details on custom fonts here.

To ensure that our title doesn’t exceed the width of the image (which will cause Cloudinary to stretch the image and add a white background to the extra space), we also scale the text overlay to have a relative width of 80% to the generated image.

Next, we specify the location of the text in the title location transformation by using the fl_layer_apply and [g_](https://cloudinary.com/documentation/transformation_reference#g_gravity) parameters. Finally, we specify the format. For this tutorial, we use jpg, but as we saw earlier, it could also be png or gif, and Cloudinary would still be able to provide the image in that format.

We put everything together in the buildThumbnailUrl function, which takes the public id and version of the asset along with the video's title and returns a string corresponding to a Cloudinary transformation URL.

Create useFormHelper hook

Before we create our components, let’s create a hook to provide helpful functionality for the form we will create.

In the src directory of the project, create a folder named hooks, and inside it, create a file called useFormHelper.js and add the following the following to it:

1import { uploadImage, downloadImage } from '../utility/api';
2 import { buildThumbnailUrl } from '../utility/urlBuilder';
3 import { message } from 'antd';
4
5 const useFormHelper = ({ image, setIsUploading }) => {
6 const formItemLayout = {
7 labelCol: {
8 sm: { span: 4 },
9 },
10 wrapperCol: {
11 sm: { span: 18 },
12 },
13 };
14 const onFailedSubmission = (errorInfo) => {
15 console.log('Failed:', errorInfo);
16 };
17 const onFinish = (values) => {
18 if (image === null) {
19 message.error('You need to upload an image first');
20 } else {
21 setIsUploading(true);
22 uploadImage({
23 file: image,
24 successCallback: (data) => {
25 setIsUploading(false);
26 const { title } = values;
27 const { public_id: publicId, version } = data;
28 const url = buildThumbnailUrl({ publicId, version, title });
29 downloadImage(url);
30 },
31 });
32 }
33 };
34 return [formItemLayout, onFailedSubmission, onFinish];
35 };
36 export default useFormHelper;

Our hook takes two parameters - image and setIsUploading. When the form is submitted, these parameters are used to update the form UI and pass the uploaded image to Cloudinary.

The formItemLayout is an object which contains the size specifications for the label columns in the form to be created.

The onFailedSubmission function will be triggered if the validation of the submitted form fails. We log the errors to the console and display a warning message to the user.

The onFinish function is called when the form is submitted with valid data. If no image is selected, an error message is shown. If an image is selected, the image is uploaded to Cloudinary using the uploadImage function. If the request was handled successfully, we retrieve the public id and version from the response and pass them to the buildThumbnailUrl function along with the submitted image title. Finally, we use the downloadImage function to download the image from the URL.

Create the ImageSelector Component

In the src folder, create a new folder called components. In the src/components directory, create a new file called ImageSelector.js and add the following to it:

1import { Button, Upload } from 'antd';
2 import { UploadOutlined } from '@ant-design/icons';
3 const ImageSelector = ({ setImage }) => {
4 const props = {
5 name: 'file',
6 onRemove: () => {
7 setImage(null);
8 },
9 beforeUpload: (selectedImage) => {
10 setImage(selectedImage);
11 return false;
12 },
13 showUploadList: false,
14 maxCount: 1,
15 };
16 return (
17 <Upload {...props}>
18 <Button icon={<UploadOutlined />}>Select Image</Button>
19 </Upload>
20 );
21 };
22 export default ImageSelector;

The ImageSelector component takes one prop - setImage, a callback function that will be used to update the application state with the latest uploaded image by the user.

By returning false in the beforeUpload key of props, we disable the Upload component's default behavior, which is to upload the selected file to a provided URL. We also restrict the maximum number of files to 1.

Putting It All Together

Update your src/App.js file to match the following:

1import './App.css';
2 import { Button, Card, Col, Form, Input } from 'antd';
3 import { useState } from 'react';
4 import ImageSelector from './components/ImageSelector';
5 import useFormHelper from './hooks/useFormHelper';
6 function App() {
7 const [image, setImage] = useState(null);
8 const [isUploading, setIsUploading] = useState(false);
9 const [formItemLayout, onFailedSubmission, onFinish] = useFormHelper({
10 image,
11 setIsUploading,
12 });
13 return (
14 <div style={{ margin: '1%' }}>
15 <Card style={{ margin: 'auto', width: '50%' }}>
16 <Form
17 {...formItemLayout}
18 onFinish={onFinish}
19 onFinishFailed={onFailedSubmission}
20 autoComplete="off"
21 >
22 <Form.Item
23 label="Title"
24 name="title"
25 rules={[
26 {
27 required: true,
28 message: 'Please provide a title for the thumbnail',
29 },
30 ]}
31 >
32 <Input />
33 </Form.Item>
34 <Col span={8} offset={9} style={{ marginBottom: '10px' }}>
35 <ImageSelector setImage={setImage} />
36 </Col>
37 <Form.Item wrapperCol={{ offset: 10, span: 16 }}>
38 <Button type="primary" htmlType="submit" loading={isUploading}>
39 Submit
40 </Button>
41 </Form.Item>
42 </Form>
43 </Card>
44 </div>
45 );
46 }
47 export default App;

In this component, we declared two state variables - one for the uploaded image and the other for whether or not the image is being uploaded. Next, we use our useFormHelper hook to get the functionality for the form, passing it the image value in state and the setIsUploading function, which updates the value of the isUploading state variable.

Next, we render the form, and it has a single field for the video's title. The ImageSelector component is rendered to allow the user to select the thumbnail image. Once the form is filled and an image is selected, the image is uploaded to Cloudinary then a popup is shown, prompting the user to save the downloaded image.

Run your application using the following command:

1npm start

By default, the application will be available at http://localhost:3000/. The final result will look like the gif shown below.

Find the complete project here on GitHub.

Conclusion

In this article, to use Cloudinary to generate thumbnails for YouTube videos. Additionally, we looked at how Cloudinary can be used to handle the upload and storage of media and the dynamic transformation and rendering of images in a developer-friendly manner. Finally, using antd, we generated a clean and intuitive user interface with minimal code and styling. Taking advantage of the benefits Cloudinary offers makes for optimized images generated in real-time without hassle.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.