{"id":34648,"date":"2024-07-08T07:00:00","date_gmt":"2024-07-08T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=34648"},"modified":"2024-07-08T12:02:46","modified_gmt":"2024-07-08T19:02:46","slug":"generating-social-images-node-js-hosting","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting","title":{"rendered":"Generating Social Images With Node.js and Hosting With Cloudinary"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>When you share a link on social media, the platform often displays a preview of the content. Creating these images manually can be time-consuming, especially if you have a lot of content to share and want a consistent style for each page. Wouldn\u2019t it be better if we could automate this?<\/p>\n<p>In this tutorial, we\u2019ll show you how to automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Then, once the images are generated, we\u2019ll upload them to Cloudinary with the Cloudinary API.<\/p>\n<p>Using <a href=\"https:\/\/cloudinary.com\/\">Cloudinary<\/a>, we can host our images and serve them quickly to our users. We can also use Cloudinary to optimize our images for the web, ensuring they load quickly and look great on any device. Cloudinary does the hard work of resizing, compressing, and delivering images to users for us!<\/p>\n<p>This tutorial will cover:<\/p>\n<ul>\n<li>Creating images for your web content using Skia Canvas and Node.js.<\/li>\n<li>Saving those images locally for testing purposes.<\/li>\n<li>Uploading these images to Cloudinary for hosting and delivery.<\/li>\n<li>Avoiding unnecessary image uploads at build time if nothing has changed.<\/li>\n<\/ul>\n<h2>Generating Social Cover Images With Canvas<\/h2>\n<p>Before uploading our social images to Cloudinary, we need to generate them! There are several ways to use web technology to generate images. Perhaps the most obvious is to use the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Canvas_API\">Canvas API<\/a>. We can load images, draw shapes, write text, and create a new image file with canvas.<\/p>\n<p>However, canvas has two downsides: First, it\u2019s a browser API, and we\u2019re generating it on the back-end with Node.js. Second, it doesn\u2019t have a native way to wrap text within a bounding box. To use canvas in Node and because it\u2019s time-consuming and challenging to implement text wrapping ourselves, we\u2019ll use the <a href=\"https:\/\/github.com\/samizdatco\/skia-canvas\">Skia Canvas<\/a> library. This library is based on Google\u2019s Skia graphics engine and re-implements and extends the canvas API for Node.js. It includes functionality for text wrapping, saving us the trouble of doing it ourselves.<\/p>\n<p>Let\u2019s start by creating a new Node.js project and installing the <code>skia-canvas<\/code> package.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">mkdir social-image-generator\ncd social-image-generator\nnpm init -y\nnpm install skia-canvas\n<\/code><\/span><\/pre>\n<p>Next we\u2019ll be creating a function that we\u2019ll use to generate our social image from a post name using <code>skia-canvas<\/code>. First, we\u2019ll import <code>Canvas<\/code> and create an instance.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { Canvas } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'skia-canvas'<\/span>;\n\n<span class=\"hljs-comment\">\/\/ We're using skia-canvas to create an image instead of native canvas.<\/span>\n<span class=\"hljs-comment\">\/\/ This is to make it easier to wrap text, which native canvas doesn't<\/span>\n<span class=\"hljs-comment\">\/\/ support out-of-the-box.<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createSocialImage<\/span>(<span class=\"hljs-params\">name: string<\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">Canvas<\/span>&gt; <\/span>{\n\t<span class=\"hljs-comment\">\/\/ Most social media platforms have an image ratio of 1.91:1.<\/span>\n\t<span class=\"hljs-comment\">\/\/ Facebook recommends 1200x630, X\/Twitter 800x418, LinkedIn 1200x627.<\/span>\n\t<span class=\"hljs-comment\">\/\/ Facebook's recommendation will work well for all platforms, so we'll use that.<\/span>\n\t<span class=\"hljs-comment\">\/\/ These dimensions were chosen in May 2024. They may change in the future.<\/span>\n\t<span class=\"hljs-keyword\">const<\/span> canvas = <span class=\"hljs-keyword\">new<\/span> Canvas(<span class=\"hljs-number\">1200<\/span>, <span class=\"hljs-number\">630<\/span>);\n\t<span class=\"hljs-keyword\">const<\/span> { width, height } = canvas;\n\t<span class=\"hljs-keyword\">const<\/span> ctx = canvas.getContext(<span class=\"hljs-string\">'2d'<\/span>);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019ll use the <code>ctx<\/code> to draw within the canvas. First, we\u2019ll load an image for the background:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { Canvas, loadImage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'skia-canvas'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createSocialImage<\/span>(<span class=\"hljs-params\">name: string<\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">Canvas<\/span>&gt; <\/span>{\n\t<span class=\"hljs-comment\">\/\/ ...<\/span>\n\t<span class=\"hljs-keyword\">const<\/span> image = <span class=\"hljs-keyword\">await<\/span> loadImage(<span class=\"hljs-string\">'.\/assets\/background.png'<\/span>);\n\tctx.drawImage(image, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, width, height);\n\t<span class=\"hljs-comment\">\/\/ ...<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Next, we\u2019ll get ready to draw the text. While <code>skia-canvas<\/code> helps with wrapping text, we still have the possibility that text could overflow the bottom of the image. To handle this, we will write some code to scale the text size to fit within the image. If it\u2019s impossible to fit the text without it becoming too small, we\u2019ll throw an error.<\/p>\n<p>We start by setting the initial position of our text, <code>textX<\/code> and <code>textY<\/code>, and the maximum width of the text, <code>maxWidth<\/code>. We then load the font we want to use. In this case, we\u2019re using the <a href=\"https:\/\/fonts.google.com\/specimen\/Inter\">Inter<\/a> font, font-weight 600, which is free from Google Fonts.<\/p>\n<p>We set the font size to 96 and check if the text fit within the image. To do this, we\u2019ll use <code>ctx.measureText()<\/code> to get the lines of text that would be drawn and add together their heights. If the total height of all lines of text is greater than our canvas <code>height<\/code> minus the vertical starting point of <code>textY<\/code>, we reduce the font size by 4 and check again. We continue this process until the text fits or the font size is less than 24. If the text size goes below 24 and it still doesn\u2019t fit, the text will be too small to look good, and we\u2019ll give up by throwing an error.<\/p>\n<p>Why not just set a character limit? Because you\u2019re probably going to use your own fonts and designs! Different fonts will take up different amounts of space at the same font size, so you\u2019ll need to play with it for yourself. A character limit won\u2019t take this into account, but measuring the space used by the drawn text will. When making your designs, you\u2019ll have to try different font sizes and see what works best.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { Canvas, loadImage, FontLibrary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'skia-canvas'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createSocialImage<\/span>(<span class=\"hljs-params\">name: string<\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">Canvas<\/span>&gt; <\/span>{\n\t<span class=\"hljs-comment\">\/\/ ...<\/span>\n\t<span class=\"hljs-keyword\">const<\/span> textX = <span class=\"hljs-number\">50<\/span>;\n\t<span class=\"hljs-keyword\">const<\/span> textY = <span class=\"hljs-number\">100<\/span>;\n\t<span class=\"hljs-keyword\">const<\/span> maxWidth = width - textX * <span class=\"hljs-number\">2<\/span>;\n\n\t<span class=\"hljs-comment\">\/\/ Load the Inter font, weight 600, available on Google Fonts.<\/span>\n\tFontLibrary.use(&#91;<span class=\"hljs-string\">'.\/assets\/Inter-SemiBold.ttf'<\/span>]);\n\tctx.textWrap = <span class=\"hljs-literal\">true<\/span>;\n\t<span class=\"hljs-keyword\">let<\/span> fontSize = <span class=\"hljs-number\">96<\/span>;\n\t<span class=\"hljs-keyword\">let<\/span> textFits = <span class=\"hljs-literal\">false<\/span>;\n\t<span class=\"hljs-keyword\">while<\/span> (!textFits &amp;&amp; fontSize &gt;= <span class=\"hljs-number\">24<\/span>) {\n\t\tctx.font = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${fontSize}<\/span>px Inter`<\/span>;\n\t\t<span class=\"hljs-keyword\">const<\/span> metrics = ctx.measureText(name, maxWidth - textX);\n\t\t<span class=\"hljs-keyword\">let<\/span> totalTextHeight = metrics.lines.reduce(\n\t\t\t<span class=\"hljs-function\">(<span class=\"hljs-params\">acc, line<\/span>) =&gt;<\/span> acc + line.height,\n\t\t\t<span class=\"hljs-number\">0<\/span>,\n\t\t);\n\t\t<span class=\"hljs-keyword\">if<\/span> (totalTextHeight &gt; height - textY) {\n\t\t\tfontSize -= <span class=\"hljs-number\">4<\/span>;\n\t\t} <span class=\"hljs-keyword\">else<\/span> {\n\t\t\ttextFits = <span class=\"hljs-literal\">true<\/span>;\n\t\t}\n\t}\n\n\t<span class=\"hljs-keyword\">if<\/span> (!textFits &amp;&amp; fontSize &lt; <span class=\"hljs-number\">24<\/span>) {\n\t\t<span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">'Text is too long to fit on the image.'<\/span>);\n\t}\n\t<span class=\"hljs-comment\">\/\/ ...<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Finally, now that we have the correct font size, we can draw our post name to the canvas. We\u2019ll add a mostly transparent white stroke to the black text for extra readability. Once we\u2019re done drawing the text, we return the canvas from our function.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createSocialImage<\/span>(<span class=\"hljs-params\">name: string<\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">Canvas<\/span>&gt; <\/span>{\n\t<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n\tctx.fillStyle = <span class=\"hljs-string\">'rgba(16, 15, 15, 0.9)'<\/span>;\n\tctx.lineWidth = <span class=\"hljs-number\">6<\/span>;\n\tctx.strokeStyle = <span class=\"hljs-string\">'rgba(247, 238, 217, 0.2)'<\/span>;\n\tctx.strokeText(name, textX, textY, maxWidth);\n\tctx.fillText(name, textX, textY, maxWidth);\n\n\t<span class=\"hljs-keyword\">return<\/span> canvas;\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h2>Saving Images Locally (Optional)<\/h2>\n<p>You\u2019ll probably want to see how your generated images look before uploading to Cloudinary, so we\u2019ll save them to our local disk first.<\/p>\n<p>For naming our images, we\u2019ll use the <code>slugify<\/code> library and create a helper function with our chosen options. This helper, <code>slugifyName<\/code>, will convert our post name to a URL-friendly string. We\u2019ll then create a function, <code>saveImageToFile<\/code>, that will save our canvas to a file in the <code>images<\/code> directory of our repository. The <code>saveImageToFile<\/code> function will take the post name, the canvas, and the export format as arguments.<\/p>\n<p>First, we\u2019ll install the <code>slugify<\/code> package.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install slugify\n<\/code><\/span><\/pre>\n<p>Next, we\u2019ll write the our functions.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { existsSync, mkdirSync } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'fs'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { join } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'path'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { ExportFormat } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'skia-canvas'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> slugify <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'slugify'<\/span>;\n\n<span class=\"hljs-comment\">\/\/ We're putting this into a helper function so we can easily reuse the options later.<\/span>\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">slugifyName<\/span>(<span class=\"hljs-params\">name: string<\/span>) <\/span>{\n\t<span class=\"hljs-keyword\">return<\/span> slugify(name, { <span class=\"hljs-attr\">lower<\/span>: <span class=\"hljs-literal\">true<\/span>, <span class=\"hljs-attr\">strict<\/span>: <span class=\"hljs-literal\">true<\/span>, <span class=\"hljs-attr\">remove<\/span>: <span class=\"hljs-regexp\">\/&#91;?&amp;#\\\\%&lt;&gt;\\+]]\/g<\/span> });\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">saveImageToFile<\/span>(<span class=\"hljs-params\">\n\tname: string,\n\tcanvas: Canvas,\n\tformat: ExportFormat,\n<\/span>) <\/span>{\n\t<span class=\"hljs-comment\">\/\/ Create the images directory if it doesn't exist.<\/span>\n\t<span class=\"hljs-keyword\">if<\/span> (!existsSync(<span class=\"hljs-string\">'.\/images'<\/span>)) {\n\t\tmkdirSync(<span class=\"hljs-string\">'.\/images'<\/span>);\n\t}\n\t<span class=\"hljs-comment\">\/\/ Then we'll save our canvas image to the format of our choosing.<\/span>\n\t<span class=\"hljs-comment\">\/\/ Pixel density is set to 2 to make the image look good on high-density displays.<\/span>\n\t<span class=\"hljs-keyword\">await<\/span> canvas.saveAs(join(<span class=\"hljs-string\">'.\/images'<\/span>, <span class=\"hljs-string\">`<span class=\"hljs-subst\">${slugifyName(name)}<\/span>.<span class=\"hljs-subst\">${format}<\/span>`<\/span>), {\n\t\tformat,\n\t\t<span class=\"hljs-attr\">density<\/span>: <span class=\"hljs-number\">2<\/span>,\n\t});\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We now have everything we need to generate and save our social images.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createAndSaveLocally<\/span>(<span class=\"hljs-params\">name: string<\/span>) <\/span>{\n\t<span class=\"hljs-keyword\">const<\/span> canvas = <span class=\"hljs-keyword\">await<\/span> createSocialImage(name);\n\t<span class=\"hljs-keyword\">await<\/span> saveImageToFile(name, canvas, <span class=\"hljs-string\">'png'<\/span>);\n}\n\ncreateAndSaveLocally(\n\t<span class=\"hljs-string\">'Whatever example post name you want to save to an image!'<\/span>,\n);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>If we run this script, it will generate our test image at <code>images\/whatever-example-post-name-you-want-to-save-to-an-image.png<\/code>.<\/p>\n<h2>Uploading to Cloudinary<\/h2>\n<p>Saving our images locally is really only necessary to make sure our generation script is outputting social images that look the way we want them to. We ultimately want to host our images with Cloudinary, and we don\u2019t need to save the images to disk to do that. We\u2019re next going to write a function that will upload our images to Cloudinary as buffer data, bypassing the need to save to disk first.<\/p>\n<p>First, we\u2019ll install the <code>cloudinary<\/code> package.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install cloudinary\n<\/code><\/span><\/pre>\n<p>Next, we\u2019ll configure Cloudinary for use. We\u2019ll create a configuration file that references our Cloudinary account information, which we\u2019ll save in environment variables. We\u2019ll also create a <code>.env<\/code> file to store these variables.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">touch<\/span> <span class=\"hljs-selector-class\">.env<\/span>\n<span class=\"hljs-selector-tag\">touch<\/span> <span class=\"hljs-selector-tag\">cloudinary<\/span><span class=\"hljs-selector-class\">.config<\/span><span class=\"hljs-selector-class\">.ts<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>In the <code>.env<\/code> file, we\u2019ll add our Cloudinary account information.<\/p>\n<pre class=\"js-syntax-highlighted\"><code>CLOUDINARY_CLOUD_NAME=your_cloud_name_here\nCLOUDINARY_API_KEY=_your_api_key_here\nCLOUDINARY_API_SECRET=your_secret_here\n<\/code><\/pre>\n<p>In <code>cloudinary.config.ts<\/code>, we\u2019ll import the <code>cloudinary<\/code> package and configure it with our account information.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { v2 <span class=\"hljs-keyword\">as<\/span> cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'cloudinary'<\/span>;\ncloudinary.config({\n\t<span class=\"hljs-attr\">cloud_name<\/span>: process.env.CLOUDINARY_CLOUD_NAME,\n\t<span class=\"hljs-attr\">api_key<\/span>: process.env.CLOUDINARY_API_KEY,\n\t<span class=\"hljs-attr\">api_secret<\/span>: process.env.CLOUDINARY_API_SECRET,\n});\n<span class=\"hljs-keyword\">export<\/span> { cloudinary };\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>For more information on how to set up and use the Cloudinary Node.js SDK, see the <a href=\"https:\/\/cloudinary.com\/documentation\/node_quickstart\">documentation<\/a>.<\/p>\n<p>With configuration done, we can import <code>cloudinary<\/code> into our main script and write a function to upload our images.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/cloudinary.config'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadToCloudinary<\/span>(<span class=\"hljs-params\">\n\tname: string,\n\tcanvas: Canvas,\n\tformat: ExportFormat,\n<\/span>) <\/span>{\n\t<span class=\"hljs-keyword\">try<\/span> {\n\t\t<span class=\"hljs-comment\">\/\/ We'll save our images to a Cloudinary folder called 'og-images'.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> folder = <span class=\"hljs-string\">'og-images'<\/span>;\n\t\t<span class=\"hljs-comment\">\/\/ We'll use the slugified name as the public_id.<\/span>\n\t\t<span class=\"hljs-comment\">\/\/ Note: slugifyName was defined in the previous optional section.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> public_id = slugifyName(name);\n\t\t<span class=\"hljs-comment\">\/\/ No need to save to a local file, we can upload directly to Cloudinary with a Buffer.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> buffer = <span class=\"hljs-keyword\">await<\/span> canvas.toBuffer(format, { <span class=\"hljs-attr\">density<\/span>: <span class=\"hljs-number\">2<\/span> });\n\n\t\t<span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">`Uploading <span class=\"hljs-subst\">${public_id}<\/span>.`<\/span>);\n\t\tcloudinary.uploader\n\t\t\t.upload_stream(\n\t\t\t\t{\n\t\t\t\t\tpublic_id,\n\t\t\t\t\tfolder,\n\t\t\t\t\tformat,\n\t\t\t\t\t<span class=\"hljs-attr\">overwrite<\/span>: <span class=\"hljs-literal\">true<\/span>, <span class=\"hljs-comment\">\/\/ Overwrite if the image already exists.<\/span>\n\t\t\t\t},\n\t\t\t\t(err, _) =&gt; {\n\t\t\t\t\t<span class=\"hljs-keyword\">if<\/span> (err) {\n\t\t\t\t\t\t<span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`Failed to upload image for \"<span class=\"hljs-subst\">${name}<\/span>\"`<\/span>, {\n\t\t\t\t\t\t\t<span class=\"hljs-attr\">cause<\/span>: err,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t)\n\t\t\t.end(buffer);\n\t} <span class=\"hljs-keyword\">catch<\/span> (error) {\n\t\t<span class=\"hljs-built_in\">console<\/span>.error(error);\n\t}\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This function will upload our images to Cloudinary. We\u2019ll use the slugified name as the <code>public_id<\/code> and save the images to a Cloudinary folder named <code>og-images<\/code>. We\u2019ll also overwrite the image if it already exists.<\/p>\n<p>Instead of uploading a file, the function uses <code>canvas.toBuffer()<\/code> to get the image data as a buffer. We then use <code>cloudinary.uploader.upload_stream()<\/code> to upload the image to Cloudinary. We execute everything in a <code>try<\/code> block and catch any errors that occur, logging them to the console for review. Since this function could be used to upload many images, we don\u2019t want a single upload error to crash our script and stop all other uploads.<\/p>\n<p>For more details on the Cloudinary Upload API, see the <a href=\"https:\/\/cloudinary.com\/documentation\/image_upload_api_reference\">API documentation<\/a> and the <a href=\"https:\/\/cloudinary.com\/documentation\/node_image_and_video_upload\">Node.js SDK documentation<\/a>.<\/p>\n<h2>Avoiding Redundant Uploads<\/h2>\n<p>Let\u2019s assume we\u2019ll use this script with a statically generated blog site. Our website might have dozens, hundreds, or thousands of pages. Most of the time, when our site is rebuilt, the content of the pages won\u2019t change. If our social images haven\u2019t changed, then we really don\u2019t need to upload them to Cloudinary at all.<\/p>\n<p>To solve this problem, we\u2019ll modify our upload function to check if an image already exists in Cloudinary. If it does, we\u2019ll skip the upload. If it doesn\u2019t, we\u2019ll upload the image.<\/p>\n<p>We can\u2019t just check for the image name, because we might\u2019ve modified our image generation code to implement a new style. We\u2019ll need to actually check that the images are different. To do this, we\u2019ll create a unique hash from the image data and store it with our upload in the image\u2019s <a href=\"https:\/\/cloudinary.com\/documentation\/image_upload_api_reference#context\">context<\/a>. Then, before we attempt an upload, we\u2019ll check if the image already exists in Cloudinary and if the hash matches.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { createHash } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'crypto'<\/span>;\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getImageHash<\/span>(<span class=\"hljs-params\">buffer: Buffer<\/span>) <\/span>{\n\t<span class=\"hljs-keyword\">return<\/span> createHash(<span class=\"hljs-string\">'md5'<\/span>).update(buffer).digest(<span class=\"hljs-string\">'hex'<\/span>);\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadToCloudinary<\/span>(<span class=\"hljs-params\">\n\tname: string,\n\tcanvas: Canvas,\n\tformat: ExportFormat,\n<\/span>) <\/span>{\n\t<span class=\"hljs-keyword\">try<\/span> {\n\t\t<span class=\"hljs-keyword\">const<\/span> folder = <span class=\"hljs-string\">'og-images'<\/span>;\n\t\t<span class=\"hljs-comment\">\/\/ We'll use the slugified name as the public_id.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> public_id = slugifyName(name);\n\t\t<span class=\"hljs-comment\">\/\/ No need to save to a local file, we can upload directly to Cloudinary with a Buffer.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> buffer = <span class=\"hljs-keyword\">await<\/span> canvas.toBuffer(format, { <span class=\"hljs-attr\">density<\/span>: <span class=\"hljs-number\">2<\/span> });\n\n\t\t<span class=\"hljs-comment\">\/\/ We'll use the hash of the image to prevent duplicates.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> hash = getImageHash(buffer);\n\n\t\t<span class=\"hljs-comment\">\/\/ Then we'll check if an image with the same ID already exists in Cloudinary.<\/span>\n\t\t<span class=\"hljs-keyword\">const<\/span> existingResources = <span class=\"hljs-keyword\">await<\/span> cloudinary.api\n\t\t\t.resource(<span class=\"hljs-string\">`<span class=\"hljs-subst\">${folder}<\/span>\/<span class=\"hljs-subst\">${public_id}<\/span>`<\/span>, {\n\t\t\t\t<span class=\"hljs-attr\">context<\/span>: <span class=\"hljs-literal\">true<\/span>,\n\t\t\t})\n\t\t\t.catch(<span class=\"hljs-function\"><span class=\"hljs-params\">error<\/span> =&gt;<\/span> {\n\t\t\t\t<span class=\"hljs-keyword\">if<\/span> (error.error.http_code !== <span class=\"hljs-number\">404<\/span>) {\n\t\t\t\t\t<span class=\"hljs-keyword\">throw<\/span> error;\n\t\t\t\t}\n\t\t\t});\n\n\t\t<span class=\"hljs-comment\">\/\/ If the image exists and the hashes match, the image hasn't changed!<\/span>\n\t\t<span class=\"hljs-keyword\">if<\/span> (existingResources &amp;&amp; existingResources.context?.custom?.hash === hash) {\n\t\t\t<span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">`No change to <span class=\"hljs-subst\">${public_id}<\/span>. Skipping upload.`<\/span>);\n\t\t\t<span class=\"hljs-keyword\">return<\/span>;\n\t\t} <span class=\"hljs-keyword\">else<\/span> {\n\t\t\t<span class=\"hljs-comment\">\/\/ The upload code will execute here...<\/span>\n\t\t}\n\t} <span class=\"hljs-keyword\">catch<\/span> (error) {\n\t\t<span class=\"hljs-built_in\">console<\/span>.error(error);\n\t}\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Notes on URLs and SEO<\/h3>\n<p>Once your images are uploaded to Cloudinary, you\u2019ll want to use the Cloudinary URLs in your site\u2019s metadata. You may not want to use the ordinary URLs generated during upload. For example, if we had a post titled \u201cExample Post Name\u201d, our image\u2019s URL would be <code>https:\/\/res.cloudinary.com\/{{cloudinary_cloud_name}}\/image\/upload\/og-images\/example-post-name.png<\/code>. This is long and unwieldy, and it\u2019s not great for SEO.<\/p>\n<p>But, you have options! Cloudinary allows for customizing your image URLs for better SEO. Their <a href=\"https:\/\/cloudinary.com\/blog\/how_to_dynamically_create_seo_friendly_urls_for_your_site_s_images\">documentation<\/a> explains how to do this. You can modify URLs to not need <code>image\/upload<\/code>, to include dynamic SEO suffixes, or to use your own domain.<\/p>\n<h2>Conclusion<\/h2>\n<p>Automating the generation and hosting of social cover images makes it much easier to produce timely content without stressing over how it will look on social media. By combining Skia Canvas and <a href=\"https:\/\/cloudinary.com\/\">Cloudinary<\/a>, we were able to create and host social images with minimal effort. Automating social image generation allows you to focus more on creating great content than dealing with the technical details of image hosting and optimization.<\/p>\n<p>If you found this blog post helpful and want to discuss it in more detail, head over to the <a href=\"https:\/\/community.cloudinary.com\/\">Cloudinary Community forum<\/a> and its associated <a href=\"https:\/\/community.cloudinary.com\/\">Discord<\/a>.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":34650,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[332,383,227],"class_list":["post-34648","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-api","tag-nodejs","tag-performance-optimization"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Generating Social Images With Node.js and Hosting With Cloudinary<\/title>\n<meta name=\"description\" content=\"Automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Upload to Cloudinary with the Cloudinary API.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Generating Social Images With Node.js and Hosting With Cloudinary\" \/>\n<meta property=\"og:description\" content=\"Automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Upload to Cloudinary with the Cloudinary API.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2024-07-08T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-07-08T19:02:46+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1720029711\/automating_social_images-blog\/automating_social_images-blog-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Generating Social Images With Node.js and Hosting With Cloudinary\",\"datePublished\":\"2024-07-08T14:00:00+00:00\",\"dateModified\":\"2024-07-08T19:02:46+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\"},\"wordCount\":10,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA\",\"keywords\":[\"API\",\"Node(JS)\",\"Performance Optimization\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2024\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\",\"url\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\",\"name\":\"Generating Social Images With Node.js and Hosting With Cloudinary\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA\",\"datePublished\":\"2024-07-08T14:00:00+00:00\",\"dateModified\":\"2024-07-08T19:02:46+00:00\",\"description\":\"Automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Upload to Cloudinary with the Cloudinary API.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Generating Social Images With Node.js and Hosting With Cloudinary\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Generating Social Images With Node.js and Hosting With Cloudinary","description":"Automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Upload to Cloudinary with the Cloudinary API.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting","og_locale":"en_US","og_type":"article","og_title":"Generating Social Images With Node.js and Hosting With Cloudinary","og_description":"Automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Upload to Cloudinary with the Cloudinary API.","og_url":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting","og_site_name":"Cloudinary Blog","article_published_time":"2024-07-08T14:00:00+00:00","article_modified_time":"2024-07-08T19:02:46+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1720029711\/automating_social_images-blog\/automating_social_images-blog-jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Generating Social Images With Node.js and Hosting With Cloudinary","datePublished":"2024-07-08T14:00:00+00:00","dateModified":"2024-07-08T19:02:46+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting"},"wordCount":10,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA","keywords":["API","Node(JS)","Performance Optimization"],"inLanguage":"en-US","copyrightYear":"2024","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting","url":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting","name":"Generating Social Images With Node.js and Hosting With Cloudinary","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA","datePublished":"2024-07-08T14:00:00+00:00","dateModified":"2024-07-08T19:02:46+00:00","description":"Automatically generate social cover images for your blog posts using Node.js and the Skia Canvas library. Upload to Cloudinary with the Cloudinary API.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/generating-social-images-node-js-hosting#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Generating Social Images With Node.js and Hosting With Cloudinary"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1720029711\/automating_social_images-blog\/automating_social_images-blog.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/34648","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=34648"}],"version-history":[{"count":2,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/34648\/revisions"}],"predecessor-version":[{"id":34651,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/34648\/revisions\/34651"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/34650"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=34648"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=34648"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=34648"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}