├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.config.ts ├── package.json ├── sanity.json ├── src ├── Editor.tsx ├── EditorField.tsx ├── Image.tsx ├── LayoutsPicker.tsx ├── app.tsx ├── defaultLayout.tsx ├── imageBuilder.ts ├── index.ts ├── types.ts └── useEditorLogic.ts ├── tsconfig.dist.json ├── tsconfig.json ├── tsconfig.settings.json └── v2-incompatible.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | .eslintrc.js 3 | commitlint.config.js 4 | dist 5 | lint-staged.config.js 6 | package.config.ts 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": [ 8 | "sanity", 9 | "sanity/typescript", 10 | "sanity/react", 11 | "plugin:react-hooks/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:react/jsx-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | 20 | publish-npm: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm ci 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | # npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | dist 61 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Katerina Baliasnikova 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 | # Sanity Plugin: Generate OG Image 2 | 3 | > This is a **Sanity Studio v3** plugin for generating OG images. 4 | 5 | Based on [sanity-plugin-asset-source-ogimage](https://www.npmjs.com/package/sanity-plugin-asset-source-ogimage) for Sanity Studio v2 6 | 7 | This Sanity plugin provides a tool to generate Open Graph (OG) images for your Sanity documents. It's designed to be flexible, allowing you to define custom layouts for the generated images. 8 | 9 | ## Features 10 | 11 | - **Custom Layouts**: Craft your own layouts for the images. 12 | - **Live Preview**: Witness changes in real-time as you tweak the layout and content. 13 | - **Download & Generate**: Create the image and either download it instantly or integrate it within your Sanity documents. 14 | 15 | ## Installation 16 | 17 | 🚨 You need `@sanity 3.5.0` or greater and `react 18.0.0` or greater 18 | 19 | ```sh 20 | npm install @catherineriver/sanity-plugin-generate-ogimage 21 | ``` 22 | 23 | ## Basic Usage 24 | 25 | ### As a studio tool 26 | 27 | Use it in `sanity.config.ts` (or .js): 28 | 29 | ```ts 30 | import {defineConfig} from 'sanity' 31 | import {generateOGImage} from 'sanity-plugin-generate-ogimage' 32 | 33 | export default defineConfig({ 34 | //... 35 | plugins: [generateOGImage()], 36 | }) 37 | ``` 38 | 39 | ### As custom source in image field 40 | 41 | Use it as [source on a single type](https://www.sanity.io/docs/custom-asset-sources#e2077d7f8ae2) 42 | 43 | ```jsx 44 | { 45 | name: 'ogImage', 46 | title: 'OG image', 47 | type: 'image', 48 | options: { 49 | sources: [ 50 | { 51 | name: 'sharing-image', 52 | title: 'Generate Image', 53 | component: (props) => ( 54 | 55 | ), 56 | }, 57 | ], 58 | } 59 | } 60 | ``` 61 | 62 | ## Advanced Usage with Custom Layouts 63 | 64 | You can define custom layouts for your generated images. A layout is essentially a React component that receives certain props and renders the desired output. 65 | 66 | Here's a basic structure of a layout: 67 | 68 | ```jsx 69 | import React from "react"; 70 | import { LayoutData } from "sanity-plugin-generate-ogimage/types"; 71 | 72 | const MyCustomLayout: React.FC = ({ title, subtitle, logo }) => { 73 | // Your rendering logic here 74 | }; 75 | 76 | export default MyCustomLayout; 77 | ``` 78 | 79 | To use your custom layouts, modify your sanity.config.ts (or .js) as follows: 80 | 81 | ```jsx 82 | import {defineConfig} from 'sanity' 83 | import {generateOGImage} from 'sanity-plugin-generate-ogimage' 84 | import MyCustomLayout from './path-to-your-layout' 85 | 86 | export default defineConfig({ 87 | // ... other config 88 | plugins: [generateOGImage({layouts: [MyCustomLayout]})], 89 | }) 90 | ``` 91 | 92 | ## Components 93 | 94 | Here's a brief overview of the main components in the repository: 95 | 96 | - `Editor`: The main editor component where users can select a layout, modify content, and generate the image. 97 | - `Image`: A utility component to display Sanity images. 98 | - `LayoutsPicker`: Allows users to pick from multiple available layouts. 99 | - `EditorField`: Represents individual fields in the editor, like text inputs, switches, etc. 100 | - `useEditorLogic`: A custom hook that encapsulates the logic for generating and downloading the image. 101 | - `imageBuilder`: A utility to build image URLs using Sanity's image URL builder. 102 | 103 | ## License 104 | 105 | [MIT](LICENSE) © Katerina Baliasnikova 106 | 107 | ## Develop & test 108 | 109 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit) 110 | with default configuration for build & watch scripts. 111 | 112 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio) 113 | on how to run this plugin with hotreload in the studio. 114 | 115 | Buy Me A Coffee 116 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | legacyExports: true, 5 | dist: 'dist', 6 | tsconfig: 'tsconfig.dist.json', 7 | 8 | // Remove this block to enable strict export validation 9 | extract: { 10 | rules: { 11 | 'ae-forgotten-export': 'off', 12 | 'ae-incompatible-release-tags': 'off', 13 | 'ae-internal-missing-underscore': 'off', 14 | 'ae-missing-release-tag': 'off', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@catherineriver/sanity-plugin-generate-ogimage", 3 | "version": "1.0.5", 4 | "description": "This is a Sanity Studio v3 plugin.", 5 | "keywords": [ 6 | "sanity", 7 | "sanity-plugin" 8 | ], 9 | "homepage": "https://github.com/catherineriver/sanity-plugin-generate-ogimage#readme", 10 | "bugs": { 11 | "url": "https://github.com/catherineriver/sanity-plugin-generate-ogimage/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+ssh://git@github.com/catherineriver/sanity-plugin-generate-ogimage.git" 16 | }, 17 | "license": "MIT", 18 | "author": "Katerina Baliasnikova ", 19 | "exports": { 20 | ".": { 21 | "source": "./src/index.ts", 22 | "require": "./dist/index.js", 23 | "import": "./dist/index.esm.mjs", 24 | "default": "./dist/index.esm.mjs" 25 | }, 26 | "./package.json": "./package.json" 27 | }, 28 | "main": "./dist/index.js", 29 | "module": "./dist/index.esm.js", 30 | "files": [ 31 | "dist", 32 | "sanity.json", 33 | "src", 34 | "v2-incompatible.js" 35 | ], 36 | "sideEffects": true, 37 | "scripts": { 38 | "build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict", 39 | "clean": "rimraf dist", 40 | "format": "prettier --write --cache --ignore-unknown .", 41 | "link-watch": "plugin-kit link-watch", 42 | "lint": "eslint .", 43 | "prepublishOnly": "run-s build", 44 | "watch": "pkg-utils watch --strict" 45 | }, 46 | "dependencies": { 47 | "@sanity/incompatible-plugin": "^1.0.4", 48 | "downloadjs": "^1.4.7", 49 | "html-to-image": "^1.11.11", 50 | "npm-check-updates": "^16.14.18", 51 | "stylis": "^4.3.1", 52 | "vite-plugin-node-polyfills": "^0.21.0" 53 | }, 54 | "devDependencies": { 55 | "@sanity/pkg-utils": "^6.4.1", 56 | "@sanity/plugin-kit": "^3.1.12", 57 | "@types/downloadjs": "^1.4.6", 58 | "@types/react": "^18.2.75", 59 | "@typescript-eslint/eslint-plugin": "^7.6.0", 60 | "@typescript-eslint/parser": "^7.6.0", 61 | "eslint": "^8.56.0", 62 | "eslint-config-prettier": "^9.1.0", 63 | "eslint-config-sanity": "^7.1.2", 64 | "eslint-plugin-prettier": "^5.1.3", 65 | "eslint-plugin-react": "^7.34.1", 66 | "eslint-plugin-react-hooks": "^4.6.0", 67 | "npm-run-all": "^4.1.5", 68 | "prettier": "^3.2.5", 69 | "prettier-plugin-packagejson": "^2.4.14", 70 | "react": "^18.2.0", 71 | "react-dom": "^18.2.0", 72 | "react-is": "^18.2.0", 73 | "rimraf": "^5.0.5", 74 | "sanity": "^3.37.2", 75 | "styled-components": "^6.1.8", 76 | "typescript": "^5.4.5" 77 | }, 78 | "peerDependencies": { 79 | "react": "^18", 80 | "sanity": "^3" 81 | }, 82 | "engines": { 83 | "node": ">=14" 84 | }, 85 | "sanityPlugin": { 86 | "verifyPackage": { 87 | "module": false 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/Editor.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Card, Flex, Inline, Spinner, Stack, Text} from '@sanity/ui' 2 | import {CloseIcon, GenerateIcon, DownloadIcon} from '@sanity/icons' 3 | import {DialogLabels, EditorLayout, SanityDocument} from './types' 4 | import * as React from 'react' 5 | 6 | import EditorField from './EditorField' 7 | import LayoutsPicker from './LayoutsPicker' 8 | import useEditorLogic from './useEditorLogic' 9 | 10 | export interface EditorProps { 11 | layouts: EditorLayout[] 12 | onSelect?: (...props: any) => void 13 | onClose?: () => void 14 | document: SanityDocument 15 | dialog?: DialogLabels 16 | scheme?: 'dark' | 'light' 17 | } 18 | 19 | const DEFAULT_DIMENSIONS = { 20 | width: 1200, 21 | height: 630, 22 | } 23 | 24 | const Editor: React.FC = (props) => { 25 | const { 26 | activeLayout, 27 | setActiveLayout, 28 | generateImage, 29 | downloadImage, 30 | disabled, 31 | captureRef, 32 | data, 33 | setData, 34 | } = useEditorLogic(props) 35 | const {dialog, onClose, layouts, scheme} = props 36 | const LayoutComponent = activeLayout.component as any 37 | const fields = activeLayout.fields || [] 38 | const width = activeLayout.dimensions?.width || DEFAULT_DIMENSIONS.width 39 | const height = activeLayout.dimensions?.height || DEFAULT_DIMENSIONS.height 40 | 41 | return ( 42 | 49 | 57 | 58 | 59 | 60 | {dialog?.title || 'Create image'} 61 | 62 | {onClose && ( 63 |