Gatsby: SEO対策（Twitterカードなどmetaタグ設置）

# Gatsby: SEO対策（Twitterカードなどmetaタグ設置）

## イントロ
SEOコンポーネントを作成してそれをスタティックページや動的ページのテンプレートに埋め込む方法。プラグインを使う方法もあるが日本語対応やその他まだ不安なのでオーソドックスなやり方を採用する。

## 方法
### SEOコンポーネントを新規作成
src/componentsの下にseo.jsを新規作成。default-starterにはすでにseo.jsがあって、以下のコードがデフォルトで準備されている。

```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](https://www.gatsbyjs.org/docs/add-seo-component/)を見ながら編集していく。


まずgatsby-config.jsのsiteMetadataの設定は以下のようにしてあるので↓

```js
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`は英語ページもあるので用意した。

したがってまず、以下のように各属性をオブジェクト化する。

```js
//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

```

<br>

それからGraphQLクエリの部分を編集。
gatsby-config.jsのsiteMetadataからクエリってくるものを、あるものはdefault...というエイリアスをつけ、またあるものはそのまま使う（titleTemplateやtwitterUsernameなど）

```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, meta, article  }) {
  const { location } = useLocation()　 //追加
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            defaultTitle: title
            titleTemplate
            defaultDescription: description
            siteUrl: url
            defaultImage: image
            defaultLang: lang
            twitterUsername
          }
        }
      }
    `
  )
・・・略・・・
```

<br>
そしてクエリからオブジェクトをつくる。まずはgatsby-config.jsのsiteMetadataの値をデフォルトの値として作成。
つぎに値渡しされた属性があればそれを、なければデフォルトの値をあてこむseoオブジェクトを作成。


```js
・・・略・・・
  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`などのようなパス値が入る。

<br />
最後に`<Helmet />`のタグ内にメタデータを配置していく。`<Helmet />`はHTMLの＜HEAD>内容を編集できるReact特有のタグ。

```js
・・・略・・・
  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>
  )
・・・略・・・
```

### 完成

```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: `${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のコードにはデフォルトで<SEO />タグがありtitle属性だけ存在しているが、そこにdescription, image, langを足していくだけ。このように↓

```js
// pages/about.js
    <SEO title="Exampleサイトの概要"
        description="概要の説明概要の説明概要の説明概要の説明概要の説明概要の説明概要の説明概要の説明" 
        image="/twitterimg/introduction.png" 
        lang="ja"
    />
```

### 動的ページの場合

いま[開発中のもの](https://qiita.com/atomyah/items/759823f6c1e86ce44b23)はmicroCMSにコンテンツをアップしているのでそこから引っ張ってくる。なのでこうなる…

```js
// 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文字だけを抜き取る関数をお借りした。[ブログ記事ページでの呼び出し](https://takumon.com/2018/09/16/#%E3%83%96%E3%83%AD%E3%82%B0%E8%A8%98%E4%BA%8B%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%A7%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97)

```js
// 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属性はその関数名でこのように記述。

```js
     <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のこの部分、

```jsx
//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()を作成。

```js
//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の完成形。

```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も。

```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) + '...';
}

```

<br />


# 本の宣伝
### Gatsbyバージョン５>>>>改訂２版

**前編**の『Gatsby５前編ー最新Gatsbyでつくるコーポレートサイト』と**後編**の『Gatsby５後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》』を合わせ、次のようなデモサイトを構築します。
→ **https://yah-space.work**
<hr />


静的サイトジェネレーターGatsby**最新バージョン５**の基本とFile System Route APIを使用して動的にページを生成する方法を解説。またバージョン５の新機能《**Slicy API**》《**Script API**》《**Head API**》を紹介、実装方法も。《**Gatsby Functions**》での問い合わせフォーム実装やGatsby Cloudへのアップロード方法も！


<a href="https://www.amazon.co.jp/gp/product/B0BXNJTJMY?&linkCode=li3&tag=kokkahasan-22&linkId=93c5e3bf1f226141ddc807300e81b07a&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B0BXNJTJMY&Format=_SL250_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=kokkahasan-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&language=ja_JP&l=li3&o=9&a=B0BXNJTJMY" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
<a target="_blank" href="https://www.amazon.co.jp/gp/product/B0BXXBDJDJ?&linkCode=ll1&tag=kokkahasan-22&linkId=df7e600968c58c0090cd735681c6732c&language=ja_JP&ref_=as_li_ss_tl">Gatsby５前編ー最新Gatsbyでつくるコーポレートサイト ～基礎の基礎から応用、新機能の導入まで（書籍2,980円）</a>

<br />
<br />

**最新Gatsby５**とmicroCMSを組み合わせてのコーポレートサイト作成手順を解説。《**サイト内検索機能**》をGatsbyバージョン４からの新機能《**Gatsby Functions**》と《**microCMSのqパラメータ**》で実装。また、**SEOコンポーネントをカスタマイズ**して**microCMS APIをツイッターカードに表示**させる**OGPタグ**実装方法も解説。

<a href="https://www.amazon.co.jp/gp/product/B0BXN99WSV?&linkCode=li3&tag=kokkahasan-22&linkId=68f1905bf915f6bd365559a0c65bcdd1&language=ja_JP&ref_=as_li_ss_il" target="_blank"><img border="0" src="//ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&ASIN=B0BXN99WSV&Format=_SL250_&ID=AsinImage&MarketPlace=JP&ServiceVersion=20070822&WS=1&tag=kokkahasan-22&language=ja_JP" ></a><img src="https://ir-jp.amazon-adsystem.com/e/ir?t=kokkahasan-22&language=ja_JP&l=li3&o=9&a=B0BXN99WSV" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />
<a target="_blank" href="https://www.amazon.co.jp/dp/B0BXN99WSV?_encoding=UTF8&qid=&sr=&linkCode=li2&tag=kokkahasan-22&linkId=f02e2f9c50736e3f074b38b4cd73894a&language=ja_JP&ref_=as_li_ss_il">Gatsby５後編ー最新GatsbyとmicroCMSでつくるコーポレートサイト《サイト内検索機能付き》（書籍 2,790円）</a>

<br />

<br /><br />

参考：
[An implementation of PHP's strip_tags in Node.js](https://github.com/ericnorris/striptags)
[ブログ記事ページでの呼び出し](https://takumon.com/2018/09/16/#%E3%83%96%E3%83%AD%E3%82%B0%E8%A8%98%E4%BA%8B%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%A7%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97)
[Adding an SEO Component](https://www.gatsbyjs.org/docs/add-seo-component/)
[Gatsby.jsにreact-helmetを導入してhead要素（メタタグ）をカスタマイズする](https://www.webopixel.net/javascript/1575.html)