Gatsby: SEO対策(Twitterカードなどmetaタグ設置)
イントロ
SEOコンポーネントを作成してそれをスタティックページや動的ページのテンプレートに埋め込む方法。プラグインを使う方法もあるが日本語対応やその他まだ不安なのでオーソドックスなやり方を採用する。
方法
SEOコンポーネントを新規作成
src/componentsの下にseo.jsを新規作成。default-starterにはすでにseo.jsがあって、以下のコードがデフォルトで準備されている。
/**
* SEO component that queries for data with
* Gatsby's useStaticQuery React hook
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
function SEO({ description, lang, meta, title }) {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site.siteMetadata.description
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
SEO.defaultProps = {
lang: `en`,
meta: [],
description: ``,
}
SEO.propTypes = {
description: PropTypes.string,
lang: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string.isRequired,
}
export default SEO
SEOコンポーネントを編集
デフォルトコードはわかりにくいのと動的ページに対応していないのでAdding an SEO Componentを見ながら編集していく。
まずgatsby-config.jsのsiteMetadataの設定は以下のようにしてあるので↓
module.exports = {
siteMetadata: {
title: `Exampleタイトル`,
titleTemplate: `%s · Exampleタイトル`,
description: `説明説明説明説明説明説明説明`,
author: `T.W author`,
siteUrl: `https://example.site`,// gatsby-plugin-canonical-urlsで使ってるのであえてurlと分けた.
url: `https://example.site`,
image: `/icons/icon-96x96.png`,
twitterUsername: `@examplemaster`
},
・・・略・・・
SEOコンポーネントを埋め込むページで
<SEO title="" description="" image="/images/xxxx.png" lang="en" />
といった具合にプロパティで値渡しすることになる。
-
image
のパス:ビルドしてデプロイするとhttps://example.site/twitcard/xxxx.png といった形で参照可能になるように、プロジェクトルート(注意:src直下ではない!)にstaticフォルダを作成してその下にtwitcardフォルダを作成、そこに画像を入れていくだけでよい。 -
lang
は英語ページもあるので用意した。
したがってまず、以下のように各属性をオブジェクト化する。
//seo.js
・・・略・・・
SEO.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
image: PropTypes.string,
lang: PropTypes.string,
}
SEO.defaultProps = {
title: null,
description: null,
image: null,
lang: null,
}
export default SEO
それからGraphQLクエリの部分を編集。
gatsby-config.jsのsiteMetadataからクエリってくるものを、あるものはdefault...というエイリアスをつけ、またあるものはそのまま使う(titleTemplateやtwitterUsernameなど)
// components/seo.js
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useLocation } from "@reach/router" //追加
import { useStaticQuery, graphql } from "gatsby"
function SEO({ title, description, image, lang, meta, article }) {
const { location } = useLocation() //追加
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
defaultTitle: title
titleTemplate
defaultDescription: description
siteUrl: url
defaultImage: image
defaultLang: lang
twitterUsername
}
}
}
`
)
・・・略・・・
そしてクエリからオブジェクトをつくる。まずはgatsby-config.jsのsiteMetadataの値をデフォルトの値として作成。 つぎに値渡しされた属性があればそれを、なければデフォルトの値をあてこむseoオブジェクトを作成。
・・・略・・・
const {
defaultTitle,
titleTemplate,
defaultDescription,
siteUrl,
defaultImage,
defaultLang,
twitterUsername,
} = site.siteMetadata
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: `${siteUrl}${image || defaultImage}`,
lang: lang || defaultLang,
url: `${siteUrl}${pathname}` // ${siteUrl}${pathname}でhttps://example.site/about とかになる
}
・・・略・・・
- urlは、表示ぺージのカレントURLを
const { pathname } = useLocation()
でとってこれるようだ。例えばhttps://example.com/about のページを表示しているとすると、pathname変数には/about
という値が入る(前にスラッシュが入る)。動的ページも同様に/articles/1
や/patients-article/9
などのようなパス値が入る。
最後に``のタグ内にメタデータを配置していく。``はHTMLの<HEAD>内容を編集できるReact特有のタグ。
・・・略・・・
return (
<Helmet>
<title>{seo.title}</title>
<html lang={seo.lang} />
<template>{seo.titleTemplate}</template>
<meta name="description" content={seo.description} />
<meta name="image" content={seo.image} />
<meta property="og:url" content={seo.url} />
<meta property="og:type" content="article" />
<meta property="og:title" content={seo.title} />
<meta property="og:description" content={seo.description} />
<meta property="og:image" content={seo.image} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content={twitterUsername} />
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
<meta name="twitter:image" content={seo.image} />
</Helmet>
)
・・・略・・・
完成
//components/seo.js
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useLocation } from "@reach/router"
import { useStaticQuery, graphql } from "gatsby"
function SEO({ title, description, image, lang }) {
const { pathname } = useLocation()
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
defaultTitle: title
titleTemplate
defaultDescription: description
siteUrl: url
defaultImage: image
defaultLang: lang
twitterUsername
}
}
}
`
)
const {
defaultTitle,
titleTemplate,
defaultDescription,
siteUrl,
defaultImage,
defaultLang,
twitterUsername,
} = site.siteMetadata
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: `${siteUrl}${image || defaultImage}`,
lang: lang || defaultLang,
url: `${siteUrl}${pathname}`
}
return (
<Helmet>
<title>{seo.title}</title>
<html lang={seo.lang} />
<template>{seo.titleTemplate}</template>
<meta name="description" content={seo.description} />
<meta name="image" content={seo.image} />
<meta property="og:url" content={seo.url} />
<meta property="og:type" content="article" />
<meta property="og:title" content={seo.title} />
<meta property="og:description" content={seo.description} />
<meta property="og:image" content={seo.image} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content={twitterUsername} />
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
<meta name="twitter:image" content={seo.image} />
</Helmet>
)
}
SEO.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
image: PropTypes.string,
lang: PropTypes.string,
}
SEO.defaultProps = {
title: null,
description: null,
image: null,
lang: null,
}
export default SEO
各ページに<SEO />
を埋め込む
スタティックページの場合
index.jsのコードにはデフォルトでタグがありtitle属性だけ存在しているが、そこにdescription, image, langを足していくだけ。このように↓
// pages/about.js
<SEO title="Exampleサイトの概要"
description="概要の説明概要の説明概要の説明概要の説明概要の説明概要の説明概要の説明概要の説明"
image="/twitterimg/introduction.png"
lang="ja"
/>
動的ページの場合
いま開発中のものはmicroCMSにコンテンツをアップしているのでそこから引っ張ってくる。なのでこうなる…
// templates/article.js
・・・略・・・
<SEO title={post.title}
description={post.body}
image={post.pict.url}
lang="ja"
/>
・・・略・・・
としたいものだが、これならチョー簡単であるが、descriptionとimageが問題となる。
description
{post.body}はそのままだとHTMLも含む本文記事全部になってしまう。したがって、以下のようにstriptagsというライブラリを使いHTMLタグを除去し、sliceで最初の120文字だけを抜き取る関数をお借りした。ブログ記事ページでの呼び出し
// templates/article.js
・・・略・・・
let striptags = require('striptags');
function sumarrize(html) {
const metaDescription = striptags(html).replace(/\r?\n/g, '').trim();
return metaDescription.length <= 120
? metaDescription
: metaDescription.slice(0, 120) + '...';
}
・・・略・・・
そしてSEOのdescription属性はその関数名でこのように記述。
<SEO title={post.title}
description={sumarrize(post.body)} // 変更
image={post.pict.url}
lang="ja"
/>
image
microCMSからもってくる{post.pict.url}はhttps://images.microcms-assets.io/protected/ap-northeast-1:xxxxxx-xxxx-xxxx-xxxx-xxxxxx2b0dee1/service/xxxxxxx/media/Michel.png
←こういったものになるので、seo.jsのこの部分、
//components/seo.js
・・・略・・・
image: `${siteUrl}${image || defaultImage}`,
・・・略・・・
${siteUrl}${image}
は、こんな文字列を出力してしまう↓
https://example.sitehttps://images.microcms-assets.io/protected/ap-northeast-1:xxxxxx-xxxx-xxxx-xxxx-xxxxxx2b0dee1/service/xxxxxxx/media/NicolL.png
これについてはスタティックページの場合と動的ページの場合とで判断して適切な画像パスを返す関数を作成した。スタティックページの場合、その画像パスはhttps://example.site/twitcard/xxxxxx_001_small.png
といったように100文字は超えず、動的ページの場合microCMSがホストする画像URLは必ず100文字超えてくるので文字数で判断、動的ページの場合は2番目のhttps://~~~のURLを採用する関数staticOrDynamic()を作成。
//components/seo.js
・・・略・・・
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: staticOrDynamic(siteUrl + image), // 変更
lang: lang || defaultLang,
url: `${siteUrl}${pathname}`
}
・・・略・・・
function staticOrDynamic(imgPath) {
const str = imgPath
let array = str.split(/https:\/\//);
//console.log('◆strは'+ str + '◆str.lengthは'+ str.length)
//console.log('■arrayは', array)
//console.log('■最終形 ' + 'https://' + array[2])
return str.length <= 100 // 100文字以下ならstrをそのまま返す.100文字以上なら`https:// + array[2]`を返す。
? str
: 'https://' + array[2]
}
・・・略・・・
ちょっと苦しみだがあらためてseo.jsの完成形。
// components/seo.js
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useLocation } from "@reach/router"
import { useStaticQuery, graphql } from "gatsby"
function SEO({ title, description, image, lang }) {
const { pathname } = useLocation()
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
defaultTitle: title
titleTemplate
defaultDescription: description
siteUrl: url
defaultImage: image
defaultLang: lang
twitterUsername
}
}
}
`
)
const {
defaultTitle,
titleTemplate,
defaultDescription,
siteUrl,
defaultImage,
defaultLang,
twitterUsername,
} = site.siteMetadata
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: staticOrDynamic(siteUrl + image), // 動的ページの場合siteUrl + imageで'https://benzoinfojapan.orghttps://images.microcms-assets.io/protected/ap-northeast-1:4fa35fa7-a818-40f5-b6bb-89f712b0dee1/service/benzoinfo/media/NicolL.png'となってしまう
lang: lang || defaultLang,
url: `${siteUrl}${pathname}`
}
function staticOrDynamic(imgPath) {
const str = imgPath
let array = str.split(/https:\/\//);
return str.length <= 100
? str
: 'https://' + array[2]
}
return (
<Helmet>
<title>{seo.title}</title>
<html lang={seo.lang} />
<template>{titleTemplate}</template>
<meta name="description" content={seo.description} />
<meta name="image" content={seo.image} />
<meta property="og:url" content={seo.url} />
<meta property="og:type" content="article" />
<meta property="og:title" content={seo.title} />
<meta property="og:description" content={seo.description} />
<meta property="og:image" content={seo.image} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content={twitterUsername} />
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
<meta name="twitter:image" content={seo.image} />
</Helmet>
)
}
SEO.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
image: PropTypes.string,
lang: PropTypes.string,
}
SEO.defaultProps = {
title: null,
description: null,
image: null,
lang: null,
}
export default SEO
念のため、動的ページを表示するarticle.jsも。
// templates/article.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"
import { Container, Row, Col, Breadcrumb} from 'react-bootstrap'
const ArticlePost = props => {
const post = props.data.microcmsArticles // ㊟allMicrocmsArticleでない
const categoryName = post.category[0].name // パンくずで使う上位ページの分類名
let categoryString = ""
return (
<Layout>
<Container fluid="md">
<SEO title={post.title}
description={sumarrize(post.body)}
image={post.pict.url}
lang="ja"
/>
<Breadcrumb style={{fontSize: `0.65rem`, backgroundColor: `white`}}>
<Breadcrumb.Item href="/">ホーム</Breadcrumb.Item>
<Breadcrumb.Item href={`/${categoryName}`}>
{categoryString}
</Breadcrumb.Item>
<Breadcrumb.Item active>{post.title}</Breadcrumb.Item>
</Breadcrumb>
<div>
<h1 style={{ fontSize: `1.25rem`}}>{post.title}</h1>
<span style={{ fontSize: `1.1rem`}}
dangerouslySetInnerHTML={{
__html: `${post.title_origin}`,
}}
>
</span>
<Row>
<Col md={8}>
<span style={{ fontSize: `0.9rem`, color: `gray` }}>著者:{post.writer.name}</span>
</Col>
<Col md={4}>
<span style={{ fontSize: `0.9rem`, color: `gray` }}>投稿:{post.date}</span>
</Col>
</Row>
<br />
<p
dangerouslySetInnerHTML={{
__html: `${post.body}`,
}}
></p>
<br />
<span>著者:{post.writer.name}</span>
<br />
<img src={post.writer.image.url} width={160} alt={post.writer.name} />
<p
dangerouslySetInnerHTML={{
__html: `${post.writer.profile}`,
}}
></p>
</div>
</Container>
</Layout>
)
}
export default ArticlePost
export const query = graphql`
query($id: String!) {
microcmsArticles(id: { eq: $id }) {
title
title_origin
date
body
pict {
url
}
body
category {
name
}
writer {
name
profile
image {
url
}
}
}
}
`
// striptagsでHTMLタグを除去しsliceで最初の120文字だけを抜き取る関数
let striptags = require('striptags');
function sumarrize(html) {
const metaDescription = striptags(html).replace(/\r?\n/g, '').trim();
return metaDescription.length <= 120
? metaDescription
: metaDescription.slice(0, 120) + '...';
}
本の宣伝
Gatsbyバージョン5>>>>改訂2版
前編の『Gatsby5前編ー最新Gatsbyでつくるコーポレートサイト』と後編の『Gatsby5後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》』を合わせ、次のようなデモサイトを構築します。
→ https://yah-space.work
静的サイトジェネレーターGatsby最新バージョン5の基本とFile System Route APIを使用して動的にページを生成する方法を解説。またバージョン5の新機能《Slicy API》《Script API》《Head API》を紹介、実装方法も。《Gatsby Functions》での問い合わせフォーム実装やGatsby Cloudへのアップロード方法も!
Gatsby5前編ー最新Gatsbyでつくるコーポレートサイト ~基礎の基礎から応用、新機能の導入まで(書籍2,980円)
最新Gatsby5とmicroCMSを組み合わせてのコーポレートサイト作成手順を解説。《サイト内検索機能》をGatsbyバージョン4からの新機能《Gatsby Functions》と《microCMSのqパラメータ》で実装。また、SEOコンポーネントをカスタマイズしてmicroCMS APIをツイッターカードに表示させるOGPタグ実装方法も解説。
Gatsby5後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》(書籍 2,790円)
参考:
An implementation of PHP's strip_tags in Node.js
ブログ記事ページでの呼び出し
Adding an SEO Component
Gatsby.jsにreact-helmetを導入してhead要素(メタタグ)をカスタマイズする