├── .gitignore ├── README.md ├── demo ├── gatsby-config.js ├── gatsby-node.js ├── package-lock.json ├── package.json └── src │ ├── assets │ └── social-card-template.jpg │ ├── pages │ └── index.js │ └── templates │ └── dynamic.js ├── gatsby-node.js ├── index.js ├── netlify.toml ├── package-lock.json ├── package.json └── src └── components └── gatsby-social-image.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | public 4 | .cache 5 | 6 | # Local Netlify folder 7 | .netlify -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gatsby-plugin-cloudinary-social-cards 2 | 3 | Add social sharing cards to your Gatsby sites using [Cloudinary](https://jason.af/cloudinary)! 4 | 5 | This plugin will: 6 | 7 | 1. Look for a template image in your repo 8 | 2. Upload that template to your Cloudinary account 9 | 3. Automatically create social media sharing images with the title (and optional tagline) of your post 10 | 11 | ## Installation 12 | 13 | ```bash 14 | # install the plugin and its peer dependencies 15 | npm install gatsby-plugin-cloudinary-social-cards react-helmet gatsby-plugin-react-helmet 16 | ``` 17 | 18 | Create a new API key and secret in your [Cloudinary console][cloudinary-console] and set the following environment variables (create a file called `.env` locally): 19 | 20 | ``` 21 | CLOUDINARY_API_KEY= 22 | CLOUDINARY_API_SECRET= 23 | ``` 24 | 25 | Next, configure the plugin in your `gatsby-config.js`: 26 | 27 | ```js 28 | require('dotenv').config(); 29 | 30 | module.exports = { 31 | plugins: [ 32 | 'gatsby-plugin-react-helmet', 33 | { 34 | resolve: 'gatsby-plugin-cloudinary-social-cards', 35 | options: { 36 | cloudName: 'jlengstorf', 37 | apiKey: process.env.CLOUDINARY_API_KEY, 38 | apiSecret: process.env.CLOUDINARY_API_SECRET, 39 | imageTemplate: 'src/assets/social-card-template.jpg', 40 | }, 41 | }, 42 | ], 43 | }; 44 | ``` 45 | 46 | ### Configuration 47 | 48 | Option | Required | Default | Description 49 | ------ | -------- | ------- | ----------- 50 | `cloudName` | `true` | | Your Cloudinary cloud name (usually your account name). 51 | `apiKey` | `true` | | Your Cloudinary API key ([get one here][cloudinary-console]). 52 | `apiSecret` | `true` | | Your Cloudinary API secret ([get one here][cloudinary-console]). 53 | `imageTemplate` | `true` | | Path to the social card image template. This should be a local file in your repo. 54 | `uploadFolder` | | `null` | Optional subfolder where the template should be uploaded in your Cloudinary account. 55 | `imageOptions` | | `{}` | Additional settings for your template image. 56 | 57 | If you need a template, I wrote a post on [creating a social sharing template image](https://www.learnwithjason.dev/blog/design-social-sharing-card/). 58 | 59 | #### `imageOptions` 60 | 61 | The options passed in `imageOptions` can be any of the [available options for the `@jlengstorf/get-share-image` utility](https://www.npmjs.com/package/@jlengstorf/get-share-image#options). 62 | 63 | > **NOTE:** The `cloudName`, `apiKey`, `apiSecret`, `title`, `tagline`, and `imagePublicID` settings will be overridden by the settings in the plugin. 64 | 65 | ## Usage 66 | 67 | There are two ways to use this plugin: 68 | 69 | 1. Import the `GatsbySocialImage` component 70 | 2. Use the `getSocialCard` GraphQL query 71 | 72 | ### Import the `GatsbySocialImage` component 73 | 74 | In most cases, the easiest solution is to use the built-in React component: 75 | 76 | ```jsx 77 | import React from 'react'; 78 | import { Helmet } from 'react-helmet'; 79 | import { GatsbySocialImage } from 'gatsby-plugin-cloudinary-social-cards'; 80 | 81 | export default () => { 82 | const title = 'This Page Boops'; 83 | const tagline = 'I hope you like corgis.'; 84 | 85 | return ( 86 | <> 87 | 88 | {title} 89 | 90 | 91 |

