diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..30468b1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,101 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..8c8ff30
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,12 @@
+logs
+*.log
+node_modules
+*.un~
+yarn.lock
+package-lock.json
+flow-typed
+coverage
+decls
+examples
+.gitattributes
+gatsby-plugin-seo.code-workspace
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..41bf941
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Pittica S.r.l.s.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..91ab546
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# pittica/gatsby-plugin-seo
+
+
+
+
+
+
+
+## Description
+
+SEO plugin for [GatsbyJS](https://www.gatsbyjs.org/).
+
+## Install
+
+[](https://www.npmjs.com/package/@pittica/gatsby-plugin-seo)
+
+```shell
+npm install @pittica/gatsby-plugin-seo
+```
+
+## Usage
+
+The plugin provides SEO optimization.
+
+## Configuration
+
+Edit your **gatsby-config.js**.
+
+```javascript
+module.exports = {
+ plugins: [
+ {
+ resolve: `@pittica/gatsby-plugin-seo`,
+ options: {
+ image: `/DEFAULT_SHARING_IMAGE.jpg`,
+ socials: {
+ instagram: {
+ username: `INSTAGRAM_USERNAME`
+ },
+ github: {
+ username: `GITHUB_USERNAME`
+ },
+ facebook: {
+ page: `FACEBOOK_PAGE_USERNAME`,
+ app: `FACEBOOK_APP_ID`
+ },
+ twitter: {
+ username: `TWITTER_USERNAME`,
+ site: `TWITTER_SITE_USERNAME`
+ }
+ }
+ },
+ ],
+}
+```
+
+## Copyright
+
+(c) 2020, Pittaca S.r.l.s.
diff --git a/gatsby-node.js b/gatsby-node.js
new file mode 100644
index 0000000..43c159a
--- /dev/null
+++ b/gatsby-node.js
@@ -0,0 +1,33 @@
+const loadsh = require(`lodash`)
+
+exports.onCreateNode = ({ node, actions }, options) => {
+ const { createNodeField } = actions
+
+ if (node.id === `SiteBuildMetadata`) {
+ const socials = loadsh.merge({
+ instagram: {
+ username: ``
+ },
+ github: {
+ username: ``
+ },
+ facebook: {
+ page: ``,
+ app: ``
+ },
+ twitter: {
+ username: ``,
+ site: ``
+ }
+ }, options.socials || {})
+
+ createNodeField({
+ name: `seo`,
+ node,
+ value: {
+ image: options.image,
+ socials
+ }
+ })
+ }
+}
diff --git a/gatsby-plugin-seo.code-workspace b/gatsby-plugin-seo.code-workspace
new file mode 100644
index 0000000..876a149
--- /dev/null
+++ b/gatsby-plugin-seo.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..0921884
--- /dev/null
+++ b/index.js
@@ -0,0 +1,6 @@
+import OpenGraph from "./src/open-graph"
+import TwitterCard from "./src/twitter-card"
+import SchemaOrg from "./src/schema-org"
+import SEO from "./src/seo"
+
+export { SEO, SchemaOrg, OpenGraph, TwitterCard }
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b0809b4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@pittica/gatsby-plugin-seo",
+ "private": false,
+ "description": "SEO optimization plugin for GatsbyJS.",
+ "version": "1.0.0",
+ "author": {
+ "name": "Lucio Benini",
+ "email": "info@pittica.com",
+ "url": "https://pittica.com"
+ },
+ "bugs": {
+ "url": "https://github.com/pittica/gatsby-plugin-seo/issues"
+ },
+ "deprecated": false,
+ "homepage": "https://github.com/pittica/gatsby-plugin-seo",
+ "keywords": [
+ "gatsby",
+ "gatsby-plugin",
+ "seo",
+ "social-networks"
+ ],
+ "dependencies": {
+ "prop-types": "^15.7.2",
+ "gatsby-plugin-react-helmet": "^3.3.1"
+ },
+ "devDependencies": {
+ "gatsby": "^2.21.22",
+ "react": "^16.13.1",
+ "react-helmet": "^6.0.0",
+ "react-dom": "^16.13.1",
+ "lodash": "^4.17.15"
+ },
+ "license": "MIT",
+ "main": "index.js",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/pittica/gatsby-plugin-seo.git"
+ }
+}
\ No newline at end of file
diff --git a/src/open-graph.js b/src/open-graph.js
new file mode 100644
index 0000000..e092de5
--- /dev/null
+++ b/src/open-graph.js
@@ -0,0 +1,80 @@
+import React from "react"
+import { Helmet } from "react-helmet"
+import { useStaticQuery, graphql } from "gatsby"
+import PropTypes from "prop-types"
+
+const OpenGraph = ({ url, title, article, description, image }) => {
+ const data = useStaticQuery(
+ graphql`
+ query {
+ site {
+ siteMetadata {
+ locale {
+ language
+ culture
+ }
+ }
+ }
+ siteBuildMetadata {
+ fields {
+ seo {
+ socials {
+ facebook {
+ page
+ app
+ }
+ }
+ }
+ }
+ }
+ }
+ `
+ )
+ const facebook = data.siteBuildMetadata.fields.seo.socials.facebook
+
+ return (
+
+
+ {article ? : null}
+
+
+
+ {image ? (
+
+ ) : null}
+ {facebook.app ? (
+
+ ) : null}
+ {article && facebook.page ? (
+
+ ) : null}
+
+ )
+}
+
+OpenGraph.propTypes = {
+ url: PropTypes.string,
+ article: PropTypes.bool,
+ image: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired
+}
+
+OpenGraph.defaultProps = {
+ url: null,
+ article: false,
+ image: null,
+ title: null,
+ description: null
+}
+
+export default OpenGraph
diff --git a/src/schema-org.jsx b/src/schema-org.jsx
new file mode 100644
index 0000000..bbc89fd
--- /dev/null
+++ b/src/schema-org.jsx
@@ -0,0 +1,82 @@
+import React from "react"
+import { Helmet } from "react-helmet"
+
+export default React.memo(
+ ({
+ author,
+ siteUrl,
+ datePublished,
+ defaultTitle,
+ description,
+ image,
+ isBlogPost,
+ organization,
+ title,
+ url
+ }) => {
+ const baseSchema = [
+ {
+ "@context": "http://schema.org",
+ "@type": "WebSite",
+ url,
+ name: title,
+ alternateName: defaultTitle
+ }
+ ]
+
+ const schema = isBlogPost
+ ? [
+ ...baseSchema,
+ {
+ "@context": "http://schema.org",
+ "@type": "BreadcrumbList",
+ itemListElement: [
+ {
+ "@type": "ListItem",
+ position: 1,
+ item: {
+ "@id": url,
+ name: title,
+ image
+ }
+ }
+ ]
+ },
+ {
+ "@context": "http://schema.org",
+ "@type": "BlogPosting",
+ url,
+ name: title,
+ alternateName: defaultTitle,
+ headline: title,
+ image: {
+ "@type": "ImageObject",
+ url: image
+ },
+ description,
+ author: {
+ "@type": "Person",
+ name: author.name
+ },
+ publisher: {
+ "@type": "Organization",
+ url: organization.url,
+ logo: organization.logo,
+ name: organization.name
+ },
+ mainEntityOfPage: {
+ "@type": "WebSite",
+ "@id": siteUrl
+ },
+ datePublished
+ }
+ ]
+ : baseSchema
+
+ return (
+
+
+
+ )
+ }
+)
diff --git a/src/seo.js b/src/seo.js
new file mode 100644
index 0000000..105ed75
--- /dev/null
+++ b/src/seo.js
@@ -0,0 +1,95 @@
+import React, { Fragment } from "react"
+import { Helmet } from "react-helmet"
+import { useStaticQuery, graphql } from "gatsby"
+import PropTypes from "prop-types"
+import { OpenGraph, TwitterCard, SchemaOrg } from "@pittica/gatsby-plugin-seo"
+
+const SEO = ({ postData, frontmatter, image, isBlogPost, title, path }) => {
+ const { site } = useStaticQuery(
+ graphql`
+ query RemarkQuery {
+ site {
+ siteMetadata {
+ title
+ description
+ siteUrl
+ locale {
+ language
+ }
+ author
+ organization {
+ company
+ url
+ logo
+ }
+ }
+ }
+ }
+ `
+ )
+
+ const siteUrl = site.siteMetadata.siteUrl.replace(/\/$/, "")
+ const postMeta = frontmatter || postData.frontmatter || {}
+ const postTitle = title
+ ? title
+ : postMeta.title
+ ? postMeta.title
+ : site.siteMetadata.title
+ const description = postMeta.description || site.siteMetadata.description
+ const postImage = image
+ ? `${siteUrl}/${image.replace(/^\//, "")}`
+ : `${siteUrl}/${site.siteMetadata.seo.image.replace(/^\//, "")}`
+ const url = `${siteUrl}${path}`
+ const datePublished = isBlogPost ? postMeta.datePublished : false
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+SEO.propTypes = {
+ isBlogPost: PropTypes.bool,
+ postData: PropTypes.shape({
+ childMarkdownRemark: PropTypes.shape({
+ frontmatter: PropTypes.any,
+ excerpt: PropTypes.any
+ })
+ }),
+ image: PropTypes.string,
+ title: PropTypes.string
+}
+
+SEO.defaultProps = {
+ isBlogPost: false,
+ postData: { childMarkdownRemark: {} },
+ image: null,
+ title: null
+}
+
+export default SEO
\ No newline at end of file
diff --git a/src/twitter-card.js b/src/twitter-card.js
new file mode 100644
index 0000000..b4e738d
--- /dev/null
+++ b/src/twitter-card.js
@@ -0,0 +1,59 @@
+import React from "react"
+import { Helmet } from "react-helmet"
+import { useStaticQuery, graphql } from "gatsby"
+import PropTypes from "prop-types"
+
+const TwitterCard = ({ title, description, image }) => {
+ const { siteBuildMetadata } = useStaticQuery(
+ graphql`
+ query {
+ siteBuildMetadata {
+ fields {
+ seo {
+ socials {
+ twitter {
+ username
+ site
+ }
+ }
+ }
+ }
+ }
+ }
+ `
+ )
+
+ const twitter = siteBuildMetadata.fields.seo.socials.twitter
+
+ return (
+
+
+
+
+ {image ? (
+
+ ) : null}
+ {twitter.site ? (
+
+ ) : null}
+ {twitter.creator ? (
+
+ ) : null}
+
+
+ )
+}
+
+TwitterCard.propTypes = {
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ image: PropTypes.string
+}
+
+TwitterCard.defaultProps = {
+ title: null,
+ description: null,
+ image: null
+}
+
+export default TwitterCard