Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

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



最後に<Helmet />のタグ内にメタデータを配置していく。<Helmet />は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) + '...';
}

宣伝

microCMSのことは除き、Gatsbyの基本とnode APIの扱いについて踏み込んで解説・ハンズオンした電子書籍を上梓しましたので、よろしければお手に取ってみて下さい。

JAMStackを学ぼう Gatsby, React bootstrap, Netlifyでつくる企業サイト: もうレンタルサーバーはいらない ヤー・ビズテック



GatsbyとmicroCMSを組み合わせてのコーポレートサイト作成手順を解説・ハンズオンした続編を上梓しました。どうぞお手に取ってみて下さい。

JAMStackを学ぼう GatsbyとmicroCMSでつくるコーポレートサイト ~WordPressはもう古い~


参考:
An implementation of PHP's strip_tags in Node.js
ブログ記事ページでの呼び出し
Adding an SEO Component
Gatsby.jsにreact-helmetを導入してhead要素(メタタグ)をカスタマイズする

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away