{title}

92 |

{tagline}

93 | 94 | ); 95 | }; 96 | ``` 97 | 98 | The component adds an `og:image` meta tag to the page: 99 | 100 | ![Rendered Gatsby page with the GatsbySocialImage component.](https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto,w_1400/v1593135024/gatsby-social-cards/react-component-demo.png) 101 | 102 | The generated social card looks like this: 103 | 104 | ![Card with the Learn With Jason logo that says, “This page boops. I hope you like corgis.”](https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:000000,g_south_west,x_480,y_254,l_text:arial_64:This%20Page%20Boops/w_760,c_fit,co_rgb:000000,g_north_west,x_480,y_445,l_text:arial_48:I%20hope%20you%20like%20corgis./gatsby-social-cards/social-card-template) 105 | 106 | ### Use the `getSocialCard` GraphQL query 107 | 108 | You can also query for the image URL, which is an alternative for dynamically generated pages. 109 | 110 | In `gatsby-node.js`: 111 | 112 | ```js 113 | const pages = [ 114 | { 115 | path: '/test1', 116 | title: 'The First Test Page', 117 | tagline: 'This page needs a snappy tagline.', 118 | }, 119 | { 120 | path: '/test2', 121 | title: 'Another Test Page', 122 | }, 123 | ]; 124 | 125 | exports.createPages = ({ actions }) => { 126 | pages.forEach((page) => { 127 | actions.createPage({ 128 | path: page.path, 129 | component: require.resolve('./src/templates/dynamic.js'), 130 | context: { 131 | title: page.title, 132 | tagline: page.tagline, 133 | }, 134 | }); 135 | }); 136 | }; 137 | ``` 138 | 139 | in `src/templates/dynamic.js`: 140 | 141 | ```js 142 | import React from 'react'; 143 | import { graphql } from 'gatsby'; 144 | import { Helmet } from 'react-helmet'; 145 | 146 | export const query = graphql` 147 | query($title: String!, $tagline: String) { 148 | getSocialCard(title: $title, tagline: $tagline) { 149 | url 150 | } 151 | } 152 | `; 153 | 154 | export default ({ data, pageContext: { title } }) => { 155 | return ( 156 | <> 157 | 158 | {title} 159 | 160 | 161 |

{title}

162 | 163 | ); 164 | }; 165 | ``` 166 | 167 | The page will have the meta tag added like so: 168 | 169 | ![Rendered Gatsby page with the GatsbySocialImage component.](https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto,w_1400/v1593134498/gatsby-social-cards/query-demo.png) 170 | 171 | The generated card looks like this: 172 | 173 | ![Card with the Learn With Jason logo that says, “The First Test Page. This page needs a snappy tagline.”](https://res.cloudinary.com/jlengstorf/image/upload/w_1280,h_669,c_fill,q_auto,f_auto/w_760,c_fit,co_rgb:000000,g_south_west,x_480,y_254,l_text:arial_64:The%20First%20Test%20Page/w_760,c_fit,co_rgb:000000,g_north_west,x_480,y_445,l_text:arial_48:This%20page%20needs%20a%20snappy%20tagline./gatsby-social-cards/social-card-template) 174 | 175 | [cloudinary-console]: https://cloudinary.com/console?ap=lwj -------------------------------------------------------------------------------- /demo/gatsby-config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | plugins: [ 5 | 'gatsby-plugin-react-helmet', 6 | { 7 | resolve: 'gatsby-plugin-cloudinary-social-cards', 8 | options: { 9 | cloudName: 'jlengstorf', 10 | apiKey: process.env.CLOUDINARY_API_KEY, 11 | apiSecret: process.env.CLOUDINARY_API_SECRET, 12 | imageTemplate: 'src/assets/social-card-template.jpg', 13 | uploadFolder: 'gatsby-social-cards', 14 | imageOptions: { 15 | // for all available options, see: 16 | // https://www.npmjs.com/package/@jlengstorf/get-share-image#options 17 | titleExtraConfig: '_line_spacing_-10', 18 | textColor: '232129', 19 | // custom fonts are possible! see this post for details: 20 | // https://www.learnwithjason.dev/blog/upload-custom-font-cloudinary-media-library/ 21 | titleFont: 'lwj-title.otf', 22 | taglineFont: 'lwj-tagline.otf', 23 | }, 24 | }, 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /demo/gatsby-node.js: -------------------------------------------------------------------------------- 1 | // dummy page data for demo purposes 2 | const pages = [ 3 | { 4 | path: '/test1', 5 | title: 'The First Test Page', 6 | tagline: 'This page needs a snappy tagline.', 7 | }, 8 | { 9 | path: '/test2', 10 | title: 'Another Test Page', 11 | }, 12 | ]; 13 | 14 | exports.createPages = ({ actions }) => { 15 | pages.forEach((page) => { 16 | actions.createPage({ 17 | path: page.path, 18 | component: require.resolve('./src/templates/dynamic.js'), 19 | context: { 20 | title: page.title, 21 | tagline: page.tagline, 22 | }, 23 | }); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "gatsby-config.js", 6 | "scripts": { 7 | "build": "gatsby build", 8 | "develop": "gatsby develop" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "gatsby": "^2.23.11", 15 | "gatsby-plugin-cloudinary-social-cards": "^1.0.1", 16 | "gatsby-plugin-react-helmet": "^3.3.6", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-helmet": "^6.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/src/assets/social-card-template.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlengstorf/gatsby-plugin-cloudinary-social-cards/d12670e3082c86e78f0b5b1f2282552b286ae1bc/demo/src/assets/social-card-template.jpg -------------------------------------------------------------------------------- /demo/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import { GatsbySocialImage } from 'gatsby-plugin-cloudinary-social-cards'; 4 | 5 | export default () => { 6 | const title = 'This Page Boops'; 7 | const tagline = 'I hope you like corgis.'; 8 | 9 | return ( 10 | <> 11 | 12 | {title} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

{title}

25 |

{tagline}

26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /demo/src/templates/dynamic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'gatsby'; 3 | import { Helmet } from 'react-helmet'; 4 | 5 | export const query = graphql` 6 | query($title: String!, $tagline: String) { 7 | getSocialCard(title: $title, tagline: $tagline) { 8 | url 9 | } 10 | } 11 | `; 12 | 13 | export default ({ data, pageContext: { title, tagline = '' } }) => { 14 | return ( 15 | <> 16 | 17 | {title} 18 | 19 | {/* use the query result in image meta tags */} 20 | 21 | 22 | 23 | 24 | {/* other social sharing tags for SEO and social previews */} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

{title}

36 |

{tagline}

37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | const getShareImage = require('@jlengstorf/get-share-image').default; 3 | 4 | exports.sourceNodes = async ({ actions, createNodeId }, options) => { 5 | const { 6 | cloudName = process.env.CLOUDINARY_CLOUD_NAME, 7 | apiKey = process.env.CLOUDINARY_API_KEY, 8 | apiSecret = process.env.CLOUDINARY_API_SECRET, 9 | imageTemplate, 10 | uploadFolder, 11 | imageOptions = {}, 12 | } = options; 13 | 14 | cloudinary.config({ 15 | cloud_name: cloudName, 16 | api_key: apiKey, 17 | api_secret: apiSecret, 18 | }); 19 | 20 | const { 21 | asset_id, 22 | public_id, 23 | version, 24 | signature, 25 | secure_url, 26 | access_mode, 27 | } = await cloudinary.uploader.upload(imageTemplate, { 28 | folder: uploadFolder, 29 | use_filename: true, 30 | unique_filename: false, 31 | overwrite: false, 32 | }); 33 | 34 | if (access_mode !== 'public') { 35 | console.warn( 36 | 'Your Cloudinary social media card is not publicly accessible. This will probably cause great sadness.', 37 | ); 38 | } 39 | 40 | actions.createNode({ 41 | id: createNodeId(`CloudinarySocialCardTemplate-${asset_id}`), 42 | templateUrl: secure_url, 43 | publicId: public_id, 44 | version, 45 | cloudName, 46 | imageOptions: JSON.stringify(imageOptions), 47 | parent: null, 48 | children: [], 49 | internal: { 50 | type: 'CloudinarySocialCardTemplate', 51 | contentDigest: signature, 52 | }, 53 | }); 54 | }; 55 | 56 | exports.createSchemaCustomization = ({ actions }) => { 57 | actions.createTypes(` 58 | type CloudinarySocialCard implements Node { 59 | url: String! 60 | } 61 | `); 62 | }; 63 | 64 | exports.createResolvers = ({ createResolvers }, { imageOptions = {} }) => { 65 | createResolvers({ 66 | Query: { 67 | getSocialCard: { 68 | type: 'CloudinarySocialCard', 69 | args: { 70 | title: 'String!', 71 | tagline: 'String', 72 | }, 73 | resolve: async (_source, args, context) => { 74 | const [template] = context.nodeModel.getAllNodes({ 75 | type: 'CloudinarySocialCardTemplate', 76 | }); 77 | 78 | const img = getShareImage({ 79 | ...imageOptions, 80 | title: args.title, 81 | tagline: args.tagline, 82 | cloudName: template.cloudName, 83 | imagePublicID: template.publicId, 84 | }); 85 | 86 | return { url: img }; 87 | }, 88 | }, 89 | }, 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { GatsbySocialImage } from './src/components/gatsby-social-image'; 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "demo" 3 | command = "npm run build" 4 | publish = "public" 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-plugin-cloudinary-social-cards", 3 | "version": "1.0.2", 4 | "description": "Automatically generate social sharing images for your Gatsby pages using Cloudinary.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jlengstorf/gatsby-plugin-cloudinary-social-cards.git" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "gatsby", 15 | "gatsby-plugin", 16 | "cloudinary" 17 | ], 18 | "files": [ 19 | "index.js", 20 | "gatsby-node.js", 21 | "src/*" 22 | ], 23 | "author": "Jason Lengstorf ", 24 | "license": "ISC", 25 | "dependencies": { 26 | "@jlengstorf/get-share-image": "^0.7.1", 27 | "cloudinary": "^1.22.0" 28 | }, 29 | "peerDependencies": { 30 | "gatsby": "^2.23.11", 31 | "gatsby-plugin-react-helmet": "^3.3.6", 32 | "react": "^16.13.1", 33 | "react-dom": "^16.13.1", 34 | "react-helmet": "^6.1.0" 35 | }, 36 | "devDependencies": { 37 | "gatsby": "^2.23.11", 38 | "react": "^16.13.1", 39 | "react-helmet": "^6.1.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/gatsby-social-image.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql, useStaticQuery } from 'gatsby'; 3 | import { Helmet } from 'react-helmet'; 4 | import getShareImage from '@jlengstorf/get-share-image'; 5 | 6 | export const GatsbySocialImage = ({ title, tagline, options = {} }) => { 7 | const { template } = useStaticQuery(graphql` 8 | query { 9 | template: cloudinarySocialCardTemplate { 10 | publicId 11 | version 12 | cloudName 13 | imageOptions 14 | } 15 | } 16 | `); 17 | 18 | const templateOptions = JSON.parse(template.imageOptions) || {}; 19 | 20 | const url = getShareImage({ 21 | title, 22 | tagline, 23 | imagePublicID: template.publicId, 24 | cloudName: template.cloudName, 25 | ...templateOptions, 26 | ...options, 27 | }); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | --------------------------------------------------------------------------------