├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest-setup.js ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ ├── __image_snapshots__ │ │ ├── index-test-js-puppeteer-social-image-render-social-image-article-template-must-accept-expected-params-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-article-template-must-accept-subtitle-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-article-template-must-generate-as-a-preview-image-as-expected-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-background-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-color-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-family-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-size-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-weight-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-google-font-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-image-url-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-logo-and-watermark-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-logo-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-unsplash-id-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-text-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-url-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-generate-as-a-preview-image-as-expected-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-not-generate-as-a-preview-image-when-using-an-invalid-preview-size-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-basic-template-must-pass-all-params-to-custom-templates-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-expected-params-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-optional-params-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-split-diagonal-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-split-diagonal-reverse-param-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-generate-as-a-preview-image-as-expected-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-prefer-watermark-to-footer-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-must-accept-a-custom-browser-instance-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-as-expected-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-size-1-snap.png │ │ ├── index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-template-1-snap.png │ │ └── index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-template-when-providing-multiple-custom-templates-1-snap.png │ └── index.test.js ├── helpers │ ├── build-unsplash-url.js │ ├── compile-image-template.js │ ├── compile-preview.js │ ├── compile-template.js │ ├── index.js │ ├── resolve-base-params.js │ └── resolve-params.js ├── index.js └── templates │ ├── article.js │ ├── basic.js │ ├── fiftyfifty.js │ └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # testing 5 | coverage 6 | 7 | # production artifacts 8 | build 9 | dist 10 | lib 11 | 12 | # non-lintable JS/JSON 13 | /packages/generator-*/app/templates 14 | /packages/generator-*/generators/app/templates 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["json"], 3 | "extends": ["eslint:recommended", "prettier"], 4 | "parser": "babel-eslint", 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "sourceType": "module", 8 | "ecmaFeatures": { 9 | "jsx": true, 10 | "generators": true, 11 | "experimentalObjectRestSpread": true 12 | } 13 | }, 14 | "env": { 15 | "browser": true, 16 | "commonjs": true, 17 | "es6": true, 18 | "node": true, 19 | "jest": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | dist 12 | lib 13 | 14 | # misc 15 | .DS_Store 16 | *.log 17 | *.tgz 18 | .idea 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # `yarn` has its own opinions on `package.json`, so `prettier` should ignore it 2 | package.json 3 | 4 | 5 | # Non-committed directories 6 | node_modules 7 | coverage 8 | dist 9 | lib 10 | 11 | # Generated Markdown files 12 | CHANGELOG.md 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | version-git-message "release: v%s" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.8.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.8.0...v0.8.1) (2020-06-16) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * pass all params to custom templates ([327c4b3](https://github.com/chrisvxd/puppeteer-social-image/commit/327c4b3)) 7 | 8 | 9 | 10 | # [0.8.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.7.1...v0.8.0) (2020-06-05) 11 | 12 | 13 | ### Features 14 | 15 | * support custom image types without using a path ([bb621f7](https://github.com/chrisvxd/puppeteer-social-image/commit/bb621f7)) 16 | 17 | 18 | ### BREAKING CHANGES 19 | 20 | * the default type is now JPEG when not specifying the "output" param 21 | 22 | 23 | 24 | ## [0.7.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.7.0...v0.7.1) (2020-05-29) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * don't render broken previews for sizes that don't support it ([a8f3aa5](https://github.com/chrisvxd/puppeteer-social-image/commit/a8f3aa5)) 30 | 31 | 32 | 33 | # [0.7.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.6.0...v0.7.0) (2020-05-29) 34 | 35 | 36 | ### Features 37 | 38 | * add instagram sizes ([d2b5797](https://github.com/chrisvxd/puppeteer-social-image/commit/d2b5797)) 39 | * add pinterest size ([d3c652c](https://github.com/chrisvxd/puppeteer-social-image/commit/d3c652c)) 40 | * pass size param to custom templates ([400b1ec](https://github.com/chrisvxd/puppeteer-social-image/commit/400b1ec)) 41 | * support custom sizes ([787a49a](https://github.com/chrisvxd/puppeteer-social-image/commit/787a49a)) 42 | 43 | 44 | 45 | # [0.6.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.5.5...v0.6.0) (2020-02-03) 46 | 47 | 48 | ### Features 49 | 50 | * add logo and refined watermark to basic and article templates. Reduce font size. ([2539a79](https://github.com/chrisvxd/puppeteer-social-image/commit/2539a79)) 51 | 52 | 53 | 54 | ## [0.5.5](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.5.4...v0.5.5) (2020-01-30) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * ensure custom templates generate in all environments ([49d084a](https://github.com/chrisvxd/puppeteer-social-image/commit/49d084a)) 60 | 61 | 62 | 63 | ## [0.5.4](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.5.3...v0.5.4) (2020-01-30) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * fix invalid markup in article template, and add regression tests ([f7ef694](https://github.com/chrisvxd/puppeteer-social-image/commit/f7ef694)) 69 | 70 | 71 | 72 | ## [0.5.3](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.5.2...v0.5.3) (2020-01-30) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * fix invalid markup in all templates ([9ac863f](https://github.com/chrisvxd/puppeteer-social-image/commit/9ac863f)) 78 | * further fixes to image templates ([fe507f3](https://github.com/chrisvxd/puppeteer-social-image/commit/fe507f3)) 79 | * slight visual tweak to previews ([2096d6b](https://github.com/chrisvxd/puppeteer-social-image/commit/2096d6b)) 80 | 81 | 82 | 83 | ## [0.5.2](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.5.1...v0.5.2) (2020-01-30) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * don't nest tags in preview, and render styles correctly ([749d5f3](https://github.com/chrisvxd/puppeteer-social-image/commit/749d5f3)) 89 | * tidy up borders in preview ([8e3bb6f](https://github.com/chrisvxd/puppeteer-social-image/commit/8e3bb6f)) 90 | 91 | 92 | ### Performance Improvements 93 | 94 | * slightly increase preview performance by avoiding calling setContent twice ([cb88485](https://github.com/chrisvxd/puppeteer-social-image/commit/cb88485)) 95 | 96 | 97 | 98 | ## [0.5.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.5.0...v0.5.1) (2020-01-30) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * ensure images and previews get cropped correctly ([1f894cf](https://github.com/chrisvxd/puppeteer-social-image/commit/1f894cf)) 104 | 105 | 106 | ### Performance Improvements 107 | 108 | * increase preview performance by 2x by only taking 1 screenshot ([72431de](https://github.com/chrisvxd/puppeteer-social-image/commit/72431de)) 109 | 110 | 111 | 112 | # [0.5.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.4.1...v0.5.0) (2020-01-30) 113 | 114 | 115 | ### Features 116 | 117 | * add support for generating previews wrapped in Twitter-style frame ([231c0db](https://github.com/chrisvxd/puppeteer-social-image/commit/231c0db)) 118 | 119 | 120 | 121 | ## [0.4.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.4.0...v0.4.1) (2020-01-27) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * ensure fiftyfifty takes watermark param ([a48ba54](https://github.com/chrisvxd/puppeteer-social-image/commit/a48ba54)) 127 | 128 | 129 | 130 | # [0.4.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.3.2...v0.4.0) (2020-01-27) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * ensure fiftyfifty handles image position correctly ([b99183a](https://github.com/chrisvxd/puppeteer-social-image/commit/b99183a)) 136 | * ensure googleFont param works for all templates, and add tests ([5805a46](https://github.com/chrisvxd/puppeteer-social-image/commit/5805a46)) 137 | * fix aspect ratio of unsplash images in fiftyfifty template ([107a139](https://github.com/chrisvxd/puppeteer-social-image/commit/107a139)) 138 | 139 | 140 | ### Features 141 | 142 | * add fiftyfifty template ([9958a33](https://github.com/chrisvxd/puppeteer-social-image/commit/9958a33)) 143 | * add support for googleFont parameter across all templates ([19332f0](https://github.com/chrisvxd/puppeteer-social-image/commit/19332f0)) 144 | * add support for logging output for debugging ([25f70a3](https://github.com/chrisvxd/puppeteer-social-image/commit/25f70a3)) 145 | 146 | 147 | 148 | ## [0.3.2](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.3.1...v0.3.2) (2020-01-22) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * ensure pages are always closed if using userBrowser ([d3bca06](https://github.com/chrisvxd/puppeteer-social-image/commit/d3bca06)) 154 | * ensure puppeteer is always set to headless ([c89653c](https://github.com/chrisvxd/puppeteer-social-image/commit/c89653c)) 155 | 156 | 157 | 158 | ## [0.3.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.3.0...v0.3.1) (2020-01-18) 159 | 160 | 161 | ### Bug Fixes 162 | 163 | * ensure more complex templates render ([9fea492](https://github.com/chrisvxd/puppeteer-social-image/commit/9fea492)) 164 | * use valid fallback font for image templates ([d1a9dc9](https://github.com/chrisvxd/puppeteer-social-image/commit/d1a9dc9)) 165 | 166 | 167 | 168 | # [0.3.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.2.1...v0.3.0) (2020-01-18) 169 | 170 | 171 | ### Bug Fixes 172 | 173 | * wait for fonts to load before rendering screenshots ([04502eb](https://github.com/chrisvxd/puppeteer-social-image/commit/04502eb)) 174 | 175 | 176 | ### Features 177 | 178 | * add browser param to allow for external puppeteer configuration ([33cdfdc](https://github.com/chrisvxd/puppeteer-social-image/commit/33cdfdc)) 179 | 180 | 181 | ### Performance Improvements 182 | 183 | * don't include Lato webfont in custom templates ([9ac12ff](https://github.com/chrisvxd/puppeteer-social-image/commit/9ac12ff)) 184 | 185 | 186 | 187 | ## [0.2.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.2.0...v0.2.1) (2020-01-17) 188 | 189 | 190 | ### Bug Fixes 191 | 192 | * fix rollup import so we can compile ([d375f6a](https://github.com/chrisvxd/puppeteer-social-image/commit/d375f6a)) 193 | 194 | 195 | 196 | # [0.2.0](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.1.1...v0.2.0) (2020-01-17) 197 | 198 | 199 | ### Features 200 | 201 | * add article template ([76be6d3](https://github.com/chrisvxd/puppeteer-social-image/commit/76be6d3)) 202 | * add web API ([ff1143e](https://github.com/chrisvxd/puppeteer-social-image/commit/ff1143e)) 203 | * move .saasify to chrisvxd/og-impact ([6af4da5](https://github.com/chrisvxd/puppeteer-social-image/commit/6af4da5)) 204 | * revamp basic template - unsplash support, tighter line heights and watermarks ([a8010b2](https://github.com/chrisvxd/puppeteer-social-image/commit/a8010b2)) 205 | * support lambda, and make puppeteer a peer dependency ([d21ca5e](https://github.com/chrisvxd/puppeteer-social-image/commit/d21ca5e)) 206 | * switch templates to web font Lato, closest match to Avenir Next ([72e2c1a](https://github.com/chrisvxd/puppeteer-social-image/commit/72e2c1a)) 207 | 208 | 209 | ### BREAKING CHANGES 210 | 211 | * basic template now uses the Lato font instead of Avenir Next 212 | * make puppeteer a peer dependency 213 | 214 | 215 | 216 | ## [0.1.1](https://github.com/chrisvxd/puppeteer-social-image/compare/v0.1.0...v0.1.1) (2019-05-20) 217 | 218 | 219 | ### Bug Fixes 220 | 221 | * correct package description ([6271a0b](https://github.com/chrisvxd/puppeteer-social-image/commit/6271a0b)) 222 | 223 | 224 | 225 | # [0.1.0](https://github.com/chrisvxd/puppeteer-social-image/compare/270d144...v0.1.0) (2019-05-20) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * close puppeteer browser after execution ([d71c8e7](https://github.com/chrisvxd/puppeteer-social-image/commit/d71c8e7)) 231 | * ensure getImage runs ([8295f4d](https://github.com/chrisvxd/puppeteer-social-image/commit/8295f4d)) 232 | 233 | 234 | ### Features 235 | 236 | * actually write image to output ([6588b66](https://github.com/chrisvxd/puppeteer-social-image/commit/6588b66)) 237 | * add background param to basic template ([b070cfe](https://github.com/chrisvxd/puppeteer-social-image/commit/b070cfe)) 238 | * add image overlay and anchoring to basic template ([ac83f65](https://github.com/chrisvxd/puppeteer-social-image/commit/ac83f65)) 239 | * add initial getImage code ([270d144](https://github.com/chrisvxd/puppeteer-social-image/commit/270d144)) 240 | * add params to basic template ([bbcf58e](https://github.com/chrisvxd/puppeteer-social-image/commit/bbcf58e)) 241 | * add support for custom templates ([8bad772](https://github.com/chrisvxd/puppeteer-social-image/commit/8bad772)) 242 | * change getImage to default export ([13ef22c](https://github.com/chrisvxd/puppeteer-social-image/commit/13ef22c)) 243 | * return image buffer from render ([31193a4](https://github.com/chrisvxd/puppeteer-social-image/commit/31193a4)) 244 | * switch default image output to png ([3bf375e](https://github.com/chrisvxd/puppeteer-social-image/commit/3bf375e)) 245 | * tweaks to show off demo - it's working! ([d6f62c9](https://github.com/chrisvxd/puppeteer-social-image/commit/d6f62c9)) 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris Villa 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppeteer-social-image 2 | 3 | [![Build Status](https://travis-ci.com/chrisvxd/puppeteer-social-image.svg?branch=master)](https://travis-ci.com/chrisvxd/puppeteer-social-image) [![NPM](https://img.shields.io/npm/v/puppeteer-social-image.svg)](https://www.npmjs.com/package/puppeteer-social-image) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 4 | 5 | Create dynamic social share images using HTML + CSS via puppeteer. For a hosted version, see [OGIMPACT](https://github.com/chrisvxd/og-impact). 6 | 7 | ![img](https://i.ibb.co/PwVm1ky/Artboard.png) 8 | 9 | ## Installation 10 | 11 | ```sh 12 | npm i puppeteer-social-image --save 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Render "basic" template 18 | 19 | ```js 20 | import renderSocialImage from "puppeteer-social-image"; 21 | 22 | renderSocialImage({ 23 | template: "basic", 24 | templateParams: { 25 | imageUrl: 26 | "https://images.unsplash.com/photo-1557958114-3d2440207108?w=1950&q=80", 27 | title: "Hello, world" 28 | }, 29 | output: "image.png", 30 | size: "facebook" 31 | }); 32 | ``` 33 | 34 | ### Render custom template 35 | 36 | ```js 37 | import renderSocialImage from "puppeteer-social-image"; 38 | 39 | renderSocialImage({ 40 | templateBody: '
Hello, {{name}}!
', 41 | templateStyles: ".Main { color: blue; }", 42 | templateParams: { 43 | name: "Jane" 44 | }, 45 | output: "image.png", 46 | size: "facebook" 47 | }); 48 | ``` 49 | 50 | ### Render on a serverless function 51 | 52 | Add the [`puppeteer-serverless` package](https://github.com/saasify-sh/puppeteer-serverless), and pass it to the render function via the `browser` option: 53 | 54 | ```js 55 | import puppeteer from "puppeteer-serverless"; 56 | import renderSocialImage from "puppeteer-social-image"; 57 | 58 | export default async () => { 59 | await renderSocialImage({ 60 | template: "basic", 61 | templateParams: { 62 | imageUrl: 63 | "https://images.unsplash.com/photo-1557958114-3d2440207108?w=1950&q=80", 64 | title: "Hello, world" 65 | }, 66 | browser: await puppeteer.launch({}) 67 | }); 68 | }; 69 | ``` 70 | 71 | ## API 72 | 73 | ### renderSocialImage 74 | 75 | Returns `Promise`. 76 | 77 | Type: function (opts): Promise 78 | 79 | - `opts` (object) Configuration options 80 | - `opts.template` (string) Name of a prebuilt template. One of: 81 | - `basic` (default) 82 | - `article` 83 | - `fiftyfifty` 84 | - `opts.templateParams` (object) Params to be passed to the template. If using prebuilt templates, see below for APIs. 85 | - `opts.templateBody` (string?) Handlebars template to render in the body for a custom template. Populated with templateParams. 86 | - `opts.templateStyles` (string?) CSS to use for a custom template. Passed to the head. 87 | - `opts.customTemplates` (object?) Define multiple custom templates 88 | - `opts.customTemplates[key]` (string) Name for the customTemplate 89 | - `opts.customTemplates[key].templateBody`(string) Handlebars template to render in the body for this custom template. Populated with templateParams. 90 | - `opts.customTemplates[key].templateStyles`(string) CSS to use for this custom template. Passed to the head 91 | - `opts.output` (string?) Path to write image 92 | - `opts.type` (string?) Type of the output image. Overwritten by output path extension. One of: 93 | - `jpeg` (default) 94 | - `png` 95 | - `opts.jpegQuality` (number, default `90`) JPEG image quality 96 | - `opts.size` (string?) Preset size for the image. One of: 97 | - `facebook` 98 | - `twitter` (default) 99 | - `ig-landscape` 100 | - `ig-portrait` 101 | - `ig-square` 102 | - `ig-story` 103 | - `WIDTHxHEIGHT` Any width, height pairing 104 | - `opts.browser` (Browser?) Instance of puppeteer's `Browser` to use instead of the internal version. Useful for serverless functions, which may require [`chrome-aws-lambda`](https://www.npmjs.com/package/chrome-aws-lambda). This browser instance will not be automatically closed. 105 | - `opts.preview` (boolean?) Render the image with a chrome, as it would look on Twitter 106 | 107 | ## Templates 108 | 109 | ### `basic` 110 | 111 | A basic template to show some short text overlaying an image. 112 | 113 | basic template preview 114 | 115 | #### API 116 | 117 | - `title` (string) Title text for the image 118 | - `logo` (string?) URL to a logo to render above the text 119 | - `imageUrl` (string?) URL for the background image 120 | - `unsplashId` (string?) Unsplash ID for the background image 121 | - `unsplashKeywords` (string?) Unsplash keywords to use for the background image 122 | - `backgroundImageAnchor` (string?, default `"C"`) Anchor point for the background image. Valid options are `C`, `N`, `NE`, `E`, `SE`, `S`, `SW`, `W` or `NW`. 123 | - `backgroundImageOverlay` (boolean?, default `true`) Add a dark overlay on top of the background image 124 | - `background` (string?) CSS background prop. Prefer `imageUrl` if using image. 125 | - `color` (string?, default `"white"`) Color for the title 126 | - `googleFont` (string?) Name for Google font to load 127 | - `fontFamily` (string?, default `'"Lato", "Helvetica Neue", sans-serif'`) Font family 128 | - `fontSize` (string?, default `"128px"`) Font size 129 | - `watermark` (string?) Watermark text to render at the bottom of the image. 130 | 131 | ### `article` 132 | 133 | A template for an article, with an eyebrow that can be used for dates 134 | 135 | article template preview 136 | 137 | #### API 138 | 139 | - `title` (string) Title text 140 | - `subtitle` (string?) Subtitle text 141 | - `eyebrow` (string) Eyebrow text, rendered above the title, like a date 142 | - `logo` (string?) URL to a logo to render above the text 143 | - `imageUrl` (string?) URL for the background image 144 | - `unsplashId` (string?) Unsplash ID for the background image 145 | - `unsplashKeywords` (string?) Unsplash keywords to use for the background image 146 | - `backgroundImageAnchor` (string?, default `"C"`) Anchor point for the background image. Valid options are `C`, `N`, `NE`, `E`, `SE`, `S`, `SW`, `W` or `NW`. 147 | - `backgroundImageOverlay` (boolean?, default `true`) Add a dark overlay on top of the background image 148 | - `background` (string?) CSS background prop. Prefer `imageUrl` if using image. 149 | - `color` (string?, default `"white"`) Color for the text 150 | - `googleFont` (string?) Name for Google font to load 151 | - `fontFamily` (string?, default `'"Lato", "Helvetica Neue", sans-serif'`) Font family 152 | - `watermark` (string?) Watermark text to render at the bottom of the image. 153 | 154 | ### `fiftyfifty` 155 | 156 | A multiuse template for an array of content 157 | 158 | fiftyfifty template preview 159 | 160 | #### API 161 | 162 | - `title` (string) Title text 163 | - `subtitle` (string?) Subtitle text 164 | - `footer` (string) Footer text 165 | - `split` (`straight` | `diagonal` | `diagonal-reverse`, default `straight`) Style of split between content and image 166 | - `logo` (string?) URL for the logo image 167 | - `imageUrl` (string?) URL for the background image 168 | - `unsplashId` (string?) Unsplash ID for the background image 169 | - `unsplashKeywords` (string?) Unsplash keywords to use for the background image 170 | - `googleFont` (string?) Name for Google font to load 171 | - `fontFamily` (string?, default `'"Lato", "Helvetica Neue", sans-serif'`) Font family 172 | - `watermark` (string?) Watermark text to render in the bottom left. Same as `footer`. 173 | 174 | ## License 175 | 176 | MIT © [Chris Villa](http://www.chrisvilla.co.uk) 177 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | const typeEnumRules = require("@commitlint/config-angular-type-enum"); 2 | 3 | const typeEnum = typeEnumRules.rules["type-enum"]; 4 | 5 | // Add any additional commit types 6 | typeEnum[2].push("release"); 7 | 8 | module.exports = { 9 | extends: ["@commitlint/config-angular"], 10 | rules: { 11 | "type-enum": typeEnum 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | const { toMatchImageSnapshot } = require("jest-image-snapshot"); 2 | 3 | expect.extend({ toMatchImageSnapshot }); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-social-image", 3 | "version": "0.8.1", 4 | "description": "Create dynamic social share images using HTML + CSS via puppeteer.", 5 | "homepage": "https://github.com/chrisvxd/puppeteer-social-image", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/chrisvxd/puppeteer-social-image.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/chrisvxd/puppeteer-social-image/issues" 13 | }, 14 | "main": "dist/index.js", 15 | "module": "dist/index.es.js", 16 | "files": [ 17 | "dist/" 18 | ], 19 | "scripts": { 20 | "compile": "rollup -c", 21 | "cz": "git-cz", 22 | "format": "yarn run internal:prettier --write", 23 | "internal:prettier": "prettier \"./*.{js,json,md}\" \"./**/*.{js,json,md}\"", 24 | "lint": "npm-run-all --parallel lint:*", 25 | "lint:js": "eslint --ignore-path .eslintignore .", 26 | "lint:json": "eslint --ignore-path .eslintignore --ext .json .", 27 | "lint:md": "remark --quiet --frail .", 28 | "lint:format": "yarn run internal:prettier --list-different", 29 | "precompile": "if [ ${SKIP_CLEANUP:-0} -ne 1 ]; then rimraf lib/*; fi", 30 | "prepublishOnly": "yarn compile", 31 | "test": "jest src --modulePathIgnorePatterns \"/dist/\"", 32 | "release": "conventional-recommended-bump -p angular | xargs yarn version --new-version$1", 33 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md" 34 | }, 35 | "remarkConfig": { 36 | "presets": [ 37 | "lint-consistent", 38 | "lint-recommended" 39 | ], 40 | "plugins": { 41 | "lint": { 42 | "list-item-indent": "space", 43 | "heading-increment": true 44 | } 45 | } 46 | }, 47 | "dependencies": { 48 | "@babel/polyfill": "^7.4.4", 49 | "handlebars": "^4.1.2", 50 | "handlebars-jest": "^0.5.0" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "^7.2.3", 54 | "@babel/core": "^7.3.4", 55 | "@babel/preset-env": "^7.3.4", 56 | "@commitlint/cli": "^7.5.2", 57 | "@commitlint/config-angular": "^7.5.0", 58 | "babel-eslint": "^10.0.1", 59 | "babel-jest": "^24.8.0", 60 | "commitizen": "^3.0.7", 61 | "conventional-changelog-cli": "^2.0.12", 62 | "conventional-recommended-bump": "^4.0.4", 63 | "eslint": "^5.15.1", 64 | "eslint-config-prettier": "^4.1.0", 65 | "eslint-plugin-json": "~1.4.0", 66 | "file-type": "^14.6.0", 67 | "jest": "^24.4.0", 68 | "jest-image-snapshot": "^2.8.1", 69 | "npm-run-all": "^4.1.5", 70 | "prettier": "~1.16.4", 71 | "puppeteer": "^2.0.0", 72 | "remark-cli": "^6.0.1", 73 | "remark-lint": "^6.0.4", 74 | "remark-preset-lint-consistent": "2.0.2", 75 | "remark-preset-lint-recommended": "3.0.2", 76 | "rimraf": "^2.6.3", 77 | "rollup": "^0.64.1", 78 | "rollup-plugin-babel": "^4.0.1", 79 | "rollup-plugin-commonjs": "^9.1.3", 80 | "rollup-plugin-json": "^4.0.0", 81 | "rollup-plugin-node-builtins": "^2.1.2", 82 | "rollup-plugin-node-globals": "^1.4.0", 83 | "rollup-plugin-node-resolve": "^3.3.0", 84 | "rollup-plugin-peer-deps-external": "^2.2.0", 85 | "rollup-plugin-url": "^1.4.0", 86 | "tempy": "^0.3.0" 87 | }, 88 | "peerDependencies": { 89 | "puppeteer": "2.x" 90 | }, 91 | "config": { 92 | "commitizen": { 93 | "path": "cz-conventional-changelog" 94 | } 95 | }, 96 | "jest": { 97 | "collectCoverage": true, 98 | "collectCoverageFrom": [ 99 | "/src/**/*.{js,jsx,ts,tsx}" 100 | ], 101 | "transform": { 102 | "^.+\\.js$": "/node_modules/babel-jest", 103 | "^.+\\.hbs$": "/node_modules/handlebars-jest" 104 | }, 105 | "setupFilesAfterEnv": [ 106 | "/jest-setup.js" 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import external from "rollup-plugin-peer-deps-external"; 4 | import resolve from "rollup-plugin-node-resolve"; 5 | import url from "rollup-plugin-url"; 6 | import json from "rollup-plugin-json"; 7 | import builtins from "rollup-plugin-node-builtins"; 8 | import globals from "rollup-plugin-node-globals"; 9 | 10 | import pkg from "./package.json"; 11 | 12 | export default { 13 | input: "src/index.js", 14 | output: [ 15 | { 16 | file: pkg.main, 17 | format: "cjs", 18 | sourcemap: true 19 | }, 20 | { 21 | file: pkg.module, 22 | format: "es", 23 | sourcemap: true 24 | } 25 | ], 26 | plugins: [ 27 | builtins(), 28 | globals(), 29 | external(), 30 | json(), 31 | url({ exclude: ["**/*.svg"] }), 32 | babel({ 33 | exclude: "node_modules/**" 34 | }), 35 | resolve(), 36 | commonjs() 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-article-template-must-accept-expected-params-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-article-template-must-accept-expected-params-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-article-template-must-accept-subtitle-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-article-template-must-accept-subtitle-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-article-template-must-generate-as-a-preview-image-as-expected-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-article-template-must-generate-as-a-preview-image-as-expected-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-background-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-background-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-color-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-color-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-family-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-family-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-size-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-size-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-weight-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-font-weight-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-google-font-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-google-font-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-image-url-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-image-url-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-logo-and-watermark-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-logo-and-watermark-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-logo-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-logo-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-unsplash-id-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-unsplash-id-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-text-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-text-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-url-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-accept-watermark-url-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-generate-as-a-preview-image-as-expected-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-generate-as-a-preview-image-as-expected-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-not-generate-as-a-preview-image-when-using-an-invalid-preview-size-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-not-generate-as-a-preview-image-when-using-an-invalid-preview-size-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-pass-all-params-to-custom-templates-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-basic-template-must-pass-all-params-to-custom-templates-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-expected-params-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-expected-params-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-optional-params-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-optional-params-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-split-diagonal-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-split-diagonal-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-split-diagonal-reverse-param-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-accept-split-diagonal-reverse-param-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-generate-as-a-preview-image-as-expected-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-generate-as-a-preview-image-as-expected-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-prefer-watermark-to-footer-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-fiftyfifty-template-must-prefer-watermark-to-footer-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-accept-a-custom-browser-instance-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-accept-a-custom-browser-instance-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-as-expected-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-as-expected-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-size-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-size-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-template-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-template-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-template-when-providing-multiple-custom-templates-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisvxd/puppeteer-social-image/51dff188470007a28dfb74ca7294b22f1394c1a1/src/__tests__/__image_snapshots__/index-test-js-puppeteer-social-image-render-social-image-must-generate-an-image-with-a-custom-template-when-providing-multiple-custom-templates-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import tempy from "tempy"; 3 | import renderSocialImage, { setTestMode } from "../index"; 4 | import puppeteer from "puppeteer"; 5 | import FileType from "file-type"; 6 | 7 | const snapshotConfig = { 8 | failureThreshold: 0.025, 9 | failureThresholdType: "percent", 10 | dumpDiffToConsole: true 11 | }; 12 | 13 | // Disable external fonts when testing 14 | setTestMode(true); 15 | 16 | describe("puppeteer-social-image", () => { 17 | describe("renderSocialImage", () => { 18 | let tempPath; 19 | 20 | beforeEach(() => { 21 | tempPath = tempy.file({ extension: "png" }); 22 | }); 23 | 24 | it("must generate an image as expected", async () => { 25 | await renderSocialImage({ 26 | templateParams: { 27 | title: "Hello, twitter! @chrisvxd" 28 | }, 29 | output: tempPath, 30 | size: "facebook" 31 | }); 32 | 33 | const testImage = fs.readFileSync(tempPath); 34 | 35 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 36 | }); 37 | 38 | it("must use the correct default content type", async () => { 39 | const data = await renderSocialImage({ 40 | templateParams: { 41 | title: "Hello, twitter! @chrisvxd" 42 | }, 43 | size: "facebook" 44 | }); 45 | 46 | const { mime } = await FileType.fromBuffer(data); 47 | 48 | expect(mime).toEqual("image/jpeg"); 49 | }); 50 | 51 | it("must use the correct content type when specified", async () => { 52 | const data = await renderSocialImage({ 53 | templateParams: { 54 | title: "Hello, twitter! @chrisvxd" 55 | }, 56 | size: "facebook", 57 | type: "png" 58 | }); 59 | 60 | const { mime } = await FileType.fromBuffer(data); 61 | 62 | expect(mime).toEqual("image/png"); 63 | }); 64 | 65 | it("must infer the content from the path, even when type is specified", async () => { 66 | const data = await renderSocialImage({ 67 | output: tempPath, 68 | templateParams: { 69 | title: "Hello, twitter! @chrisvxd" 70 | }, 71 | size: "facebook", 72 | type: "jpeg" 73 | }); 74 | 75 | const { mime } = await FileType.fromBuffer(data); 76 | 77 | expect(mime).toEqual("image/png"); 78 | }); 79 | 80 | it("must generate an image with a custom size", async () => { 81 | await renderSocialImage({ 82 | templateParams: { 83 | title: "Hello, twitter! @chrisvxd" 84 | }, 85 | output: tempPath, 86 | size: "512x512" 87 | }); 88 | 89 | const testImage = fs.readFileSync(tempPath); 90 | 91 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 92 | }); 93 | 94 | it("must accept a custom browser instance", async () => { 95 | const browser = await puppeteer.launch(); 96 | 97 | const spy = jest.spyOn(browser, "newPage"); 98 | 99 | await renderSocialImage({ 100 | templateParams: { 101 | title: "Hello, twitter! @chrisvxd" 102 | }, 103 | output: tempPath, 104 | size: "facebook", 105 | browser 106 | }); 107 | 108 | const testImage = fs.readFileSync(tempPath); 109 | 110 | expect(spy).toHaveBeenCalled(); 111 | 112 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 113 | 114 | await browser.close(); 115 | }); 116 | 117 | describe("basic Template", () => { 118 | it("must accept imageUrl param", async () => { 119 | await renderSocialImage({ 120 | templateParams: { 121 | title: "Hello, twitter! @chrisvxd", 122 | imageUrl: 123 | "https://images.unsplash.com/photo-1557996199-8d219159a1d0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=200&q=30" 124 | }, 125 | output: tempPath 126 | }); 127 | 128 | const testImage = fs.readFileSync(tempPath); 129 | 130 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 131 | }); 132 | 133 | it("must accept unsplashId param", async () => { 134 | await renderSocialImage({ 135 | templateParams: { 136 | title: "Hello, twitter! @chrisvxd", 137 | unsplashId: "2S4FDh3AtGw" 138 | }, 139 | output: tempPath 140 | }); 141 | 142 | const testImage = fs.readFileSync(tempPath); 143 | 144 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 145 | }); 146 | 147 | // Can't test keywords because it uses a random image each time 148 | // it("must accept unsplashKeywords param", async () => { 149 | // await renderSocialImage({ 150 | // templateParams: { 151 | // title: "Hello, twitter! @chrisvxd", 152 | // unsplashKeywords: "mountains" 153 | // }, 154 | // output: tempPath 155 | // }); 156 | 157 | // const testImage = fs.readFileSync(tempPath); 158 | 159 | // expect(testImage).toMatchImageSnapshot(snapshotConfig); 160 | // }); 161 | 162 | // it("must accept googleFont param", async () => { 163 | // await renderSocialImage({ 164 | // templateParams: { 165 | // title: "Hello, twitter! @chrisvxd", 166 | // googleFont: "Sigmar One" 167 | // }, 168 | // output: tempPath, 169 | // compileArgs: { 170 | // testMode: false 171 | // } 172 | // }); 173 | 174 | // const testImage = fs.readFileSync(tempPath); 175 | 176 | // expect(testImage).toMatchImageSnapshot(snapshotConfig); 177 | // }); 178 | 179 | it("must pass all params to custom templates", async () => { 180 | // These params were previously suppressed, causing confusion when creating custom templates 181 | const body = `
182 |

Name: {{ name }}

183 |

googleFont: {{googleFont}}

184 |

fontFamily: {{fontFamily}}

185 |

unsplashId: {{unsplashId}}

186 |

unsplashKeywords: {{unsplashKeywords}}

187 |

size: {{size.width}}x{{size.height}}

188 |
`; 189 | 190 | const styles = ""; 191 | 192 | await renderSocialImage({ 193 | templateParams: { 194 | name: "@chrisvxd", 195 | googleFont: "Sigmar One", 196 | unsplashKeywords: "cat" 197 | }, 198 | templateBody: body, 199 | templateStyles: styles, 200 | output: tempPath, 201 | compileArgs: { 202 | testMode: true // We don't actually need to render googleFont 203 | } 204 | }); 205 | 206 | const testImage = fs.readFileSync(tempPath); 207 | 208 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 209 | }); 210 | 211 | it("must accept background param", async () => { 212 | await renderSocialImage({ 213 | templateParams: { 214 | title: "Hello, twitter! @chrisvxd", 215 | background: "blue" 216 | }, 217 | output: tempPath 218 | }); 219 | 220 | const testImage = fs.readFileSync(tempPath); 221 | 222 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 223 | }); 224 | 225 | it("must accept color param", async () => { 226 | await renderSocialImage({ 227 | templateParams: { 228 | title: "Hello, twitter! @chrisvxd", 229 | color: "hotpink" 230 | }, 231 | output: tempPath 232 | }); 233 | 234 | const testImage = fs.readFileSync(tempPath); 235 | 236 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 237 | }); 238 | 239 | it("must accept fontFamily param", async () => { 240 | await renderSocialImage({ 241 | templateParams: { 242 | title: "Hello, twitter! @chrisvxd", 243 | fontFamily: '"Times New Roman"' 244 | }, 245 | output: tempPath 246 | }); 247 | 248 | const testImage = fs.readFileSync(tempPath); 249 | 250 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 251 | }); 252 | 253 | it("must accept fontSize param", async () => { 254 | await renderSocialImage({ 255 | templateParams: { 256 | title: "Hello, twitter! @chrisvxd", 257 | fontSize: "64px" 258 | }, 259 | output: tempPath 260 | }); 261 | 262 | const testImage = fs.readFileSync(tempPath); 263 | 264 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 265 | }); 266 | 267 | it("must accept fontWeight param", async () => { 268 | await renderSocialImage({ 269 | templateParams: { 270 | title: "Hello, twitter! @chrisvxd", 271 | fontWeight: "400" 272 | }, 273 | output: tempPath 274 | }); 275 | 276 | const testImage = fs.readFileSync(tempPath); 277 | 278 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 279 | }); 280 | 281 | it("must accept logo param", async () => { 282 | await renderSocialImage({ 283 | templateParams: { 284 | title: "Hello, twitter! @chrisvxd", 285 | logo: "https://i.imgur.com/L5ujMCQ.png" 286 | }, 287 | output: tempPath 288 | }); 289 | 290 | const testImage = fs.readFileSync(tempPath); 291 | 292 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 293 | }); 294 | 295 | it("must accept watermark param", async () => { 296 | await renderSocialImage({ 297 | templateParams: { 298 | title: "Hello, twitter! @chrisvxd", 299 | watermark: "example.com" 300 | }, 301 | output: tempPath 302 | }); 303 | 304 | const testImage = fs.readFileSync(tempPath); 305 | 306 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 307 | }); 308 | 309 | it("must accept logo and watermark param", async () => { 310 | await renderSocialImage({ 311 | templateParams: { 312 | title: "Hello, twitter! @chrisvxd", 313 | watermark: "example.com", 314 | logo: "https://i.imgur.com/L5ujMCQ.png" 315 | }, 316 | output: tempPath 317 | }); 318 | 319 | const testImage = fs.readFileSync(tempPath); 320 | 321 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 322 | }); 323 | 324 | it("must generate as a preview image as expected", async () => { 325 | await renderSocialImage({ 326 | templateParams: { 327 | title: "Hello, twitter! @chrisvxd" 328 | }, 329 | output: tempPath, 330 | size: "facebook", 331 | preview: true 332 | }); 333 | 334 | const testImage = fs.readFileSync(tempPath); 335 | 336 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 337 | }); 338 | 339 | it("must not generate as a preview image when using an invalid preview size", async () => { 340 | await renderSocialImage({ 341 | templateParams: { 342 | title: "Hello, twitter! @chrisvxd" 343 | }, 344 | output: tempPath, 345 | size: "ig-story", 346 | preview: true 347 | }); 348 | 349 | const testImage = fs.readFileSync(tempPath); 350 | 351 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 352 | }); 353 | }); 354 | 355 | describe("article Template", () => { 356 | it("must accept expected params", async () => { 357 | await renderSocialImage({ 358 | template: "article", 359 | templateParams: { 360 | eyebrow: "27 AUGUST / REMOTE", 361 | title: "What not to do when remote working", 362 | unsplashId: "2S4FDh3AtGw", 363 | watermark: "example.com", 364 | logo: "https://i.imgur.com/L5ujMCQ.png" 365 | }, 366 | size: "facebook", 367 | 368 | output: tempPath 369 | }); 370 | 371 | const testImage = fs.readFileSync(tempPath); 372 | 373 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 374 | }); 375 | 376 | it("must accept subtitle", async () => { 377 | await renderSocialImage({ 378 | template: "article", 379 | templateParams: { 380 | eyebrow: "27 AUGUST / REMOTE", 381 | title: "What not to do when remote working", 382 | subtitle: "A simple guide on what to avoid when working at home." 383 | }, 384 | output: tempPath 385 | }); 386 | 387 | const testImage = fs.readFileSync(tempPath); 388 | 389 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 390 | }); 391 | 392 | it("must generate as a preview image as expected", async () => { 393 | await renderSocialImage({ 394 | template: "article", 395 | templateParams: { 396 | eyebrow: "27 AUGUST / REMOTE", 397 | title: "What not to do when remote working", 398 | unsplashId: "2S4FDh3AtGw" 399 | }, 400 | output: tempPath, 401 | size: "facebook", 402 | preview: true 403 | }); 404 | 405 | const testImage = fs.readFileSync(tempPath); 406 | 407 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 408 | }); 409 | }); 410 | 411 | describe("fiftyfifty Template", () => { 412 | it("must accept expected params", async () => { 413 | await renderSocialImage({ 414 | template: "fiftyfifty", 415 | templateParams: { 416 | title: "What not to do when remote working", 417 | unsplashId: "2S4FDh3AtGw" 418 | }, 419 | output: tempPath 420 | }); 421 | 422 | const testImage = fs.readFileSync(tempPath); 423 | 424 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 425 | }); 426 | 427 | it("must accept split=diagonal param", async () => { 428 | await renderSocialImage({ 429 | template: "fiftyfifty", 430 | templateParams: { 431 | title: "What not to do when remote working", 432 | split: "diagonal", 433 | unsplashId: "2S4FDh3AtGw" 434 | }, 435 | output: tempPath 436 | }); 437 | 438 | const testImage = fs.readFileSync(tempPath); 439 | 440 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 441 | }); 442 | 443 | it("must accept split=diagonal-reverse param", async () => { 444 | await renderSocialImage({ 445 | template: "fiftyfifty", 446 | templateParams: { 447 | title: "What not to do when remote working", 448 | split: "diagonal-reverse", 449 | unsplashId: "2S4FDh3AtGw" 450 | }, 451 | output: tempPath 452 | }); 453 | 454 | const testImage = fs.readFileSync(tempPath); 455 | 456 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 457 | }); 458 | 459 | it("must prefer watermark to footer", async () => { 460 | await renderSocialImage({ 461 | template: "fiftyfifty", 462 | templateParams: { 463 | title: "What not to do when remote working", 464 | footer: "Test Footer", 465 | watermark: "Test Watermark", 466 | unsplashId: "2S4FDh3AtGw" 467 | }, 468 | output: tempPath 469 | }); 470 | 471 | const testImage = fs.readFileSync(tempPath); 472 | 473 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 474 | }); 475 | 476 | it("must accept optional params", async () => { 477 | await renderSocialImage({ 478 | template: "fiftyfifty", 479 | templateParams: { 480 | title: "What not to do when remote working", 481 | subtitle: "A simple guide on what to avoid when working at home.", 482 | footer: "Test Footer", 483 | logo: 484 | "data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0nMjA5NicgaGVpZ2h0PSc0NDAnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGcgZmlsbD0nbm9uZScgZmlsbC1ydWxlPSdldmVub2RkJz48cmVjdCBmaWxsPScjRkZFNjAwJyB3aWR0aD0nNDQwJyBoZWlnaHQ9JzQ0MCcgcng9JzUyJy8+PGcgdHJhbnNmb3JtPSd0cmFuc2xhdGUoODYgMTQyKSc+PHBhdGggZD0nTTE3Ni4xMzkgOTcuMTVsLTE2LjA5MSA1Mi45MDNTMTU4Ljg2NCAxNTUgMTUzLjY1NSAxNTVoLTE5Ljg4N2MtNi4xNTUgMC03LjU3Ni01LjQxOC03LjU3Ni01LjQxOGwtMjMuNjc1LTc1LjE0NC0yMy42NzUgNzUuMTQ0Uzc3LjQyMiAxNTUgNzEuMjY2IDE1NUg1MC40M2MtNS4yMDggMC02LjM5Mi00Ljk0Ny02LjM5Mi00Ljk0N0wuMjQgNi44MzFDLS40NyA0LjAwNS4yNCAwIDQuNTAyIDBoMjYuMDQzYzcuMTAzIDAgOC41MjMgNC43MTEgOS40NyA3LjUzOGwyMy40MzkgODAuMDkxTDg2LjE4IDcuNTM4Qzg3LjEyOCA0LjI0IDg4Ljc4NSAwIDk0LjIzMSAwaDE2LjU3MmM1LjIwOSAwIDYuODY2IDQuMjQgNy44MTMgNy41MzhsMjIuOTY1IDgwLjA5MSAyMy40MzgtODAuMDkxYy42Mi0yLjQ2NyAxLjk2Mi02LjM3IDcuMDE1LTcuMzIzLjY1LS4xNDIgMS40LS4yMTUgMi4yNjUtLjIxNWg0Ny40NDlDMjU4LjAyIDAgMjc3IDE5LjcyNCAyNzcgNDguNjhjMCAyOS4xNjUtMTkuNDAxIDQ4LjQ3LTU1LjQ2MyA0OC40N0gxNzYuMTR6bTQ0LjcyLTMwLjY0NWMxNS4zNzIgMCAyMi4wNzMtOC40MzEgMjIuMDczLTE4LjIzNSAwLTEwLjE5Ny02LjctMTguNDMyLTIyLjA3Mi0xOC40MzJoLTIzLjQ0M2wtMTEuMTcyIDM2LjY2N2gzNC42MTV6JyBmaWxsLW9wYWNpdHk9Jy44NycgZmlsbD0nIzAwMCcvPjxjaXJjbGUgZmlsbD0nIzIxMUQwMCcgY3g9JzI2MycgY3k9JzE0MScgcj0nMTQnLz48L2c+PHRleHQgZm9udC1mYW1pbHk9J0F2ZW5pck5leHQtRGVtaUJvbGQsIEF2ZW5pciBOZXh0JyBmb250LXNpemU9JzIxMCcgZm9udC13ZWlnaHQ9JzUwMCcgbGV0dGVyLXNwYWNpbmc9JzE3LjIxMycgZmlsbD0nIzIxMUQwMCc+PHRzcGFuIHg9JzU2MCcgeT0nMjk3Jz5XRUxMUEFJRC5JTzwvdHNwYW4+PC90ZXh0PjwvZz48L3N2Zz4=", 485 | unsplashId: "2S4FDh3AtGw" 486 | }, 487 | output: tempPath 488 | }); 489 | 490 | const testImage = fs.readFileSync(tempPath); 491 | 492 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 493 | }); 494 | 495 | it("must generate as a preview image as expected", async () => { 496 | await renderSocialImage({ 497 | template: "fiftyfifty", 498 | templateParams: { 499 | title: "What not to do when remote working", 500 | unsplashId: "2S4FDh3AtGw" 501 | }, 502 | output: tempPath, 503 | size: "facebook", 504 | preview: true 505 | }); 506 | 507 | const testImage = fs.readFileSync(tempPath); 508 | 509 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 510 | }); 511 | }); 512 | 513 | it("must generate an image with a custom template", async () => { 514 | const body = `
Hello, {{name}}!
`; 515 | 516 | const styles = ` 517 | .Main { 518 | align-items: center; 519 | background: rebeccapurple; 520 | color: white; 521 | display: flex; 522 | justify-content: center; 523 | font-family: Arial; 524 | font-size: 128px; 525 | font-weight: 700; 526 | padding: 32px; 527 | text-align: center; 528 | width: 100%; 529 | height: 100%; 530 | } 531 | `; 532 | 533 | await renderSocialImage({ 534 | templateBody: body, 535 | templateStyles: styles, 536 | templateParams: { 537 | name: "@chrisvxd" 538 | }, 539 | output: tempPath 540 | }); 541 | 542 | const testImage = fs.readFileSync(tempPath); 543 | 544 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 545 | }); 546 | 547 | it("must generate an image with a custom template when providing multiple custom templates", async () => { 548 | const body = `
Hello, {{name}}!
`; 549 | 550 | const styles = ` 551 | .Main { 552 | align-items: center; 553 | background: rebeccapurple; 554 | color: white; 555 | display: flex; 556 | justify-content: center; 557 | font-family: Arial; 558 | font-size: 128px; 559 | font-weight: 700; 560 | padding: 32px; 561 | text-align: center; 562 | width: 100%; 563 | height: 100%; 564 | } 565 | `; 566 | 567 | await renderSocialImage({ 568 | customTemplates: { 569 | foo: { 570 | body, 571 | styles 572 | }, 573 | bar: { 574 | body, 575 | styles 576 | } 577 | }, 578 | template: "bar", 579 | templateParams: { 580 | name: "chrisvxd" 581 | }, 582 | output: tempPath 583 | }); 584 | 585 | const testImage = fs.readFileSync(tempPath); 586 | 587 | expect(testImage).toMatchImageSnapshot(snapshotConfig); 588 | }); 589 | }); 590 | }); 591 | -------------------------------------------------------------------------------- /src/helpers/build-unsplash-url.js: -------------------------------------------------------------------------------- 1 | export default ({ unsplashId, unsplashKeywords, size }) => { 2 | const baseUrl = "https://source.unsplash.com"; 3 | const sizeStr = `${size.width}x${size.height}`; 4 | 5 | if (unsplashId) { 6 | return `${baseUrl}/${unsplashId}/${sizeStr}`; 7 | } else if (unsplashKeywords) { 8 | return `${baseUrl}/${sizeStr}?${unsplashKeywords}`; 9 | } 10 | 11 | throw new Error("No ID or keywords specified for unsplash URL"); 12 | }; 13 | -------------------------------------------------------------------------------- /src/helpers/compile-image-template.js: -------------------------------------------------------------------------------- 1 | import { compileTemplate, resolveParams } from "."; 2 | 3 | const buildStyles = ({ 4 | background = "white", 5 | imageUrl, 6 | color = background === "white" && !imageUrl ? "black" : "white", 7 | additionalStyles = "" 8 | } = {}) => ` 9 | ${additionalStyles} 10 | 11 | .Image { 12 | width: 100%; 13 | height: 100%; 14 | background-size: cover; 15 | background-repeat: no-repeat; 16 | background-origin: center; 17 | } 18 | 19 | .Main { 20 | background: ${imageUrl ? "transparent" : background}; 21 | position: relative; 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | .Inner { 27 | background: ${imageUrl ? "transparent" : background}; 28 | color: ${color}; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 100%; 33 | height: 100%; 34 | } 35 | `; 36 | 37 | export default ({ body, styles, templateParams, ...params }) => 38 | compileTemplate({ 39 | body: ` 40 |
41 | {{#if imageUrl}} 42 |
43 | {{/if}} 44 | 45 |
46 | ${body} 47 |
48 |
49 | `, 50 | styles: buildStyles( 51 | resolveParams({ 52 | templateParams: { 53 | ...templateParams, 54 | additionalStyles: styles 55 | }, 56 | size: params.size 57 | }) 58 | ), 59 | templateParams: { googleFont: "Lato", ...templateParams }, 60 | ...params 61 | }); 62 | -------------------------------------------------------------------------------- /src/helpers/compile-preview.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const handlebars = require("handlebars"); 4 | 5 | export default ({ body, styles, params, compileArgs }) => { 6 | const compiled = handlebars.compile( 7 | ` 8 | 9 | 10 | 25 | 26 | 27 | 28 |
29 |
30 | {{{body}}} 31 |
32 |
33 |
Web page
34 |
35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 36 | eiusmod tempor incididunt ut labore et dolore magna aliqua. 37 |
38 |
example.com
39 |
40 |
41 | 42 | 43 | 44 | ` 45 | )({ 46 | body: handlebars.compile(body)(params), 47 | styles 48 | }); 49 | 50 | if (compileArgs.log) { 51 | console.log("Compiled Output:", compiled); 52 | } 53 | 54 | return compiled; 55 | }; 56 | -------------------------------------------------------------------------------- /src/helpers/compile-template.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { resolveBaseParams, resolveParams } from "."; 4 | const handlebars = require("handlebars"); 5 | 6 | export default ({ 7 | body, 8 | styles, 9 | templateParams, 10 | size, 11 | compileArgs, 12 | imageSize 13 | }) => { 14 | const baseParams = resolveBaseParams({ 15 | templateParams, 16 | size, 17 | imageSize, 18 | compileArgs 19 | }); 20 | const params = resolveParams({ 21 | templateParams, 22 | size, 23 | imageSize, 24 | compileArgs 25 | }); 26 | 27 | const compiledBody = handlebars.compile(body)(params); 28 | 29 | const compiled = handlebars.compile( 30 | ` 31 | 32 | 33 | {{#unless testMode}} 34 | {{#if googleFont}} 35 | 36 | {{/if}} 37 | {{/unless}} 38 | 39 | {{{head}}} 40 | 41 | 58 | 59 | 60 | 61 | {{{body}}} 62 | 63 | 64 | 65 | ` 66 | )({ 67 | ...baseParams, 68 | body: compiledBody, 69 | styles 70 | }); 71 | 72 | if (compileArgs.log) { 73 | console.log( 74 | "Unresolved Template Params:", 75 | JSON.stringify(templateParams, null, 2) 76 | ); 77 | console.log("Base Template Params:", JSON.stringify(baseParams, null, 2)); 78 | console.log("Template Params:", JSON.stringify(params, null, 2)); 79 | console.log("---"); 80 | console.log("Compiled Output:", compiled); 81 | } 82 | 83 | return { 84 | html: compiled, 85 | body: compiledBody, 86 | styles 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export { default as buildUnsplashUrl } from "./build-unsplash-url"; 2 | export { default as compileTemplate } from "./compile-template"; 3 | export { default as compileImageTemplate } from "./compile-image-template"; 4 | export { default as resolveBaseParams } from "./resolve-base-params"; 5 | export { default as resolveParams } from "./resolve-params"; 6 | -------------------------------------------------------------------------------- /src/helpers/resolve-base-params.js: -------------------------------------------------------------------------------- 1 | export default ({ templateParams, size, compileArgs }) => { 2 | const { 3 | fontFamily = templateParams.googleFont 4 | ? `"${templateParams.googleFont}", Arial` 5 | : "Arial" 6 | } = templateParams; 7 | 8 | return { 9 | googleFont: templateParams.googleFont 10 | ? templateParams.googleFont.replace(" ", "+") 11 | : null, 12 | fontFamily, 13 | height: size.height, 14 | width: size.width, 15 | testMode: compileArgs.testMode 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/helpers/resolve-params.js: -------------------------------------------------------------------------------- 1 | import { buildUnsplashUrl } from "."; 2 | 3 | export default ({ templateParams, size, imageSize = size }) => { 4 | const { unsplashId, unsplashKeywords, googleFont } = templateParams; 5 | 6 | let imageUrl = templateParams.imageUrl; 7 | 8 | if (unsplashId) { 9 | imageUrl = buildUnsplashUrl({ unsplashId, size: imageSize }); 10 | } else if (unsplashKeywords) { 11 | imageUrl = buildUnsplashUrl({ unsplashKeywords, size: imageSize }); 12 | } 13 | 14 | return { 15 | fontFamily: `"${googleFont}", Arial`, 16 | size, 17 | ...templateParams, 18 | backgroundImageOverlay: 19 | typeof templateParams.gradient !== "undefined" 20 | ? templateParams.gradient 21 | : true, 22 | imageUrl, 23 | includeWatermark: 24 | templateParams.watermarkUrl || templateParams.watermark || false 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "@babel/polyfill"; 2 | import path from "path"; 3 | import templates from "./templates"; 4 | import { compileTemplate } from "./helpers"; 5 | import compilePreview from "./helpers/compile-preview"; 6 | 7 | const sizeMap = { 8 | facebook: { width: 1200, height: 630 }, 9 | twitter: { width: 1200, height: 630 }, 10 | "ig-landscape": { width: 1080, height: 608 }, 11 | "ig-square": { width: 1080, height: 1080 }, 12 | "ig-portrait": { width: 1080, height: 1350 }, 13 | "ig-story": { width: 1080, height: 1920 }, 14 | pinterest: { width: 1000, height: 1500 } 15 | }; 16 | 17 | let testMode = false; 18 | 19 | export const setTestMode = val => { 20 | testMode = val; 21 | }; 22 | 23 | /** 24 | * Renders the given HTML as an image via Puppeteer. 25 | */ 26 | export default async ({ 27 | jpegQuality = 90, 28 | output = "", 29 | type: userType = "jpeg", 30 | size = "twitter", 31 | template = "basic", 32 | templateParams = {}, 33 | templateBody, 34 | templateStyles = "", 35 | customTemplates = {}, 36 | browser: userBrowser, 37 | preview = false, 38 | compileArgs = {} 39 | }) => { 40 | // Resolve preferences 41 | let _size = sizeMap[size]; 42 | 43 | if (!_size) { 44 | if (size.indexOf("x") === -1) { 45 | throw new Error("Size is invalid"); 46 | } 47 | 48 | const sizeSplit = size.split("x"); 49 | _size = { width: parseInt(sizeSplit[0]), height: parseInt(sizeSplit[1]) }; 50 | } 51 | 52 | const { width, height } = _size; 53 | const customTemplate = customTemplates[template]; 54 | const createTemplate = template && templates[template]; 55 | const ext = path 56 | .extname(output) 57 | .slice(1) 58 | .toLowerCase(); 59 | const type = output 60 | ? ext === "jpg" || ext === "jpeg" 61 | ? "jpeg" 62 | : "png" 63 | : userType; 64 | 65 | let browser = userBrowser; 66 | 67 | if (!userBrowser) { 68 | let puppeteer; 69 | 70 | try { 71 | puppeteer = require("puppeteer"); 72 | } catch (err) { 73 | throw new Error( 74 | "Puppeteer was not installed. Either install puppeteer@^2.0.0 as a peer dependency, or provide the `browser` arg" 75 | ); 76 | } 77 | 78 | browser = await puppeteer.launch({ 79 | headless: true 80 | }); 81 | } 82 | 83 | const page = await browser.newPage(); 84 | await page.setViewport({ 85 | width, 86 | height 87 | }); 88 | // Using template builders instead of handlebars templates allows 89 | // us to hide size, body and styles from the user template 90 | 91 | const usingCustomTemplate = 92 | typeof customTemplate !== "undefined" || 93 | typeof templateBody !== "undefined"; 94 | const customBody = (customTemplate && customTemplate.body) || templateBody; 95 | const customStyles = 96 | (customTemplate && customTemplate.styles) || templateStyles; 97 | 98 | const { html, body, styles } = usingCustomTemplate 99 | ? compileTemplate({ 100 | body: customBody, 101 | styles: customStyles, 102 | templateParams, 103 | size: _size, 104 | compileArgs: { testMode, ...compileArgs } 105 | }) 106 | : createTemplate({ 107 | templateParams, 108 | size: _size, 109 | compileArgs: { testMode, ...compileArgs } 110 | }); 111 | 112 | let screenshot; 113 | 114 | const validPreviewSizes = ["facebook", "twitter"]; 115 | 116 | if (!preview || validPreviewSizes.indexOf(size) === -1) { 117 | // Wait for fonts to load (via networkidle) 118 | await page.setContent(html, { waitUntil: "networkidle0" }); 119 | 120 | // Get root of page 121 | const pageFrame = page.mainFrame(); 122 | const rootHandle = await pageFrame.$("body"); 123 | 124 | // Take screenshot 125 | screenshot = await rootHandle.screenshot({ 126 | path: output, 127 | omitBackground: true, 128 | type, 129 | quality: type === "jpeg" ? jpegQuality : undefined 130 | }); 131 | } else { 132 | await page.setViewport({ 133 | // Just needs to be larger than preview, so we can deal with any environmental rendering nuances and crop cleanly 134 | width: 1250, 135 | height: 1250 136 | }); 137 | 138 | const previewHtml = compilePreview({ body, styles, compileArgs }); 139 | await page.setContent(previewHtml, { waitUntil: "networkidle0" }); 140 | 141 | // Get root of page 142 | const pageFrame = page.mainFrame(); 143 | const rootHandle = await pageFrame.$("body > *"); 144 | 145 | // Take screenshot 146 | screenshot = await rootHandle.screenshot({ 147 | path: output, 148 | omitBackground: true, 149 | type, 150 | quality: type === "jpeg" ? jpegQuality : undefined 151 | }); 152 | } 153 | 154 | if (!userBrowser) { 155 | browser.close(); 156 | } else { 157 | page.close(); 158 | } 159 | 160 | return screenshot; 161 | }; 162 | -------------------------------------------------------------------------------- /src/templates/article.js: -------------------------------------------------------------------------------- 1 | import { compileImageTemplate } from "../helpers"; 2 | 3 | export default ({ templateParams, ...params }) => 4 | compileImageTemplate({ 5 | ...params, 6 | body: ` 7 |
8 |
9 | 14 |
15 | {{#if eyebrow}}
{{eyebrow}}
{{/if}} 16 |

{{title}}

17 | {{#if subtitle}}

{{subtitle}}

{{/if}} 18 |
19 |
20 | {{#if watermark}} 21 |
22 | {{watermark}} 23 |
24 | {{/if}} 25 |
26 |
27 |
28 | `, 29 | styles: ` 30 | .Content { 31 | display: flex; 32 | flex-direction: column; 33 | height: 100%; 34 | } 35 | 36 | .Content-inner { 37 | display: flex; 38 | flex-direction: column; 39 | height: 100%; 40 | padding: 64px; 41 | padding-top: 48px; 42 | } 43 | 44 | .Eyebrow { 45 | font-size: 24px; 46 | font-weight: 600; 47 | margin-bottom: 8px; 48 | } 49 | 50 | .Content-watermark { 51 | display: flex; 52 | height: 100%; 53 | } 54 | 55 | .Content-watermarkInner { 56 | border-top: 1px solid lightgrey; 57 | font-size: 32px; 58 | align-self: flex-end; 59 | opacity: 0.8; 60 | width: 100%; 61 | padding-top: 32px; 62 | } 63 | 64 | .logo { 65 | height: 100%; 66 | } 67 | 68 | .logo img { 69 | height: 100px; 70 | padding-bottom: 16px; 71 | } 72 | 73 | h1, 74 | h2 { 75 | margin: 0; 76 | padding: 0; 77 | font-weight: 400; 78 | line-height: 1.2; 79 | } 80 | 81 | h1 { 82 | font-size: 96px; 83 | font-weight: 700; 84 | margin-bottom: 8px; 85 | display: -webkit-box; 86 | -webkit-line-clamp: 2; 87 | -webkit-box-orient: vertical; 88 | overflow: hidden; 89 | } 90 | 91 | h2 { 92 | font-size: 40px; 93 | display: -webkit-box; 94 | -webkit-line-clamp: 1; 95 | -webkit-box-orient: vertical; 96 | overflow: hidden; 97 | } 98 | `, 99 | templateParams 100 | }); 101 | -------------------------------------------------------------------------------- /src/templates/basic.js: -------------------------------------------------------------------------------- 1 | import { compileImageTemplate } from "../helpers"; 2 | 3 | export default ({ 4 | templateParams: { fontWeight = "800", fontSize = "80px", ...templateParams }, 5 | ...params 6 | }) => 7 | compileImageTemplate({ 8 | ...params, 9 | body: ` 10 |
13 | 18 |

{{title}}

19 |
20 | {{#if includeWatermark}} 21 |
24 | {{watermark}} 25 |
26 | {{/if}} 27 |
28 |
29 | `, 30 | styles: ` 31 | .body { 32 | align-items: center; 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | padding: 48px 152px; 37 | height: 100%; 38 | } 39 | 40 | h1 { 41 | display: flex; 42 | align-items: center; 43 | flex-grow: 1; 44 | font-size: ${fontSize}; 45 | font-weight: ${fontWeight}; 46 | text-align: center; 47 | margin: 0; 48 | } 49 | 50 | .logo { 51 | height: 100px; 52 | width: 100%; 53 | margin-bottom: 24px; 54 | max-width: 150px; 55 | } 56 | .logo-inner { 57 | align-items: center; 58 | display: flex; 59 | justify-content: center; 60 | font-size: 48px; 61 | } 62 | 63 | .logo-inner img{ 64 | height: 100px; 65 | } 66 | 67 | .logo-text { 68 | font-weight: 600; 69 | margin-left: 24px; 70 | whitespace: nowrap; 71 | } 72 | .watermark { 73 | margin-top: 40px; 74 | height: 80px; 75 | opacity: 0.7; 76 | } 77 | .watermark-inner { 78 | font-size: 32px; 79 | font-weight: 500; 80 | } 81 | `, 82 | templateParams 83 | }); 84 | -------------------------------------------------------------------------------- /src/templates/fiftyfifty.js: -------------------------------------------------------------------------------- 1 | import { compileTemplate } from "../helpers"; 2 | 3 | export default ({ templateParams, ...params }) => 4 | compileTemplate({ 5 | ...params, 6 | imageSize: { height: 700, width: 700 }, // Square - enough for standard width of 610px + 10% 7 | templateParams, 8 | body: ` 9 | 27 | `, 28 | styles: ` 29 | :root { 30 | --split-width: 7.5%; 31 | --split-absolute-width: calc((100% + var(--split-width)) / 2); 32 | } 33 | 34 | .social { 35 | display: flex; 36 | height: 100%; 37 | } 38 | 39 | .social--diagonal-split .content { 40 | clip-path: polygon(0 0, 86% 0%, 100% 100%, 0% 100%) 41 | } 42 | 43 | .social--diagonal-split .content-inner { 44 | padding-right: 16%; 45 | } 46 | 47 | .social--diagonal-reverse-split .content { 48 | clip-path: polygon(0 0, 100% 0%, 86% 100%, 0% 100%) 49 | } 50 | 51 | .social--diagonal-reverse-split .content-inner { 52 | padding-right: 16%; 53 | } 54 | 55 | .content { 56 | background: white; 57 | display: flex; 58 | flex-direction: column; 59 | position:absolute; 60 | width: 50%; 61 | height: 100%; 62 | } 63 | 64 | .social--diagonal-split .content, .social--diagonal-reverse-split .content { 65 | width: var(--split-absolute-width); 66 | } 67 | 68 | .content-inner { 69 | flex-grow: 1; 70 | padding: 40px; 71 | padding-bottom: 20px; 72 | overflow: hidden; 73 | } 74 | 75 | .logo { 76 | height: 72px; 77 | margin-bottom: 8px; 78 | } 79 | 80 | .image { 81 | background: lightgray; 82 | background-position: center; 83 | background-repeat: no-repeat; 84 | background-size: cover; 85 | height: 100%; 86 | width: 50%; 87 | margin-left: 50%; 88 | 89 | } 90 | 91 | .social--diagonal-split .image, .social--diagonal-reverse-split .image { 92 | width: var(--split-absolute-width); 93 | margin-left: calc(100% - var(--split-absolute-width)); 94 | } 95 | 96 | 97 | .truncate { 98 | overflow: hidden; 99 | display: -webkit-box; 100 | -webkit-box-orient: vertical; 101 | } 102 | 103 | 104 | h1 { 105 | font-size: 64px; 106 | margin-top: 16px; 107 | margin-bottom: 16px; 108 | font-weight: 800; 109 | -webkit-line-clamp: 6; 110 | } 111 | 112 | .social--has-logo h1 { 113 | -webkit-line-clamp: 5; 114 | } 115 | 116 | .social--has-subtitle h1 { 117 | -webkit-line-clamp: 3; 118 | } 119 | 120 | .social--has-subtitle.social--has-logo h1 { 121 | font-size: 56px; 122 | -webkit-line-clamp: 3; 123 | } 124 | 125 | h2 { 126 | font-size: 40px; 127 | -webkit-line-clamp: 4; 128 | margin-top: 16px; 129 | margin-bottom: 16px; 130 | font-weight: 500; 131 | } 132 | 133 | .social--has-logo h2 { 134 | -webkit-line-clamp: 3; 135 | } 136 | 137 | .footer { 138 | opacity: 0.75; 139 | font-size: 32px; 140 | font-weight: 800; 141 | padding: 40px; 142 | padding-top: 0px; 143 | } 144 | ` 145 | }); 146 | -------------------------------------------------------------------------------- /src/templates/index.js: -------------------------------------------------------------------------------- 1 | import article from "./article"; 2 | import basic from "./basic"; 3 | import fiftyfifty from "./fiftyfifty"; 4 | 5 | export default { article, basic, fiftyfifty }; 6 | --------------------------------------------------------------------------------