├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── package.json ├── renovate.json ├── src ├── facebook.tsx ├── index.tsx ├── seo.tsx └── twitter.tsx ├── test └── index.test.tsx ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 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 spences10apps@gmail.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Scott Spence 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React SEO Component 2 | 3 | [![CodeFactor](https://www.codefactor.io/repository/github/spences10/react-seo-component/badge)](https://www.codefactor.io/repository/github/spences10/react-seo-component) 4 | ![bundlephobia min](https://badgen.net/bundlephobia/min/react-seo-component) 5 | ![bundlephobia minzip](https://badgen.net/bundlephobia/minzip/react) 6 | 7 | Use it for adding canonical links, metadata and OpenGraph information 8 | to your React projects! 9 | 10 | If you are rendering client side (not using Gatsby, or Next.js static 11 | routes) then you can use [react-snap] to create your static HTML. 12 | 13 | This is primarily targeted for use in Gatsby sites. 14 | 15 | ## Use it! 16 | 17 | Install it from npm! 18 | 19 | ```bash 20 | yarn add react-seo-component 21 | # peer dependency of react helmet 22 | yarn add react-helmet 23 | ``` 24 | 25 | If you are using it with Gatsby you will also need to install the 26 | Gatsby plugin: 27 | 28 | ```bash 29 | yarn add react-seo-component 30 | yarn add react-helmet 31 | yarn add gatsby-plugin-react-helmet 32 | # or in one command 33 | yarn add react-seo-component react-helmet gatsby-plugin-react-helmet 34 | ``` 35 | 36 | This will create the meta tags at build time. 37 | 38 | **Examples:** 39 | 40 | For an index page: 41 | 42 | ```jsx 43 | 54 | ``` 55 | 56 | For a blog post: 57 | 58 | ```jsx 59 | 74 | ``` 75 | 76 | ## Props 77 | 78 | | Prop | Type | Default | 79 | | --------------- | ------------------------------- | ------------ | 80 | | title | Page title | '' | 81 | | titleTemplate | Page Title + Site title | '' | 82 | | titleSeparator | Between Page Title + Site title | · | 83 | | description | Page description | '' | 84 | | pathname | Full Page URL | '' | 85 | | article | `article` or `website` | `website` | 86 | | image | Full image URL | '' | 87 | | siteLanguage | Content Language | `en` | 88 | | siteLocale | Content Locale | `en_gb` | 89 | | twitterUsername | can be empty | '' | 90 | | author | can _not_ be empty | 'J Doe' | 91 | | datePublished | ISO date string | `Date.now()` | 92 | | dateModified | ISO date string | `Date.now()` | 93 | 94 | ## To test locally 95 | 96 | Use `npm pack` or `yarn pack` to create a `.tgz` of the project you 97 | can install locally on your project to test with. 98 | 99 | ```bash 100 | # from here 101 | yarn pack 102 | # copy to project to test 103 | cp react-seo-component-2.0.1.tgz ../project-to-test-with/ 104 | # ~/project-to-test-with 105 | yarn add file:react-seo-component-2.0.1.tgz 106 | ``` 107 | 108 | ## Thanks: 109 | 110 | - **[LekoArts]** for the initial components detailed in his Gatsby 111 | [Prismic starter]. 112 | 113 | - **[Leigh Halliday]** for the [primer video] on using [TSDX] 114 | 115 | - **[Jared Palmer]** for [TSDX] 116 | 117 | ## Resources 118 | 119 | https://medium.com/recraftrelic/building-a-react-component-as-a-npm-module-18308d4ccde9 120 | 121 | https://github.com/recraftrelic/dummy-react-npm-module/blob/master/package.json 122 | 123 | 124 | 125 | [lekoarts]: https://github.com/LekoArts 126 | [prismic starter]: https://github.com/LekoArts/gatsby-starter-prismic 127 | [jared palmer]: https://github.com/jaredpalmer 128 | [leigh halliday]: https://github.com/leighhalliday 129 | [tsdx]: https://github.com/jaredpalmer/tsdx 130 | [primer video]: https://www.youtube.com/watch?v=V3XZYC8zmvo 131 | [react-snap]: https://github.com/stereobooster/react-snap 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.2", 3 | "license": "MIT", 4 | "author": "Scott Spence (https://scottspence.dev/)", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "start": "tsdx watch", 16 | "build": "tsdx build", 17 | "test": "tsdx test --passWithNoTests", 18 | "lint": "tsdx lint", 19 | "prepare": "tsdx build" 20 | }, 21 | "peerDependencies": { 22 | "react": ">=16", 23 | "react-helmet": "6.0.0" 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "25.2.1", 27 | "@types/react": "16.9.34", 28 | "@types/react-dom": "16.9.6", 29 | "@types/react-helmet": "5.0.15", 30 | "husky": "4.2.5", 31 | "react": "16.13.1", 32 | "react-dom": "16.13.1", 33 | "tsdx": "0.13.1", 34 | "tslib": "1.11.1", 35 | "typescript": "3.8.3" 36 | }, 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "tsdx lint" 40 | } 41 | }, 42 | "prettier": { 43 | "printWidth": 70, 44 | "semi": false, 45 | "singleQuote": true, 46 | "trailingComma": "es5", 47 | "proseWrap": "always" 48 | }, 49 | "name": "react-seo-component", 50 | "module": "dist/react-seo-component.esm.js", 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/spences10/react-seo-component.git" 54 | }, 55 | "keywords": [ 56 | "react", 57 | "seo", 58 | "component", 59 | "gatsby", 60 | "opengraph", 61 | "og", 62 | "facebook", 63 | "twitter", 64 | "jsonld" 65 | ], 66 | "bugs": { 67 | "url": "https://github.com/spences10/react-seo-component/issues" 68 | }, 69 | "homepage": "https://github.com/spences10/react-seo-component#readme" 70 | } 71 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "reviewers": ["spences10"], 4 | "bumpVersion": "patch", 5 | "baseBranches": ["patch"], 6 | "automerge": true, 7 | "major": { 8 | "automerge": false 9 | }, 10 | "lockFileMaintenance": { 11 | "enabled": true, 12 | "extends": "schedule:monthly" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/facebook.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | 4 | interface Props { 5 | url: string 6 | type: string 7 | title: string 8 | desc: string 9 | image: string 10 | locale: string 11 | } 12 | 13 | export const Facebook = ({ 14 | url, 15 | type, 16 | title, 17 | desc, 18 | image, 19 | locale, 20 | }: Props) => ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Facebook } from './facebook' 2 | import { SEO } from './seo' 3 | import { Twitter } from './twitter' 4 | 5 | export { Facebook, Twitter } 6 | 7 | export default SEO 8 | -------------------------------------------------------------------------------- /src/seo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { Facebook } from './facebook' 4 | import { Twitter } from './twitter' 5 | 6 | interface Props { 7 | title: string 8 | titleTemplate: string 9 | titleSeparator?: string 10 | description: string 11 | pathname: string 12 | article?: boolean 13 | image?: string 14 | siteLanguage: string 15 | siteLocale: string 16 | twitterUsername: string 17 | author?: string 18 | datePublished?: string 19 | dateModified?: string 20 | } 21 | 22 | export const SEO = ({ 23 | title, 24 | titleTemplate, 25 | titleSeparator, 26 | description, 27 | pathname, 28 | article = false, 29 | image, 30 | siteLanguage, 31 | siteLocale, 32 | twitterUsername, 33 | author = 'J Doe.', 34 | datePublished, 35 | dateModified, 36 | }: Props) => { 37 | const seo = { 38 | title: title.slice(0, 70), 39 | description: description.slice(0, 160), 40 | datePublished: datePublished 41 | ? null 42 | : new Date(Date.now()).toISOString(), 43 | dateModified: dateModified 44 | ? null 45 | : new Date(Date.now()).toISOString(), 46 | } 47 | 48 | const copyrightYear = new Date().getFullYear() 49 | 50 | // schema.org in JSONLD format 51 | // https://developers.google.com/search/docs/guides/intro-structured-data 52 | // You can fill out the 'author', 'creator' with more data or another type (e.g. 'Organization') 53 | // Structured Data Testing Tool >> 54 | // https://search.google.com/structured-data/testing-tool 55 | 56 | const schemaOrgWebPage = { 57 | '@context': 'http://schema.org', 58 | '@type': 'WebPage', 59 | url: pathname, 60 | headline: seo.description, 61 | inLanguage: siteLanguage, 62 | mainEntityOfPage: pathname, 63 | description: seo.description, 64 | name: seo.title, 65 | author: { 66 | '@type': 'Person', 67 | name: author, 68 | }, 69 | copyrightHolder: { 70 | '@type': 'Person', 71 | name: author, 72 | }, 73 | copyrightYear, 74 | creator: { 75 | '@type': 'Person', 76 | name: author, 77 | }, 78 | publisher: { 79 | '@type': 'Person', 80 | name: author, 81 | }, 82 | datePublished: seo.datePublished, 83 | dateModified: seo.dateModified, 84 | image: { 85 | '@type': 'ImageObject', 86 | url: `${image}`, 87 | }, 88 | } 89 | 90 | // Initial breadcrumb list 91 | 92 | const itemListElement = [ 93 | { 94 | '@type': 'ListItem', 95 | item: { 96 | '@id': pathname, 97 | name: 'Homepage', 98 | }, 99 | position: 1, 100 | }, 101 | ] 102 | 103 | let schemaArticle = null 104 | 105 | if (article) { 106 | schemaArticle = { 107 | '@context': 'http://schema.org', 108 | '@type': 'Article', 109 | author: { 110 | '@type': 'Person', 111 | name: author, 112 | }, 113 | copyrightHolder: { 114 | '@type': 'Person', 115 | name: author, 116 | }, 117 | copyrightYear, 118 | creator: { 119 | '@type': 'Person', 120 | name: author, 121 | }, 122 | publisher: { 123 | '@type': 'Organization', 124 | name: author, 125 | logo: { 126 | '@type': 'ImageObject', 127 | url: `${image}`, 128 | }, 129 | }, 130 | datePublished: seo.datePublished, 131 | dateModified: seo.dateModified, 132 | description: seo.description, 133 | headline: seo.title, 134 | inLanguage: siteLanguage, 135 | url: pathname, 136 | name: seo.title, 137 | image: { 138 | '@type': 'ImageObject', 139 | url: image, 140 | }, 141 | mainEntityOfPage: pathname, 142 | } 143 | // Push current blog post into breadcrumb list 144 | itemListElement.push({ 145 | '@type': 'ListItem', 146 | item: { 147 | '@id': pathname, 148 | name: seo.title, 149 | }, 150 | position: 2, 151 | }) 152 | } 153 | 154 | const breadcrumb = { 155 | '@context': 'http://schema.org', 156 | '@type': 'BreadcrumbList', 157 | description: 'Breadcrumbs list', 158 | name: 'Breadcrumbs', 159 | itemListElement, 160 | } 161 | 162 | return ( 163 | <> 164 | 170 | 171 | 172 | 173 | 174 | {!article && ( 175 | 178 | )} 179 | {article && ( 180 | 183 | )} 184 | 187 | 188 | {image && ( 189 | <> 190 | 198 | 204 | 205 | )} 206 | 207 | ) 208 | } 209 | -------------------------------------------------------------------------------- /src/twitter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | 4 | interface Props { 5 | type?: string // optional denoted with ? 6 | username: string 7 | title: string 8 | desc: string 9 | image: string 10 | } 11 | 12 | export const Twitter = ({ 13 | type = 'summary_large_image', 14 | username, 15 | title, 16 | desc, 17 | image, 18 | }: Props) => ( 19 | 20 | {username && } 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import SEO from '../src' 4 | 5 | describe('it', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div') 8 | ReactDOM.render( 9 | , 19 | div 20 | ) 21 | ReactDOM.unmountComponentAtNode(div) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "importHelpers": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "./src", 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "strictPropertyInitialization": true, 15 | "noImplicitThis": true, 16 | "alwaysStrict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "moduleResolution": "node", 22 | "baseUrl": "./", 23 | "paths": { 24 | "*": ["src/*", "node_modules/*"] 25 | }, 26 | "jsx": "react", 27 | "esModuleInterop": true 28 | } 29 | } 30 | --------------------------------------------------------------------------------