├── .prettierrc ├── .gitignore ├── package.json ├── LICENSE ├── src └── index.ts └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | index.js 3 | index.d.ts 4 | index.mjs 5 | index.cjs 6 | index.cjs.* 7 | index.umd.* 8 | index.esm.* 9 | index.js.map 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jlengstorf/get-share-image", 3 | "type": "module", 4 | "version": "1.0.1", 5 | "source": "src/index.ts", 6 | "exports": { 7 | "import": "./dist/index.js", 8 | "require": "./dist/index.cjs", 9 | "types": "./index.d.ts" 10 | }, 11 | "main": "./dist/index.cjs", 12 | "module": "./dist/index.esm.js", 13 | "unpkg": "./dist/index.umd.js", 14 | "author": "Jason Lengstorf (https://lengstorf.com)", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/jlengstorf/get-share-image.git" 19 | }, 20 | "scripts": { 21 | "build": "microbundle", 22 | "dev": "microbundle watch", 23 | "prepublish": "npm run build" 24 | }, 25 | "files": [ 26 | "./dist" 27 | ], 28 | "types": "dist/index.d.ts", 29 | "devDependencies": { 30 | "microbundle": "^0.15.1", 31 | "typescript": "^5.3.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Jason Lengstorf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | type Gravity = 2 | | 'north_east' 3 | | 'north' 4 | | 'north_west' 5 | | 'west' 6 | | 'south_west' 7 | | 'south' 8 | | 'south_east' 9 | | 'east' 10 | | 'center'; 11 | 12 | type GetShareImageConfig = { 13 | title: string; 14 | tagline?: string; 15 | cloudName: string; 16 | imagePublicID: string; 17 | 18 | /** 19 | * Only needed if you have a custom Cloudinary URL 20 | * @default 'https://res.cloudinary.com' 21 | */ 22 | cloudinaryUrlBase?: string; 23 | 24 | /** 25 | * @default 'arial' 26 | */ 27 | titleFont?: string; 28 | titleExtraConfig?: string; 29 | taglineExtraConfig?: string; 30 | 31 | /** 32 | * @default 'arial' 33 | */ 34 | taglineFont?: string; 35 | 36 | /** 37 | * @default 1280 38 | */ 39 | imageWidth?: number; 40 | 41 | /** 42 | * @default 669 43 | */ 44 | imageHeight?: number; 45 | 46 | /** 47 | * @default 760 48 | */ 49 | textAreaWidth?: number; 50 | 51 | /** 52 | * @default 480 53 | */ 54 | textLeftOffset?: number; 55 | 56 | /** 57 | * @default 'south_west' 58 | */ 59 | titleGravity?: 60 | | 'north_east' 61 | | 'north' 62 | | 'north_west' 63 | | 'west' 64 | | 'south_west' 65 | | 'south' 66 | | 'south_east' 67 | | 'east' 68 | | 'center'; 69 | 70 | /** 71 | * @default 'north_west' 72 | */ 73 | taglineGravity?: 74 | | 'north_east' 75 | | 'north' 76 | | 'north_west' 77 | | 'west' 78 | | 'south_west' 79 | | 'south' 80 | | 'south_east' 81 | | 'east' 82 | | 'center'; 83 | titleLeftOffset?: number | null; 84 | taglineLeftOffset?: number | null; 85 | 86 | /** 87 | * @default 254 88 | */ 89 | titleBottomOffset?: number; 90 | 91 | /** 92 | * @default 445 93 | */ 94 | taglineTopOffset?: number; 95 | 96 | /** 97 | * @default '000000' 98 | */ 99 | textColor?: string; 100 | titleColor: string; 101 | taglineColor: string; 102 | 103 | /** 104 | * @default 64 105 | */ 106 | titleFontSize?: number; 107 | 108 | /** 109 | * @default 48 110 | */ 111 | taglineFontSize?: number; 112 | 113 | version?: string | null; 114 | }; 115 | 116 | /** 117 | * Encodes characters for Cloudinary URL 118 | * Encodes some not allowed in Cloudinary parameter values twice: 119 | * hash (#), comma (,), slash (/), question mark (?), backslash (\) 120 | * 121 | * @see https://support.cloudinary.com/hc/en-us/articles/202521512-How-to-add-a-slash-character-or-any-other-special-characters-in-text-overlays- 122 | */ 123 | function cleanText(text: string): string { 124 | return encodeURIComponent(text).replace(/%(23|2C|2F|3F|5C)/g, '%25$1'); 125 | } 126 | 127 | /** 128 | * Generates a social sharing image with custom text using Cloudinary’s APIs. 129 | * 130 | * @see https://cloudinary.com/documentation/image_transformations#adding_text_captions 131 | */ 132 | export function generateSocialImage({ 133 | title, 134 | tagline, 135 | cloudName, 136 | imagePublicID, 137 | cloudinaryUrlBase = 'https://res.cloudinary.com', 138 | titleFont = 'arial', 139 | titleExtraConfig = '', 140 | taglineExtraConfig = '', 141 | taglineFont = 'arial', 142 | imageWidth = 1280, 143 | imageHeight = 669, 144 | textAreaWidth = 760, 145 | textLeftOffset = 480, 146 | titleGravity = 'south_west', 147 | taglineGravity = 'north_west', 148 | titleLeftOffset = null, 149 | taglineLeftOffset = null, 150 | titleBottomOffset = 254, 151 | taglineTopOffset = 445, 152 | textColor = '000000', 153 | titleColor, 154 | taglineColor, 155 | titleFontSize = 64, 156 | taglineFontSize = 48, 157 | version = null, 158 | }: GetShareImageConfig): string { 159 | // configure social media image dimensions, quality, and format 160 | const imageConfig = [ 161 | `w_${imageWidth}`, 162 | `h_${imageHeight}`, 163 | 'c_fill', 164 | 'q_auto', 165 | 'f_auto', 166 | ].join(','); 167 | 168 | // configure the title text 169 | const titleConfig = [ 170 | `w_${textAreaWidth}`, 171 | 'c_fit', 172 | `co_rgb:${titleColor || textColor}`, 173 | `g_${titleGravity}`, 174 | `x_${titleLeftOffset || textLeftOffset}`, 175 | `y_${titleBottomOffset}`, 176 | `l_text:${titleFont}_${titleFontSize}${titleExtraConfig}:${cleanText( 177 | title, 178 | )}`, 179 | ].join(','); 180 | 181 | // configure the tagline text 182 | const taglineConfig = tagline 183 | ? [ 184 | `w_${textAreaWidth}`, 185 | 'c_fit', 186 | `co_rgb:${taglineColor || textColor}`, 187 | `g_${taglineGravity}`, 188 | `x_${taglineLeftOffset || textLeftOffset}`, 189 | `y_${taglineTopOffset}`, 190 | `l_text:${taglineFont}_${taglineFontSize}${taglineExtraConfig}:${cleanText( 191 | tagline, 192 | )}`, 193 | ].join(',') 194 | : undefined; 195 | 196 | // combine all the pieces required to generate a Cloudinary URL 197 | const urlParts = [ 198 | cloudinaryUrlBase, 199 | cloudName, 200 | 'image', 201 | 'upload', 202 | imageConfig, 203 | titleConfig, 204 | taglineConfig, 205 | version, 206 | imagePublicID, 207 | ]; 208 | 209 | // remove any falsy sections of the URL (e.g. an undefined version) 210 | const validParts = urlParts.filter(Boolean); 211 | 212 | // join all the parts into a valid URL to the generated image 213 | return validParts.join('/'); 214 | } 215 | 216 | /** 217 | * @deprecated It's recommended to use named imports instead (`import { generateSocialImage } from '@jlengstorf/get-share-image'`). The default export will be removed in a future major version. 218 | */ 219 | export default generateSocialImage; 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate Social Media Images Using Cloudinary 2 | 3 | This is a utility function that builds social media images by overlaying a title and tagline over an image using [Cloudinary’s APIs](https://cloudinary.com/documentation/image_transformations?ap=lwj#adding_text_captions). 4 | 5 | > **NOTE:** a Cloudinary account is required to use this package. The free tier should be more than enough for most small to medium websites using this package. [Sign up for an account here!](https://jason.energy/cloudinary) 6 | 7 | **This was created as part of an article series:** 8 | 9 | - [How to design a social sharing card template](https://www.learnwithjason.dev/blog/design-social-sharing-card) 10 | - [How the code in this package works](https://www.learnwithjason.dev/blog/auto-generate-social-image) 11 | 12 | ## Installation 13 | 14 | ```bash 15 | # install using npm 16 | npm install --save @jlengstorf/get-share-image 17 | 18 | # install using yarn 19 | yarn add @jlengstorf/get-share-image 20 | ``` 21 | 22 | See how this is used in a production site in the [learnwithjason.dev source code](https://github.com/jlengstorf/learnwithjason.dev/blob/070468828e8c758d150a8d573fd471d786278243/packages/%40jlengstorf/gatsby-theme-code-blog/src/gatsby-theme-blog-core/components/post.js#L55-L64). 23 | 24 | ## Example Usage 25 | 26 | ```js 27 | import getShareImage from '@jlengstorf/get-share-image'; 28 | 29 | const socialImage = getShareImage({ 30 | title: 'Deploy a Node.js App to DigitalOcean with SSL', 31 | tagline: '#devops #nodejs #ssl', 32 | cloudName: 'jlengstorf', 33 | imagePublicID: 'lwj/blog-post-card', 34 | titleFont: 'futura', 35 | taglineFont: 'futura', 36 | textColor: '232129', 37 | }); 38 | ``` 39 | 40 | This generates an image URL: 41 | 42 | ```text 43 | https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:232129,g_south_west,x_480,y_254,l_text:futura_64:Deploy%20a%20Node.js%20App%20to%20DigitalOcean%20with%20SSL/w_760,c_fit,co_rgb:232129,g_north_west,x_480,y_445,l_text:futura_48:%23devops%20%23nodejs%20%23ssl/lwj/blog-post-card 44 | ``` 45 | 46 | Which looks like this: 47 | 48 | ![Deploy a Node.js App to DigitalOcean with SSL, from learnwithjason.dev](https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:232129,g_south_west,x_480,y_254,l_text:futura_64:Deploy%20a%20Node.js%20App%20to%20DigitalOcean%20with%20SSL/w_760,c_fit,co_rgb:232129,g_north_west,x_480,y_445,l_text:futura_48:%23devops%20%23nodejs%20%23ssl/lwj/blog-post-card) 49 | 50 | ## Options 51 | 52 | This utility function accepts a config object. Available options are as follows: 53 | 54 | | name | required | description | 55 | | ------------------ | -------- | -------------------------------------------------------------------- | 56 | | title | true | (string) title text to be placed on the card | 57 | | tagline | | (string) tagline text to be placed on the card | 58 | | cloudName | true | (string) your Cloudinary cloud name (i.e. your username) | 59 | | imagePublicID | true | (string) the public ID of your social image template | 60 | | cloudinaryUrlBase | | (string, default `https://res.cloudinary.com`) Cloudinary asset URL | 61 | | titleFont | | (string, default `arial`) font to use for rendering title | 62 | | titleExtraConfig | | (string) optional additional text overlay config | 63 | | taglineExtraConfig | | (string) optional additional text overlay config | 64 | | taglineFont | | (string, default `arial`) font to use for rendering tagline | 65 | | imageWidth | | (number, default `1280`) SEO image width (defaults to Twitter ratio) | 66 | | imageHeight | | (number, default `669`) SEO image height (defaults to Twitter ratio) | 67 | | textAreaWidth | | (number, default `760`) width of title and tagline text areas | 68 | | textLeftOffset | | (number, default `480`) distance from left edge to start text boxes | 69 | | titleGravity | | (string, default `south_west`) location the title is anchored from | 70 | | taglineGravity | | (string, default `north_west`) location the tagline is anchored from | 71 | | titleLeftOffset | | (number, `null`) distance from left edge to start text boxes | 72 | | taglineLeftOffset | | (number, default `null`) distance from left edge to start text boxes | 73 | | titleBottomOffset | | (number, default `254`) distance from bottom to start title text | 74 | | taglineTopOffset | | (number, default `445`) distance from top to start tagline text | 75 | | textColor | | (string, default `000000`) hex value for text color | 76 | | titleColor | | (string) hex value specific for title color. If this is not set, the color will be the one set to `textColor` | 77 | | taglineColor | | (string) hex value specific for tagline color. If this is not set, the color will be the one set to `textColor` | 78 | | titleFontSize | | (number, default `64`) font size to use for the title | 79 | | taglineFontSize | | (number, default `48`) font size to use for the tagline | 80 | | version | | (string) optional version string for caching | 81 | 82 | ### Setting config options 83 | 84 | ```js 85 | const socialImage = getShareImage({ 86 | title: 'My Post Title', 87 | tagline: 'A tagline for the post', 88 | cloudName: 'myusername', 89 | imagePublicID: 'my-template-image.jpg', 90 | titleExtraConfig: '_bold', // optional - set title font weight to bold 91 | textColor: '663399', // optional — set the color to purple 92 | }); 93 | ``` 94 | 95 | ## Who is using this? 96 | 97 | - [Echobind](https://echobind.com/) with their [blog image generator](https://github.com/echobind/blog-image-generator) 98 | - [@jsjoeio](https://github.com/jsjoeio) on his [personal website](https://github.com/jsjoeio/joeprevite.com) 99 | - [Horacio Herrera](https://horacioh.com) 100 | - [@chris_berry](https://twitter.com/chris_berry) on his [blog](https://chrisberry.io) 101 | - [@codebender828](https://twitter.com/codebender828) on his [blog](https://jbakebwa.dev) 102 | - [@ryan_c_harris](https://twitter.com/ryan_c_harris) on his [blog](https://ryanharris.dev) 103 | - [Idiomatic Programmers](https://idiomaticprogrammers.com/) 104 | --------------------------------------------------------------------------------