├── .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 | [](https://www.codefactor.io/repository/github/spences10/react-seo-component)
4 | 
5 | 
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 |
--------------------------------------------------------------------------------