diff --git a/gatsby-config.js b/gatsby-config.js index 89f6f96..8a3c1de 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -7,6 +7,9 @@ module.exports = { title: `Layer5 Recognition Program`, description: `Showcasing Your Achievements as a User and a Contributor`, siteUrl: `https://badges.layer5.io`, + social: { + twitter: `@layer5`, + }, }, plugins: [ `gatsby-plugin-styled-components`, diff --git a/gatsby-node.js b/gatsby-node.js index 84800ec..57b0667 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -94,6 +94,8 @@ exports.createSchemaCustomization = ({ actions }) => { // blog posts are stored inside "content/blog" instead of returning an error createTypes(` type SiteSiteMetadata { + title: String + description: String author: Author siteUrl: String social: Social diff --git a/src/pages/404.js b/src/pages/404.js index d0e63f3..9a18cee 100644 --- a/src/pages/404.js +++ b/src/pages/404.js @@ -1,4 +1,5 @@ import * as React from "react" +import Seo from "../sitecomponents/SEO" const NotFoundPage = ({ location }) => { return ( @@ -9,6 +10,6 @@ const NotFoundPage = ({ location }) => { ) } -export const Head = () => 404: Not Found +export const Head = () => export default NotFoundPage \ No newline at end of file diff --git a/src/pages/discussion-leaderboard.js b/src/pages/discussion-leaderboard.js index 3942796..13e427d 100644 --- a/src/pages/discussion-leaderboard.js +++ b/src/pages/discussion-leaderboard.js @@ -12,6 +12,7 @@ import { } from '../sitecomponents/index.style'; import Navigation from '../sitecomponents/Navigation'; import Header from '../sitecomponents/Leaderboard/Header'; +import Seo from '../sitecomponents/SEO'; const LeaderBoard = () => { const [theme, toggleTheme] = useDarkMode(); @@ -38,11 +39,6 @@ const LeaderBoard = () => { theme === 'light' ? layer5LeaderboardLightMode : layer5LeaderboardDarkMode; return ( <> - Layer5 LeaderBoard - { }; export default LeaderBoard; + +export const Head = () => ( + +); diff --git a/src/pages/index.js b/src/pages/index.js index 22a6248..8685f30 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -16,6 +16,7 @@ import recognitionLogo from '../assets/images/recognition-program.png'; import recognitionBanner from '../assets/images/recognition-banner.png'; import '../fonts.css'; import GithubLogo from './githubLogo'; +import Seo from '../sitecomponents/SEO'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import layer5Logo from '../assets/images/layer5/layer5-badges.png'; import layer5LogoLight from '../assets/images/layer5/layer5-badges-white.png'; @@ -218,11 +219,6 @@ const App = () => { // }; return ( <> - Layer5 Recognition Program - @@ -380,3 +376,5 @@ const App = () => { }; export default App; + +export const Head = () => ; diff --git a/src/sitecomponents/SEO/index.js b/src/sitecomponents/SEO/index.js new file mode 100644 index 0000000..b4ad8c0 --- /dev/null +++ b/src/sitecomponents/SEO/index.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { useStaticQuery, graphql, withPrefix } from 'gatsby'; +import defaultSocialImage from '../../assets/images/recognition-banner.png'; + +// Intrinsic dimensions of `defaultSocialImage`. Update alongside the image so +// that crawlers can lay the card out before the image itself is fetched. +const DEFAULT_SOCIAL_IMAGE_WIDTH = 3629; +const DEFAULT_SOCIAL_IMAGE_HEIGHT = 1599; + +/** + * Renders the document's SEO metadata, including Open Graph and Twitter Card + * tags so that links to the site unfurl with a rich preview on social media, + * Slack, Discord and other community channels. + * + * Intended to be used from a page's `Head` export: + * + * export const Head = () => ; + */ +const Seo = ({ title, description, image, pathname, children }) => { + const { site } = useStaticQuery(graphql` + query SeoMetadata { + site { + siteMetadata { + title + description + siteUrl + social { + twitter + } + } + } + } + `); + + const metadata = site.siteMetadata; + const siteUrl = metadata.siteUrl.replace(/\/$/, ''); + // Gatsby serves pages with a trailing slash, so keep the canonical and + // `og:url` values in step with the production URL that is actually shared. + // A page with no URL of its own omits `pathname` and gets neither tag: the + // 404 is served for every unknown path, so claiming `/404/` as its canonical + // would be a claim about a URL the visitor did not request. + const path = pathname ? pathname.replace(/\/?$/, '/') : null; + + // Assets do need the prefix. An imported asset gets it from webpack's + // publicPath, so `defaultSocialImage` is already prefixed and must not be + // passed through `withPrefix` again; a caller-supplied path out of + // `static/` does not, so it goes through `withPrefix`. + const imageSrc = image ? withPrefix(image) : defaultSocialImage; + + const seo = { + title: title || metadata.title, + description: description || metadata.description, + // Deliberately unprefixed. `pathPrefix` is only ever set by the PR preview + // workflow, which is an ephemeral deployment; production is served from the + // root of siteUrl. The canonical and og:url of a preview should therefore + // point at the production route, not at the preview path. + url: path && `${siteUrl}${path}`, + // Only a root-relative path is ours to qualify. `withPrefix` returns an + // absolute URL unchanged, and webpack inlines a small enough asset as a + // `data:` URI, so prepending siteUrl unconditionally would corrupt both. + image: imageSrc.startsWith('/') ? `${siteUrl}${imageSrc}` : imageSrc, + twitter: metadata.social?.twitter, + }; + + return ( + <> + {seo.title} + + {seo.url && } + + + + + + {seo.url && } + + + {!image && ( + + )} + {!image && ( + + )} + + + + + + + {seo.twitter && } + {seo.twitter && } + + {children} + + ); +}; + +export default Seo;