19
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-05-10

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要素(メタタグ)をカスタマイズする

19
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?