#16 feat(seo): Added SEO support and site config (#23)

* fix(seo): og:title parsing & remove synopsis meta since it's dynamic

* cleanup(seo): consistent imports & newlines at eof

* Removed viewport meta tag from layout since it's duplicate

* siteMetadata => useSiteMeta; inline pageTitle; consistent defaultProps
This commit is contained in:
Buzzbyte
2020-11-24 19:41:46 -05:00
committed by GitHub
parent 66b465c4c6
commit fab507ef11
14 changed files with 156 additions and 5 deletions
+4
View File
@@ -1,3 +1,7 @@
# General
.env
.env.*
# NPM
/node_modules
+12 -1
View File
@@ -1,5 +1,6 @@
const fs = require("fs");
const path = require("path");
const siteConfig = require("./site-config")
// Load environment configuration
require("dotenv").config({
@@ -16,8 +17,18 @@ const aliases = fs.readdirSync(path.join(__dirname, "src")).reduce((obj, directo
return obj;
}, {});
// If Gatsby already set up a path prefix, use that instead
const pathPrefix = process.env.GATSBY_PATH_PREFIX || siteConfig.pathPrefix;
module.exports = {
pathPrefix: process.env.GATSBY_PATH_PREFIX || "/animethemes",
pathPrefix: pathPrefix,
siteMetadata: {
lang: siteConfig.siteLanguage,
siteName: siteConfig.siteName,
description: siteConfig.description,
titleTemplate: `%s · ${siteConfig.siteName}`,
siteUrl: path.posix.join((process.env.SITE_URL || siteConfig.rootUrl), pathPrefix)
},
plugins: [
"gatsby-source-animethemes",
"gatsby-plugin-styled-components",
+1 -1
View File
@@ -19,7 +19,7 @@
"gatsby-plugin-fontawesome-css": "^1.0.0",
"gatsby-plugin-layout": "^1.3.12",
"gatsby-plugin-manifest": "^2.7.0",
"gatsby-plugin-react-helmet": "^3.3.12",
"gatsby-plugin-react-helmet": "^3.5.0",
"gatsby-plugin-root-import": "^2.0.5",
"gatsby-plugin-styled-components": "^3.3.12",
"node-fetch": "^2.6.1",
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
// Basic site metadata
siteName: "AnimeThemes",
description: "AnimeThemes is a simple and consistent repository of anime opening and ending themes.",
siteLanguage: "en",
// Prefix for all links (for production builds)
pathPrefix: "/animethemes",
// Site URL without pathPrefix
rootUrl: "https://animethemes.github.io"
}
+2 -3
View File
@@ -7,6 +7,7 @@ import Navigation from "components/navigation";
import Container from "components/container";
import VideoPlayer from "components/videoPlayer";
import PlayerContext from "context/playerContext";
import SEO from "components/seo";
export default function Layout({ children, data }) {
const video = data ? data.video : null;
@@ -21,11 +22,9 @@ export default function Layout({ children, data }) {
return (
<ThemeProvider theme={theme}>
<PlayerContext.Provider value={{ currentVideo, setCurrentVideo }}>
<SEO />
<Helmet>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://fonts.googleapis.com/css?family=Roboto:wght@400,700&display=swap" rel="stylesheet"/>
<title>AnimeThemes</title>
</Helmet>
<GlobalStyle/>
<Navigation/>
+60
View File
@@ -0,0 +1,60 @@
import React from "react"
import PropTypes from "prop-types";
import {useLocation} from "@reach/router";
import {Helmet} from "react-helmet";
import useSiteMeta from "hooks/useSiteMeta";
export default function SEO({title, description, meta = [], lang}) {
const {pathname} = useLocation();
const {
description: defaultDescription,
lang: defaultLanguage,
titleTemplate,
siteName,
siteUrl
} = useSiteMeta();
const seo = {
url: `${siteUrl}${pathname}`,
lang: lang || defaultLanguage,
title: title || siteName,
description: description || defaultDescription
}
return (
<Helmet
htmlAttributes={{
lang: seo.lang
}}
title={seo.title}
titleTemplate={title ? titleTemplate : null}
meta={[
{
name: "description",
content: seo.description
},
{
property: "og:site_name",
content: siteName
},
{
property: "og:title",
content: title ? titleTemplate.replace(/%s/g, title) : seo.title
},
{
property: "og:description",
content: seo.description
}
].concat(meta)}
/>
)
}
SEO.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
lang: PropTypes.string
}
+20
View File
@@ -0,0 +1,20 @@
import {useStaticQuery, graphql} from "gatsby";
export default function useSiteMeta() {
const {site} = useStaticQuery(query);
return site.siteMetadata;
}
export const query = graphql`
query SEO {
site {
siteMetadata {
titleTemplate
description
siteName
siteUrl
lang
}
}
}
`
+7
View File
@@ -9,6 +9,7 @@ import Title from "components/text/title";
import Flex, {FlexItem} from "components/flex";
import Switcher from "components/switcher";
import Button from "components/button";
import SEO from "components/seo";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faTimes} from "@fortawesome/free-solid-svg-icons";
@@ -35,6 +36,11 @@ export default function SearchPage({ location: { pathname, search, hash } }) {
const [ searchQuery, setSearchQuery ] = useState(urlParams.get("q") || "");
const [ debouncedSearchQuery ] = useDebounce(searchQuery, 500);
// Generates page title based on search query
const pageTitle = searchQuery && searchQuery.trim()
? `${searchQuery} - Search`
: "Search";
// Temporary effect to listen for changes to the search query that may be made by the quick search (WIP)
useEffect(() => { setSearchQuery(urlParams.get("q")) }, [ urlParams ]);
@@ -59,6 +65,7 @@ export default function SearchPage({ location: { pathname, search, hash } }) {
return (
<StyledSearchPage>
<SEO title={pageTitle} />
<Title>Search</Title>
<StyledSearchOptions>
<FlexItem flex={1}>
+2
View File
@@ -2,6 +2,7 @@ import styled from "styled-components";
import Flex from "components/flex";
import Button from "components/button";
import Title from "components/text/title";
import SEO from "components/seo";
import {graphql} from "gatsby";
const StyledYearPage = styled.div`
@@ -13,6 +14,7 @@ const StyledYearPage = styled.div`
export default function YearPage({ data: { allAnime } }) {
return (
<StyledYearPage>
<SEO title="Browse by Year"></SEO>
{allAnime.groupedByYear.map(({ year }) => (
<Flex key={year} alignItems="center" justifyContent="center">
<Button to={`/year/${year}`}>
+2
View File
@@ -11,6 +11,7 @@ import ContainerSidebar from "components/container/sidebar";
import CollapseCard from "components/card/collapse";
import {fullWidth, gapsColumn} from "styles/mixins";
import ThemeSwitcher from "components/switcher/theme";
import SEO from "components/seo";
const StyledAnimePage = styled.div`
${gapsColumn("1.5rem")}
@@ -76,6 +77,7 @@ export default function AnimeDetailPage({ data: { anime } }) {
return (
<StyledAnimePage>
<SEO title={anime.name} />
<Title>{anime.name}</Title>
<ContainerSidebar sidebar={sidebar}>
<Flex gapsColumn="1rem">
+2
View File
@@ -10,6 +10,7 @@ import Flex from "components/flex";
import ContainerSidebar from "components/container/sidebar";
import {fullWidth, gapsColumn} from "styles/mixins";
import ThemeSearchResultCard from "components/card/searchResult/theme";
import SEO from "components/seo";
const StyledArtistPage = styled.div`
${gapsColumn("1.5rem")}
@@ -76,6 +77,7 @@ export default function ArtistDetailPage({ data: { artist } }) {
return (
<StyledArtistPage>
<SEO title={artist.name} />
<Title>{artist.name}</Title>
<ContainerSidebar sidebar={sidebar}>
<Flex gapsColumn="1rem">
+4
View File
@@ -7,6 +7,7 @@ import {gapsColumn} from "styles/mixins";
import Button from "components/button";
import Flex from "components/flex";
import Switcher from "components/switcher";
import SEO from "components/seo";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown} from "@fortawesome/free-solid-svg-icons";
@@ -35,9 +36,12 @@ const StyledYearNext = styled(StyledYear)`
export default function SeasonIndexPage({ data: { allAnime }, pageContext: { year, season, yearList, seasonList } }) {
const previousYear = yearList.indexOf(year) > 0 ? yearList[yearList.indexOf(year) - 1] : null;
const nextYear = yearList.indexOf(year) < yearList.length - 1 ? yearList[yearList.indexOf(year) + 1] : null;
const pageTitle = season ? `${season} ${year} Anime` : `${year} Anime`;
return (
<StyledPage>
<SEO title={pageTitle} />
<StyledYearContainer>
<StyledYearPrevious>
{previousYear && (
+2
View File
@@ -5,6 +5,7 @@ import ContainerSidebar from "components/container/sidebar";
import styled from "styled-components";
import {gapsColumn} from "styles/mixins";
import AnimeSearchResultCard from "components/card/searchResult/anime";
import SEO from "components/seo";
import useAniList from "hooks/useAniList";
import {graphql} from "gatsby";
@@ -61,6 +62,7 @@ export default function SeriesDetailPage({ data: { series } }) {
return (
<StyledSeriesPage>
<SEO title={series.name} />
<Title>{series.name}</Title>
<ContainerSidebar sidebar={sidebar}>
<Flex gapsColumn="1rem">
+26
View File
@@ -3,6 +3,7 @@ import Flex, {FlexItem} from "components/flex";
import styled from "styled-components";
import Text from "components/text";
import useAniList from "hooks/useAniList";
import useSiteMeta from "hooks/useSiteMeta";
import SongTitleWithArtists from "components/utils/songTitleWithArtists";
import VideoTags from "components/utils/videoTags";
import ThemeEntryTags from "components/utils/themeEntryTags";
@@ -11,6 +12,7 @@ import Title from "components/text/title";
import VideoButton from "components/button/video";
import AnimeSearchResultCard from "components/card/searchResult/anime";
import ArtistSearchResultCard from "components/card/searchResult/artist";
import SEO from "components/seo";
const StyledVideoInfo = styled(Flex).attrs({
gapsColumn: "2rem"
@@ -23,6 +25,7 @@ export default function VideoPage({ data: { video } }) {
const theme = entry.theme;
const anime = theme.anime;
const { siteName } = useSiteMeta();
const { image } = useAniList(anime);
useEffect(() => {
@@ -52,8 +55,31 @@ export default function VideoPage({ data: { video } }) {
};
}).filter((otherEntry) => !!otherEntry);
// Generates and returns page title
const pageTitle = entry.version
? `${theme.song.title} (${anime.name} ${theme.slug} V${entry.version})`
: `${theme.song.title} (${anime.name} ${theme.slug})`;
const pageDesc = (() => {
// Generates and returns page description for SEO
let song = theme.song,
artistStr = "",
version = entry.version ? ` Version ${entry.version}` : "";
if (song.performances && song.performances.length) {
artistStr = " by ";
song.performances.map((performance, index) => {
artistStr += performance.as || performance.artist.name;
if (index < song.performances.length - 1) {
artistStr += (index === song.performances.length - 2 ? ", and " : ", ");
}
});
}
return `Watch ${anime.name} ${theme.slug}${version}: ${song.title}${artistStr} on ${siteName}`;
})();
return (
<StyledVideoInfo>
<SEO title={pageTitle} description={pageDesc} />
<Flex row alignItems="center" gapsRow="1rem">
<FlexItem flex={1}>
<Flex justifyContent="center" gapsColumn="0.25rem">