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 + +![License](https://img.shields.io/github/license/pittica/gatsby-plugin-seo) +![Version](https://img.shields.io/github/package-json/v/pittica/gatsby-plugin-seo) +![Release](https://img.shields.io/github/v/release/pittica/gatsby-plugin-seo) +![GitHub package.json dependency version (dev dep on branch)](https://img.shields.io/github/package-json/dependency-version/pittica/gatsby-plugin-seo/dev/gatsby) +![GitHub package.json dependency version (dev dep on branch)](https://img.shields.io/github/package-json/dependency-version/pittica/gatsby-plugin-seo/dev/react) + +## Description + +SEO plugin for [GatsbyJS](https://www.gatsbyjs.org/). + +## Install + +[![npm](https://img.shields.io/npm/v/@pittica/gatsby-plugin-seo)](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