├── .babelrc ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ossar-analysis.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SUPPORT.md ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── components │ └── MetaHeadEmbed.tsx ├── index.ts ├── types.tsx ├── utils.ts └── utils │ ├── copyToClipboard.ts │ ├── getFacebookUrl.ts │ ├── getLinkedinUrl.ts │ ├── getShareUrl.ts │ ├── getTwitterUrl.ts │ ├── getWhatsAppUrl.ts │ └── objectToUrlParams.ts ├── tsconfig.json └── tsconfig.test.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ] 6 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @balraj-johal @JPedersen -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ossar-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates a collection of open source static analysis tools 2 | # with GitHub code scanning. For documentation, or to provide feedback, visit 3 | # https://github.com/github/ossar-action 4 | name: OSSAR 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | # The branches below must be a subset of the branches above 11 | branches: [master] 12 | schedule: 13 | - cron: "17 20 * * 3" 14 | 15 | jobs: 16 | OSSAR-Scan: 17 | # OSSAR runs on windows-latest. 18 | # ubuntu-latest and macos-latest support coming soon 19 | runs-on: windows-latest 20 | 21 | permissions: 22 | contents: read 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | 28 | # Ensure a compatible version of dotnet is installed. 29 | # The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201. 30 | # A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action. 31 | # GitHub hosted runners already have a compatible version of dotnet installed and this step may be skipped. 32 | # For self-hosted runners, ensure dotnet version 3.1.201 or later is installed by including this action: 33 | # - name: Install .NET 34 | # uses: actions/setup-dotnet@v1 35 | # with: 36 | # dotnet-version: '3.1.x' 37 | # Run open source static analysis tools 38 | - name: Run OSSAR 39 | uses: github/ossar-action@v1 40 | id: ossar 41 | 42 | # Upload results to the Security tab 43 | - name: Upload OSSAR results 44 | uses: github/codeql-action/upload-sarif@v1 45 | with: 46 | sarif_file: ${{ steps.ossar.outputs.sarifFile }} 47 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | contents: read 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18.x, 20.x, 22.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: npm install, build 28 | run: | 29 | npm ci 30 | npm run build --if-present 31 | npm run lint 32 | env: 33 | CI: true 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x, 20.x, 22.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: npm install, build 27 | run: | 28 | npm ci 29 | npm run build --if-present 30 | npm run lint 31 | env: 32 | CI: true 33 | 34 | publish-npm: 35 | needs: test 36 | runs-on: ubuntu-latest 37 | 38 | permissions: 39 | contents: read 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions/setup-node@v1 44 | with: 45 | node-version: 22 46 | registry-url: https://registry.npmjs.org/ 47 | - run: npm ci 48 | - run: npm publish --access=public 49 | env: 50 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | dist 5 | .cache 6 | lib 7 | !src/* 8 | !test/* 9 | .idea 10 | .DS_Store 11 | .vscode/settings.json 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.14.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | public 5 | tmp 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at developers@phntms.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | I'm really happy that you're interested in helping out with this little project. 4 | 5 | As this is very early days for the project there's not a lot in the way of 6 | resources, but please check out the [documentation](./README.md), and also the 7 | [list of issues](https://github.com/phantomstudios/react-share/issues). 8 | 9 | Please submit an issue if you need help with anything. 10 | 11 | We have a [code of conduct](./CODE_OF_CONDUCT.md) so please make sure you follow 12 | it. 13 | 14 | ## Submitting changes 15 | 16 | Please send a 17 | [GitHub Pull Request to react-share](https://github.com/phantomstudios/react-share/pull/new/master) 18 | with a clear list of what you've done (read more about 19 | [pull requests](https://help.github.com/en/articles/about-pull-requests)). When you send a pull 20 | request, please make sure you've covered off all the points in the template. 21 | 22 | Make sure you've read about our workflow (below); in essence make sure each Pull 23 | Request is atomic but don't worry too much about the commits themselves as we use 24 | squash-and-merge. 25 | 26 | ## Our workflow 27 | 28 | We use [GitHub flow](https://guides.github.com/introduction/flow/); it's a lot 29 | like git-flow but simpler and more forgiving. We use the `squash and merge` 30 | strategy to merge Pull Requests. 31 | 32 | In effect this means: 33 | 34 | - Don't worry about individual commits. They will be preserved, but not on the 35 | main `master` branch history, so feel free to commit early and often, using 36 | git as a save mechanism. 37 | - Your Pull Request title and description become very important; they are the 38 | history of the master branch and explain all the changes. 39 | - You ought to be able to find any previous version easily using GitHub tabs, or 40 | [Releases](https://github.com/phantomstudios/react-share/releases) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Phantom Studios Ltd 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 | # react-share 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Actions Status][ci-image]][ci-url] 5 | [![PR Welcome][npm-downloads-image]][npm-downloads-url] 6 | 7 | An all-in-one React library to implement custom Page Sharing Meta and Social Media Sharing Buttons. 8 | 9 | ## Introduction 10 | 11 | Designed to use and extend [OpenGraph](https://ogp.me/) standards, including built-in support for sharing with: 12 | 13 | - Facebook 14 | - Linkedin 15 | - Twitter 16 | - WhatsApp 17 | - Copy to Clipboard 18 | 19 | ## Installation 20 | 21 | Install this package with `npm`. 22 | 23 | ```bash 24 | npm i @phntms/react-share 25 | ``` 26 | 27 | ## Usage 28 | 29 | Example usage in Next.js: 30 | 31 | ```JSX 32 | import Head from 'next/head'; 33 | import { MetaHeadEmbed } from "@phntms/react-share"; 34 | 35 | const PageLayout: React.FC = ({children}) => { 36 | <> 37 | {meta}} 39 | siteTitle="PHANTOM" 40 | pageTitle="Our Work" 41 | titleTemplate="[pageTitle] | [siteTitle]" 42 | description="Transforming challenges of all shapes and sizes into inventive, engaging and performance driven solutions that change the game." 43 | baseSiteUrl="https://phantom.land" 44 | pagePath="work" 45 | keywords={["creative-agency", "phantom", "work"]} 46 | imageUrl="https://bit.ly/3wiUOuk" 47 | imageAlt="PHANTOM logo." 48 | twitter={{ 49 | cardSize: "large", 50 | siteUsername: "@phntmLDN", 51 | creatorUsername: "@phntmLDN", 52 | }} 53 | /> 54 | {children} 55 | 56 | ); 57 | 58 | export default PageLayout; 59 | ``` 60 | 61 | ### <MetaHeadEmbed /> 62 | 63 | | Property | Type | Required | Notes | 64 | | -------------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 65 | | **render** | React.ReactNode | **Yes** | Unfortunately `react-helmet` and `next/head` are strict with how they accept meta tags. `react-helmet` doesn't support nesting. Whereas Next.JS only supports some children and not all, therefore a render function is required. | 66 | | **pageTitle** | string | No | Unique page title that describes the page, such as `Home`, `About` etc. etc. | 67 | | **siteTitle** | string | **Yes** | Title of the site, usually the organization / brand name. If `pageTitle` and `siteTitle` are the same, only this shows. | 68 | | **titleTemplate** | string | No | Title template used to display `pageTitle` and `siteTitle` in a template, displays the values using corresponding `[pageTitle]` and `[siteTitle]`. Example template: "[pageTitle] | [siteTitle]". | 69 | | **description** | string | **Yes** | A one to two sentence description of your webpage. Keep it within 160 characters, and write it to catch the user's attention. | 70 | | **baseSiteUrl** | string | **Yes** | Base site URL, excluding trailing slash. | 71 | | **pagePath** | string | No | The path of the current page, excluding leading slash. | 72 | | **disableCanonical** | boolean | No | Disable canonical if not desired, defaults to `false`. | 73 | | **keywords** | string|string[] | No | List of SEO keywords describing what your webpage does. Example, `"your, tags"` or `["your", "tags"]`. | 74 | | **imageUrl** | string | No | Image url of asset to share. Recommended aspect ratio for landscape is 1.9:1 (1200x630) or for squares 1:1 (1200x1200). For more info, visit [here](https://iamturns.com/open-graph-image-size/). If a relative URL is provided, `baseSiteUrl` is prefixed. If specifying a relative URL do not add the leading slash. | 75 | | **imageAlt** | string | No | Image alt for users who are visually impaired. | 76 | | **imageWidth** | number | No | Width of share image, defaults to `1200`. | 77 | | **imageHeight** | number | No | Height of share image, defaults to `630`. | 78 | | **locale** | string | No | The locale these tags are marked up in, such as; `en_GB`, `fr_FR` and `es_ES`. Defaults to `en_US`. | 79 | | **twitter** | TwitterEmbedProps | No | Optional twitter embed properties to include. | 80 | 81 | To use simply add `MetaHeadEmbed` to a shared layout to get the best out of page specific properties such as `pagePath`. 82 | 83 | **Note**: `baseSiteUrl` and `imageUrl` must start with `https://`, else they won't work when sharing. 84 | 85 | ### TwitterEmbedProps 86 | 87 | | Property | Type | Required | Notes | 88 | | ------------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 89 | | **cardSize** | 'small'|'large' | **Yes** | Summary card size. | 90 | | **title** | string | No | A concise title for the related content. If left blank, page title will be inherited instead. | 91 | | **description** | string | No | A description that concisely summarizes the content as appropriate for presentation within a Tweet. Should not be the same as title. If left blank, `MetaHeadEmbed` description will be inherited instead. | 92 | | **siteUsername** | string | No | The Twitter @username the card should be attributed to. | 93 | | **creatorUsername** | string | No | The Twitter @username for the content creator / author. | 94 | | **imageUrl** | string | No | Image to show in card. Images must be less than 5MB in size. Supported file types; JPG, PNG, WEBP and GIF. | 95 | | **imageAlt** | string | No | Image alt for users who are visually impaired. Maximum 420 characters. | 96 | 97 | **Note**: Image used should be different based on `cardSize`: 98 | 99 | - For `large` cards, use a 2:1 aspect ratio (300x157 px minium or 4096x4096 px maximum). 100 | - For `small` cards, use a 1:1 aspect ratio (144x144 px minium or 4096x4096 px maximum). 101 | 102 | **A Note on Twitter Tags** 103 | 104 | Twitter will inherit `og:title`, `og:description` and `og:image` tags by default, so unless you want unique fields, respective fields in `TwitterEmbedProps` should be left blank to avoid duplication. 105 | 106 | ### getFacebookUrl() 107 | 108 | | Parameter | Type | Required | Notes | 109 | | --------- | ------ | -------- | --------------------------------- | 110 | | url | string | **Yes** | URL of shared webpage. | 111 | | hashtag | string | No | Hashtag to show in Facebook card. | 112 | 113 | Basic component example usage: 114 | 115 | ```jsx 116 | import { getFacebookUrl } from "@phntms/react-share"; 117 | 118 | const ShareToFacebook = () => ( 119 | 120 | Share to Facebook 121 | 122 | ); 123 | 124 | export default ShareToFacebook; 125 | ``` 126 | 127 | ### getLinkedinUrl() 128 | 129 | | Parameter | Type | Required | Notes | 130 | | --------- | ------ | -------- | --------------------------------------------------------------------- | 131 | | url | string | **Yes** | URL of shared webpage. | 132 | | title | string | No | Title to show in card. | 133 | | summary | string | No | Description to show in card. | 134 | | source | string | No | Source of the content. For example, your website or application name. | 135 | 136 | Basic component example usage: 137 | 138 | ```jsx 139 | import { getLinkedinUrl } from "@phntms/react-share"; 140 | 141 | const ShareToLinkedin = () => ( 142 | 143 | Share to Linkedin 144 | 145 | ); 146 | 147 | export default ShareToLinkedin; 148 | ``` 149 | 150 | ### getTwitterUrl() 151 | 152 | | Parameter | Type | Required | Notes | 153 | | --------- | -------------------- | -------- | ------------------------------------------------------------------------------- | 154 | | url | string | **Yes** | URL of shared webpage. | 155 | | text | string | No | Text to show in Twitter card. | 156 | | hashtags | string|string[] | No | Hashtags to show in Twitter card. Example, `"your,tags"` or `["your", "tags"]`. | 157 | | related | string|string[] | No | Accounts to recommend following. Example, `"your, tags"` or `["your", "tags"]`. | 158 | 159 | Basic component example usage: 160 | 161 | ```jsx 162 | import { getTwitterUrl } from "@phntms/react-share"; 163 | 164 | const ShareToTwitter = () => ( 165 | Share to Twitter 166 | ); 167 | 168 | export default ShareToTwitter; 169 | ``` 170 | 171 | ### getWhatsAppUrl() 172 | 173 | | Parameter | Type | Required | Notes | 174 | | --------- | ------ | -------- | ---------------------------------------------------- | 175 | | url | string | **Yes** | URL of shared webpage. | 176 | | text | string | No | Text to show in the WhatsApp message before the URL. | 177 | 178 | Basic component example usage: 179 | 180 | ```jsx 181 | import { getWhatsAppUrl } from "@phntms/react-share"; 182 | 183 | const ShareToWhatsApp = () => ( 184 | 185 | Share to WhatsApp 186 | 187 | ); 188 | 189 | export default ShareToWhatsApp; 190 | ``` 191 | 192 | **Note**: WhatsApp links will only work on mobile, so be sure to hide any WhatsApp links if the user is not on a mobile device! 193 | 194 | ### getShareUrl() 195 | 196 | If you would rather have all share urls in one place, `getShareUrl()` can be used! It includes props from every social platform listed above, so simply pass in a `SocialPlatform`, and the platforms corresponding props. 197 | 198 | Example usage: 199 | 200 | ```jsx 201 | import { getShareUrl, SocialPlatforms } from "@phntms/react-share"; 202 | 203 | const Share = () => ( 204 | 205 | Share to Facebook 206 | 207 | 208 | Share to Linkedin 209 | 210 | 211 | Share to Twitter 212 | 213 | 214 | Share to WhatsApp 215 | 216 | ); 217 | 218 | export default Share; 219 | ``` 220 | 221 | ### copyToClipboard() 222 | 223 | Method used to copy a given text into your clipboard. 224 | 225 | ```jsx 226 | import { copyToClipboard } from "@phntms/react-share"; 227 | 228 | const Copy = () => ( 229 |
copyToClipboard("https://phantom.land")}>Copy
230 | ); 231 | 232 | export default Copy; 233 | ``` 234 | 235 | ## Further Resources 236 | 237 | Useful resources for testing meta properties: 238 | 239 | - [Meta Tags](https://metatags.io/) - With Meta Tags you can preview how your webpage will look on Google, Facebook, Twitter and more. 240 | - [Social Share Preview](https://chrome.google.com/webstore/detail/social-share-preview/ggnikicjfklimmffbkhknndafpdlabib?hl=en) - Chrome browser extension to live preview how the webpage will look when shared. Especially useful for testing when app is auth protected. 241 | 242 | ## 🍰 Requests and Contributing 243 | 244 | If a social media platform you want to use isn't already supported, or found an issue? Get involved! Please contribute using the GitHub Flow. Create a branch, add commits, and open a Pull Request or submit a new issue. 245 | 246 | Please read `CONTRIBUTING` for details on our `CODE_OF_CONDUCT`, and the process for submitting pull requests to us! 247 | 248 | [npm-image]: https://img.shields.io/npm/v/@phntms/react-share.svg?style=flat-square&logo=react 249 | [npm-url]: https://npmjs.org/package/@phntms/react-share 250 | [npm-downloads-image]: https://img.shields.io/npm/dm/@phntms/react-share.svg 251 | [npm-downloads-url]: https://npmcharts.com/compare/@phntms/react-share?minimal=true 252 | [ci-image]: https://github.com/phantomstudios/react-share/workflows/test/badge.svg 253 | [ci-url]: https://github.com/phantomstudios/react-share/actions 254 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # react-share Support 2 | 3 | For _questions_ on how to use `react-share` or what went wrong when you tried something, our primary resource is by opening a 4 | [GitHub Issue](https://github.com/phantomstudios/react-share/issues), where you can get help from developers. 5 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; 3 | import prettier from "eslint-plugin-prettier"; 4 | import _import from "eslint-plugin-import"; 5 | import globals from "globals"; 6 | import tsParser from "@typescript-eslint/parser"; 7 | import path from "node:path"; 8 | import { fileURLToPath } from "node:url"; 9 | import js from "@eslint/js"; 10 | import { FlatCompat } from "@eslint/eslintrc"; 11 | 12 | const FILE_NAME = fileURLToPath(import.meta.url); 13 | const DIRECTORY_NAME = path.dirname(FILE_NAME); 14 | const compat = new FlatCompat({ 15 | baseDirectory: DIRECTORY_NAME, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }); 19 | 20 | export default defineConfig([{ 21 | extends: fixupConfigRules(compat.extends( 22 | "plugin:react/recommended", 23 | "plugin:react-hooks/recommended", 24 | "plugin:@typescript-eslint/recommended", 25 | "plugin:prettier/recommended", 26 | )), 27 | plugins: { 28 | prettier: fixupPluginRules(prettier), 29 | import: fixupPluginRules(_import), 30 | }, 31 | languageOptions: { 32 | globals: { 33 | ...globals.browser, 34 | ...globals.node, 35 | React: "writable", 36 | }, 37 | parser: tsParser, 38 | ecmaVersion: 5, 39 | sourceType: "commonjs", 40 | parserOptions: { 41 | jsx: true, 42 | useJSXTextNode: true, 43 | }, 44 | }, 45 | settings: { 46 | react: { 47 | version: "detect", 48 | }, 49 | }, 50 | rules: { 51 | "react-hooks/rules-of-hooks": "error", 52 | "react-hooks/exhaustive-deps": "error", 53 | "react/react-in-jsx-scope": "off", 54 | "@typescript-eslint/explicit-function-return-type": "off", 55 | "@typescript-eslint/explicit-module-boundary-types": "off", 56 | "@typescript-eslint/ban-ts-comment": "off", 57 | "react/prop-types": "off", 58 | "import/order": ["error", { 59 | groups: ["builtin", "external", "internal"], 60 | pathGroups: [{ 61 | pattern: "react", 62 | group: "external", 63 | position: "before", 64 | }], 65 | pathGroupsExcludedImportTypes: ["react"], 66 | "newlines-between": "always", 67 | alphabetize: { 68 | order: "asc", 69 | caseInsensitive: true, 70 | }, 71 | }], 72 | "prettier/prettier": "error", 73 | }, 74 | }]); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@phntms/react-share", 3 | "description": "An all-in-one React library to implement custom Sharing Meta and Social Media Sharing Buttons.", 4 | "version": "1.0.2-rc1", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "homepage": "https://github.com/phantomstudios/react-share#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/phantomstudios/react-share.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/phantomstudios/react-share/issues" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "metadata", 18 | "share", 19 | "social-media", 20 | "sharing", 21 | "opengraph" 22 | ], 23 | "scripts": { 24 | "build": "tsc", 25 | "build:types": "tsc --emitDeclarationOnly", 26 | "prepublishOnly": "npm run build", 27 | "lint": "npm-run-all --parallel lint:*", 28 | "lint:js": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", 29 | "lint:format": "prettier \"**/*.{md,yml}\" --check", 30 | "lint:type-check": "tsc --noEmit", 31 | "fix": "npm-run-all --sequential fix:*", 32 | "fix:js": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix", 33 | "fix:format": "prettier \"**/*.{md,yml}\" --write", 34 | "depcheck": "npx npm-check --update" 35 | }, 36 | "author": "Josua Pedersen (josua@phantom.agency)", 37 | "license": "MIT", 38 | "peerDependencies": { 39 | "react": ">=17.0.2", 40 | "react-dom": ">=17.0.2" 41 | }, 42 | "devDependencies": { 43 | "@babel/eslint-parser": "^7.27.1", 44 | "@babel/preset-env": "^7.4.5", 45 | "@babel/preset-typescript": "^7.3.3", 46 | "@eslint/compat": "^1.2.9", 47 | "@eslint/eslintrc": "^3.3.1", 48 | "@eslint/js": "^9.28.0", 49 | "@types/react": "^19.0.12", 50 | "@typescript-eslint/eslint-plugin": "^8.33.0", 51 | "@typescript-eslint/parser": "^8.33.0", 52 | "eslint": "^9.28.0", 53 | "eslint-config-prettier": "^10.1.5", 54 | "eslint-plugin-import": "^2.31.0", 55 | "eslint-plugin-prettier": "^5.4.1", 56 | "eslint-plugin-react": "^7.37.5", 57 | "eslint-plugin-react-hooks": "^5.2.0", 58 | "globals": "^16.2.0", 59 | "npm-run-all": "^4.1.5", 60 | "prettier": "^3.5.3", 61 | "typescript": "^5.8.3" 62 | }, 63 | "dependencies": { 64 | "is-absolute-url": "^4.0.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/MetaHeadEmbed.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import isAbsoluteUrl from "is-absolute-url"; 4 | 5 | import { commaSeparate } from "../utils"; 6 | 7 | export interface TwitterEmbedProps { 8 | /** Summary card size. */ 9 | cardSize: "small" | "large"; 10 | 11 | /** A concise title for the related content. */ 12 | title?: string; 13 | 14 | /** 15 | * A description that concisely summarizes the content as appropriate for 16 | * presentation within a Tweet. Should not be the same as title. 17 | */ 18 | description?: string; 19 | 20 | /** The Twitter @username the card should be attributed to. */ 21 | siteUsername?: string; 22 | 23 | /** The Twitter @username for the content creator / author. */ 24 | creatorUsername?: string; 25 | 26 | /** 27 | * Image to show in card. _Should_ only be used if image is different to 28 | * `MetaHeadEmbed` image. 29 | * 30 | * Should be different based on `useLargeCard`: 31 | * - For large cards, use a 2:1 aspect ratio (300x157 px minium or 32 | * 4096x4096 px maximum). 33 | * - For small cards, use a 1:1 aspect ratio (144x144 px minium or 34 | * 4096x4096 px maximum). 35 | * 36 | * Images must be less than 5MB in size. 37 | * 38 | * Supported file types; JPG, PNG, WEBP and GIF. 39 | * 40 | * Note: Only the first frame of an animated GIF will be used. 41 | */ 42 | imageUrl?: string; 43 | 44 | /** Image alt for users who are visually impaired. Maximum 420 characters. */ 45 | imageAlt?: string; 46 | } 47 | 48 | export interface MetaEmbedProps { 49 | /** Returns meta properties to be rendered. */ 50 | render: (meta: React.ReactNode) => React.JSX.Element; 51 | 52 | /** Unique page title that describes the page, such as `Home`, `About` etc. */ 53 | pageTitle?: string; 54 | 55 | /** Title of the site, usually the organization / brand name. */ 56 | siteTitle: string; 57 | 58 | /** 59 | * Title template used to display `pageTitle` and `siteTitle` in a template. 60 | * Replaced `[pageTitle]` with `pageTitle` and `[siteTitle]` with `siteTitle`. 61 | * 62 | * Example template: `[pageTitle] | [siteTitle]`. 63 | */ 64 | titleTemplate?: string; 65 | 66 | /** Webpage description. Should be less than 160 characters. */ 67 | description: string; 68 | 69 | /** 70 | * Disable canonical if not desired, defaults to `false`. 71 | */ 72 | disableCanonical?: boolean; 73 | 74 | /** Base site URL, excluding trailing slash. */ 75 | baseSiteUrl: string; 76 | 77 | /** The path of the page, excluding leading slash. */ 78 | pagePath?: string; 79 | 80 | /** 81 | * List of SEO keywords describing what your webpage does. 82 | * Example: `"your, tags"` or `["your", "tags"]`. 83 | */ 84 | keywords?: string | string[]; 85 | 86 | /** 87 | * Image url of asset to share. Recommended aspect ratio for landscape is 88 | * 1.9:1 (1200x630) or for squares 1:1 (1200x1200). 89 | */ 90 | imageUrl?: string; 91 | 92 | /** 93 | * Width of share image. 94 | */ 95 | imageWidth?: number; 96 | 97 | /** 98 | * height of share image. 99 | */ 100 | imageHeight?: number; 101 | 102 | /** Image alt for users who are visually impaired. */ 103 | imageAlt?: string; 104 | 105 | /** 106 | * The locale these tags are marked up in, such as; `en_GB`, 107 | * `fr_FR` and `es_ES`. 108 | * 109 | * Defaults to `en_US`. 110 | */ 111 | locale?: string; 112 | 113 | /** Twitter embed properties */ 114 | twitter?: TwitterEmbedProps; 115 | } 116 | 117 | const MetaHeadEmbed = ({ 118 | render, 119 | pageTitle, 120 | siteTitle, 121 | titleTemplate, 122 | description, 123 | disableCanonical = false, 124 | baseSiteUrl, 125 | pagePath, 126 | keywords, 127 | imageUrl, 128 | imageAlt, 129 | imageWidth = 1200, 130 | imageHeight = 630, 131 | locale = "en_US", 132 | twitter, 133 | }: MetaEmbedProps) => { 134 | let title = siteTitle; 135 | if (titleTemplate && pageTitle && pageTitle !== siteTitle) { 136 | title = titleTemplate 137 | .replace("[pageTitle]", pageTitle) 138 | .replace("[siteTitle]", siteTitle); 139 | } 140 | 141 | const pageUrl = pagePath 142 | ? isAbsoluteUrl(pagePath) 143 | ? pagePath 144 | : `${baseSiteUrl}/${pagePath}` 145 | : baseSiteUrl; 146 | 147 | const image = 148 | imageUrl && 149 | (isAbsoluteUrl(imageUrl) ? imageUrl : `${baseSiteUrl}/${imageUrl}`); 150 | 151 | const metaEmbed = [ 152 | {title}, 153 | , 154 | keywords && ( 155 | 160 | ), 161 | 162 | !disableCanonical && ( 163 | 164 | ), 165 | 166 | , 167 | , 168 | , 169 | , 174 | , 175 | , 180 | , 185 | , 186 | , 187 | , 188 | ]; 189 | 190 | const twitterEmbed = ({ 191 | cardSize, 192 | title, 193 | description, 194 | siteUsername, 195 | creatorUsername, 196 | imageUrl, 197 | imageAlt, 198 | }: TwitterEmbedProps) => [ 199 | , 204 | title && , 205 | description && ( 206 | 211 | ), 212 | siteUsername && ( 213 | 214 | ), 215 | creatorUsername && ( 216 | 221 | ), 222 | imageUrl && ( 223 | 224 | ), 225 | imageAlt && ( 226 | 227 | ), 228 | ]; 229 | 230 | return render([metaEmbed, twitter && twitterEmbed({ ...twitter })]); 231 | }; 232 | 233 | export default MetaHeadEmbed; 234 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as MetaHeadEmbed, 3 | MetaEmbedProps, 4 | TwitterEmbedProps, 5 | } from "./components/MetaHeadEmbed"; 6 | 7 | export { 8 | default as getLinkedinUrl, 9 | LinkedinProps, 10 | } from "./utils/getLinkedinUrl"; 11 | 12 | export { default as getTwitterUrl, TwitterProps } from "./utils/getTwitterUrl"; 13 | 14 | export { 15 | default as getFacebookUrl, 16 | FacebookProps, 17 | } from "./utils/getFacebookUrl"; 18 | 19 | export { 20 | default as getWhatsAppUrl, 21 | WhatsAppProps, 22 | } from "./utils/getWhatsAppUrl"; 23 | 24 | export { 25 | default as getShareUrl, 26 | AllSocialPlatformProps, 27 | } from "./utils/getShareUrl"; 28 | 29 | export { default as copyToClipboard } from "./utils/copyToClipboard"; 30 | 31 | export { SocialPlatforms } from "./types"; 32 | -------------------------------------------------------------------------------- /src/types.tsx: -------------------------------------------------------------------------------- 1 | export interface BaseShareProps { 2 | /** URL of webpage shared. */ 3 | url: string; 4 | } 5 | 6 | export enum SocialPlatforms { 7 | Facebook, 8 | Linkedin, 9 | Twitter, 10 | WhatsApp, 11 | } 12 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const commaSeparate = (words?: string | string[]) => 2 | typeof words === "string" ? words : words?.join(","); 3 | -------------------------------------------------------------------------------- /src/utils/copyToClipboard.ts: -------------------------------------------------------------------------------- 1 | const fallbackCopyToClipboard = (text: string) => { 2 | const placeholder = document.createElement("textarea"); 3 | placeholder.value = text; 4 | 5 | // Avoid scrolling to bottom 6 | placeholder.style.top = "0"; 7 | placeholder.style.left = "0"; 8 | placeholder.style.position = "fixed"; 9 | 10 | // Append element, and focus 11 | document.body.appendChild(placeholder); 12 | placeholder.focus(); 13 | placeholder.select(); 14 | 15 | // Finally, remove element after copy 16 | document.body.removeChild(placeholder); 17 | }; 18 | 19 | export const copyToClipboard = (text: string) => { 20 | if (!navigator.clipboard) fallbackCopyToClipboard(text); 21 | else navigator.clipboard.writeText(text); 22 | }; 23 | 24 | export default copyToClipboard; 25 | -------------------------------------------------------------------------------- /src/utils/getFacebookUrl.ts: -------------------------------------------------------------------------------- 1 | import objectToUrlParams from "./objectToUrlParams"; 2 | import { BaseShareProps } from "../types"; 3 | 4 | export interface FacebookProps extends BaseShareProps { 5 | /** Hashtag to show in Facebook card. */ 6 | hashtag?: string; 7 | } 8 | 9 | export const getFacebookUrl = ({ 10 | url, 11 | hashtag: suppliedHashtag, 12 | }: FacebookProps) => { 13 | let hashtag = suppliedHashtag; 14 | if (hashtag && hashtag.charAt(0) !== "#") hashtag = `#${hashtag}`; 15 | return `https://www.facebook.com/sharer/sharer.php${objectToUrlParams({ 16 | u: url, 17 | hashtag, 18 | })}`; 19 | }; 20 | 21 | export default getFacebookUrl; 22 | -------------------------------------------------------------------------------- /src/utils/getLinkedinUrl.ts: -------------------------------------------------------------------------------- 1 | import objectToUrlParams from "./objectToUrlParams"; 2 | import { BaseShareProps } from "../types"; 3 | 4 | export interface LinkedinProps extends BaseShareProps { 5 | /** Title value to show in card. */ 6 | title?: string; 7 | 8 | /** Description to show in card. */ 9 | summary?: string; 10 | 11 | /** Source of the content (for example... your website or application name). */ 12 | source?: string; 13 | } 14 | 15 | const getLinkedinUrl = ({ url, title, summary, source }: LinkedinProps) => 16 | `https://linkedin.com/shareArticle${objectToUrlParams({ 17 | url, 18 | mini: "true", 19 | title, 20 | summary, 21 | source, 22 | })}`; 23 | 24 | export default getLinkedinUrl; 25 | -------------------------------------------------------------------------------- /src/utils/getShareUrl.ts: -------------------------------------------------------------------------------- 1 | import getFacebookUrl, { FacebookProps } from "./getFacebookUrl"; 2 | import getLinkedinUrl, { LinkedinProps } from "./getLinkedinUrl"; 3 | import getTwitterUrl, { TwitterProps } from "./getTwitterUrl"; 4 | import getWhatsAppUrl, { WhatsAppProps } from "./getWhatsAppUrl"; 5 | import { SocialPlatforms } from "../types"; 6 | 7 | export type AllSocialPlatformProps = FacebookProps & 8 | LinkedinProps & 9 | TwitterProps & 10 | WhatsAppProps; 11 | 12 | export const getShareUrl = ( 13 | socialPlatform: SocialPlatforms, 14 | { 15 | url, 16 | hashtag, 17 | title, 18 | summary, 19 | source, 20 | text, 21 | hashtags, 22 | related, 23 | }: AllSocialPlatformProps, 24 | ) => { 25 | switch (socialPlatform) { 26 | case SocialPlatforms.Facebook: 27 | return getFacebookUrl({ url, hashtag }); 28 | 29 | case SocialPlatforms.Linkedin: 30 | return getLinkedinUrl({ url, title, summary, source }); 31 | 32 | case SocialPlatforms.Twitter: 33 | return getTwitterUrl({ url, text, hashtags, related }); 34 | 35 | case SocialPlatforms.WhatsApp: 36 | return getWhatsAppUrl({ url, text }); 37 | } 38 | }; 39 | 40 | export default getShareUrl; 41 | -------------------------------------------------------------------------------- /src/utils/getTwitterUrl.ts: -------------------------------------------------------------------------------- 1 | import objectToUrlParams from "./objectToUrlParams"; 2 | import { BaseShareProps } from "../types"; 3 | import { commaSeparate } from "../utils"; 4 | 5 | export interface TwitterProps extends BaseShareProps { 6 | /** Text to show in card. */ 7 | text?: string; 8 | 9 | /** 10 | * Hashtags to show in Twitter card. 11 | * Example: `"your, tags"` or `["your", "tags"]`. 12 | */ 13 | hashtags?: string | string[]; 14 | 15 | /** 16 | * Accounts to recommend following. 17 | * Example: `"your, tags"` or `["your", "tags"]`. 18 | */ 19 | related?: string | string[]; 20 | } 21 | 22 | export const getTwitterUrl = ({ url, text, hashtags, related }: TwitterProps) => 23 | `https://twitter.com/share${objectToUrlParams({ 24 | url, 25 | text, 26 | hashtags: commaSeparate(hashtags), 27 | related: commaSeparate(related), 28 | })}`; 29 | 30 | export default getTwitterUrl; 31 | -------------------------------------------------------------------------------- /src/utils/getWhatsAppUrl.ts: -------------------------------------------------------------------------------- 1 | import objectToUrlParams from "./objectToUrlParams"; 2 | import { BaseShareProps } from "../types"; 3 | 4 | export interface WhatsAppProps extends BaseShareProps { 5 | /** Text to prefill into the WhatsApp message. Appears prior to the URL. */ 6 | text?: string; 7 | } 8 | 9 | export const getWhatsAppUrl = ({ url, text }: WhatsAppProps) => 10 | `whatsapp://send${objectToUrlParams({ 11 | text: text ? `${text} ${url}` : url, 12 | })}`; 13 | 14 | export default getWhatsAppUrl; 15 | -------------------------------------------------------------------------------- /src/utils/objectToUrlParams.ts: -------------------------------------------------------------------------------- 1 | interface UrlDataProps { 2 | [key: string]: string | number | undefined | null; 3 | } 4 | 5 | export default function objectToUrlParams(data: UrlDataProps) { 6 | const params = Object.entries(data) 7 | .filter(([, value]) => value) 8 | .map( 9 | ([key, value]) => 10 | `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`, 11 | ); 12 | return params.length ? `?${params.join("&")}` : ""; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["dom", "es2017"], 6 | "jsx": "react", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "outDir": "./lib", 11 | "rootDir": "./src", 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noImplicitReturns": true, 19 | "allowSyntheticDefaultImports": true, 20 | "esModuleInterop": true 21 | }, 22 | "include": ["src/**/*.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6" 5 | } 6 | } 7 | --------------------------------------------------------------------------------