Front End Programming

Gatsbyブログサイト移行物語~プラグインHelmetでSEO調整~

ReactGatsby

Gatsby記事もこれで8記事目となりました!!表示が早いだけではNG!!!サイトをちゃんと機能させるため、SEOで出力する情報を調整しました。パンくずやページの構造化データやOGP画像なども調整しています。

※FBシェアのcanonilcal属性を修正しました。

※GA4に対応しました。

※ v4に対応しました。

今までのGatsbyの記事と注意点

現在ここまで記載しています。
制作するまでを目標にUPしていくので順を追ったらGatsbyサイトが作れると思います。

  1. インストールからNetlifyデプロイまで
  2. ヘッダーとフッターを追加する
  3. 投稿テンプレにカテゴリやらメインビジュアル(アイキャッチ)追加
  4. ブログ記事、カテゴリ、タグ一覧の出力
  5. プラグインを利用して目次出力
  6. プラグインナシで一覧にページネーション実装
  7. 個別ページテンプレート作成
  8. プラグインHelmetでSEO調整(←イマココ)
  9. CSSコンポーネントでオリジナルページを作ろう!!
  10. 関連記事一覧出力
  11. タグクラウドコンポーネントを作成する
  12. パンくずリストを追加する
  13. 記事内で独自タグ(コンポーネント)を使えるようにする

このシリーズはGithub・gatsby-blogに各内容ブランチごとで分けて格納しています。

今回のソースはseoブランチにあります。

このシリーズではテーマGatsby Starter Blogを改造

この記事は一番メジャーなテンプレート、「Gatsby Starter Blog」を改造しています。同じテーマでないと動かない可能性があります。

プラグインHelmetとは?

Helmetは「Gatsby Starter Blog」に最初からインストールされているプラグインで、head内にメタタグをまとめて出力してくれます。

今回ファイルの追加はとくにしません。

基本的には既存ファイルのsrc/components/seo.jsを改造します。

/ (プロジェクトディレクトリー)
  ├ static/
  |  └ images/
  |    ├ ogp.png(汎用ogp画像)
  |    └ logo.png(サイトのロゴ )
  └ src/
    |  ├ thumbnail/ 記事のキービジュアルが格納されている
    |  └ templates/
    |    └ blog-post.js
    └ components/
          └ seo.js(head内にmetaタグなどを出力する)

OGP画像が取得できるように下準備

Facebook、twitter、スマホでのGoogle検索一覧にも(運が良ければ)サムネイル画像が表示できるようにします。

<!-- Facebookなど -->
<meta property="og:image" content=" サムネイル画像の URL" />
<!-- twitter -->
<meta property="twitter:image" content="サムネイル画像の URL">
<!-- スマホでのGoogle検索一覧 -->
<meta name="thumbnail" content="サムネイル画像の URL">

OGP画像は以下のように出しわけします。

基本のアイキャッチの設定のあるblog-list.jsを改造する

画像のパスを取得するため、GraphQLのスキーマ allFilepublicURL を追加します。

blog-list.js
export const pageQuery = graphql`
  query BlogPostBySlug(
    $id: String!
    $previousPostId: String
    $nextPostId: String
    $hero: String
  ) {
    site {
      siteMetadata {
        title
      }
    }
    allFile(
      filter: {
        relativePath: { eq: $hero }
        sourceInstanceName: { eq: "images" }
      }
    ) {
      edges {
        node {
          publicURL          relativePath
          childImageSharp {
            gatsbyImageData(
              width: 640
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
    # 省略
  }
`

<Seo/>コンポーネントに値を付与します。ogp画像のパスを取得し、seo.jsに値を渡します。

blog-list.js
// 省略
const BlogPostTemplate = ({ data, location }) => {
  // 省略

  const ogpImg = data.allFile.edges[0].node.publicURL

  return (
    <Layout location={location} title={siteTitle}>
    <Seo
        title={post.frontmatter.title}
        description={post.frontmatter.description || post.excerpt}
        // ブログ記事のみ追加
        img={ogpImg}        // すべてのテンプレに追加
        location={location}      />

ページを出力しているすべてのテンプレにlocation属性を追加します。

seo.jsを変更します。

seo.js
import * as React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"

// img,location追加
const Seo = ({ description, lang, meta, title, img, location }) => {  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            siteUrl
            social {
              twitter
              instagram
            }
          }
        }
      }
    `
  )

  const metaDescription = description || site.siteMetadata.description
  const defaultTitle = site.siteMetadata?.title
  // 画像パス取得
  const imgPath = `${site.siteMetadata.siteUrl.replace(/\/$/, "")}${img ? img : "/images/ogp.png"}`  // トップページか判定
  const rootPath = `${__PATH_PREFIX__}/`  const isRootPath = location.pathname === rootPath
  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}
      meta={[
        {
          name: `description`,
          content: metaDescription,
        },
        {          name: `thumbnail`,          content: imgPath,        },        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `${isRootPath ? "website" : "webpage"}`,
        },
        {          property: `og:url`,          content: imgPath,        },        {
          name: `twitter:card`,
          content: `summary_large_image`,
        },
        {
          name: `twitter:creator`,
          content: site.siteMetadata?.social?.twitter || ``,
        },
        {
          name: `twitter:title`,
          content: title,
        },
        {
          name: `twitter:description`,
          content: metaDescription,
        },
        {          property: `twitter:image`,          content: imgPath,        },      ].concat(meta)}
    />
  )
}

Seo.defaultProps = {
  lang: `ja`,  meta: [],
  description: ``,
}

Seo.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
}

export default Seo

補足。og:typeをトップページ以外を判定してwebpageを出力しています。

{
  property: `og:type`,
  content: `${isRootPath ? "website" : "webpage"}`,
},

Twitterカードもサイズをお好みで変えてください。

{
  name: `twitter:card`,
  content: `summary_large_image`,
},

日本語にするのも忘れずに。

Seo.defaultProps = {
  lang: `ja`,
  meta: [],
  description: ``,
}

URLの正規化をする

このサイトは投稿が増えると、一覧ページが無限に増えます。
類似ページが何個もあると検索エンジンに認識されるのはSEO的にもよろしくないので、カノニカル属性を使ってURLを正規化します。
※ FBシェアにおけるパラメーターの不具合を発見しcanonilcal属性を修正しました。

<link rel="canonical" href="URL"/>

canonical属性とは?

canonical(カノニカル)属性とは、Google、Yahoo!、MSNなどの大手検索エンジンがサポートするURLを正規化するためのタグです。 同一のコンテンツが複数のURLで閲覧できる状態になっている場合、検索エンジンがインデックスするべきURLを統一させておく必要があります。 SEO対策において必須の内部対策です。

URLに/page/数字/を含む場合は除去し、URLをcanonical属性として出力します。

seo.js
  // 省略
  const isRootPath = location.pathname === rootPath

  let blogUrl = location ? location.href : site.siteMetadata.siteUrl
  // 一覧の2ページ目以降は除去
  blogUrl = String(blogUrl).replace(/page\/([0-9])+\//, "")
  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}
      meta={[
        // 省略
        {
          property: `twitter:image`,
          content: imgPath,
        },
      ].concat(meta)}
    >
      <link rel="canonical" href={blogUrl} />    </Helmet>
  )
}
// 省略

構造化データ(JSON-LD)追加

構造化データ(JSON-LD)を追加します。トップページはtypeをWebSiteそれ以外はWebPageとします。

またブログが記事のもののみ、BlogPosting(この投稿はブログだよ!!って検索エンジンに教えます)の構造化データを追加します。

ページのデータ

seo.jsに構造化データのJSONを出力できるようにします。markdownファイルも公開日はもちろん、更新日(modifieddate)が取得できるようにしておきます。

何が大事って、更新されて、手が入っているページかです。古い記事でも更新していることをアピールできることが大事です。

mdファイル
---
title: 記事サンプル
date: 2020-11-26
modifieddate: 2020-11-26pagetype: blog
hero: thumbnail/entry01.jpg
description: この記事はテスト投稿です
cate: seo
tags: ['Gatsby', 'React']
---

JSONデータを追記して、出力します。

seo.js
import * as React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
//記述方法変更
const Seo = props => {
  const {
    description,
    img,
    location,
    lang,
    title,
    meta,
    type,
    date,
    modified,
  } = props
  // 省略

  const metaDescription = description || site.siteMetadata.description
  const defaultTitle = site.siteMetadata?.title
  const imgPath = `${site.siteMetadata.siteUrl.replace(/\/$/, "")}${
    img ? img : "/images/ogp.png"
  }`
  const rootPath = `${__PATH_PREFIX__}/`
  const isRootPath = location.pathname === rootPath

  let blogUrl = location ? location.href : site.siteMetadata.siteUrl
  // ページネーションのある箇所は削除
  blogUrl = String(blogUrl).replace(/page\/([0-9])+\//, "")

  // 構造化データの追加
  // 執筆者情報
  const author = [
    {
      "@type": "Person",
      name: site.siteMetadata.author.name,
      description: site.siteMetadata.author.summary,
      url: site.siteMetadata.siteUrl,
      sameAs: [
        site.siteMetadata.social.twitter,
        site.siteMetadata.social.instagram,
      ],
    },
  ]

  // 公開する組織など
  const publisher = {
    "@type": "Organization",
    name: site.siteMetadata.title,
    description: site.siteMetadata.description,
    logo: {
      "@type": "ImageObject",
      url: `${site.siteMetadata.siteUrl}images/logo.png`,
      width: 72,
      height: 72,
    },
  }

  // JSON-LDの設定
  let jsonLd = [
    {
      "@context": "http://schema.org",
      "@type": isRootPath ? "webSite" : "webPage",
      inLanguage: "ja",
      url: blogUrl,
      name: title,
      author,
      publisher,
      image: imgPath,
      description: metaDescription,
    },
  ]
  if (type === "blog") {
    const article = {
      "@context": "http://schema.org",
      "@type": "BlogPosting",
      url: blogUrl,
      name: title,
      headline: title,
      image: {
        "@type": "ImageObject",
        url: imgPath,
      },
      description: description,
      datePublished: date,
      dateModified: modified,
      mainEntityOfPage: {
        "@type": "WebPage",
        "@id": blogUrl,
      },
      author,
      publisher,
    }
    jsonLd = [...jsonLd, article]
  }

  return (
    <Helmet
      {/* 省略 */}
    >
      <link rel="canonical" href={blogUrl} />
      {/* 構造化データ出力 */}
      <script type="application/ld+json">
        {JSON.stringify(jsonLd)}
      </script>
    </Helmet>
  )
}

// 省略

export default Seo

パンくずリスト

パンくずリストの構造化データも作成します。先ほどページ用に作ったJSONデータを格納した変数jsonLdと結合させます。

現時点では目に見えたパンくずリストはありませんが(後日記事書きました)、SEO上重要です。検索画面で親ページが日本語で表示されます。

Google検索でのパンくずリスト

ブログ・カテゴリ・タグのテンプレにtypeを追加します。こちらはカテゴリのテンプレのSeoコンポーネントです。

属性がちゃんとセットされているか確認しておきましょう。

{/*ブログ*/}
<Seo
  title={title}
  location={location}
  type="blog-list"
  discription="ブログ一覧記事です。"
/>
{/*カテゴリ*/}
<Seo
  title={cate.name}
  location={location}
  type="cate-list"
  discription={`${cate.name}一覧記事です。${cate.description}`}
/>
{/*タグ*/}
<Seo
  title={tag}
  location={location}
  type="tag-list"
  discription={`${tag}一覧記事です。`}
/>

トップページ以外にパンくずの構造化データを追加します。

if (!isRootPath) {
  let breadCrumbList
  const home = {
    "@type": "ListItem",
    "position": 1,
    "item": `${site.siteMetadata.siteUrl}/`,
    "name": "ホーム",
  }
  const blogList = {
    "@type": "ListItem",
    "position": 2,
    "item": `${site.siteMetadata.siteUrl}/blogs/`,
    "name": `ブログ一覧`,
  }

  if (type === "blog" || type === "cate-list" || type === "tag-list") {
    breadCrumbList = [
      home,
      blogList,
      {
        "@type": "ListItem",
        "position": 3,
        "item": blogUrl,
        "name": title,
      }
    ]
  } else if (type === "blog-list") {
    breadCrumbList = [
      home,
      blogList
    ]
  } else {
    breadCrumbList = [
      home,
      {
        "@type": "ListItem",
        "position": 2,
        "item": blogUrl,
        "name": title,
      }
    ]
  }
  jsonLd = [
    ...jsonLd,
    {
      "@context": "http://schema.org",
      "@type": "BreadcrumbList",
      "itemListElement": breadCrumbList,
    },
  ]
}

コードがきちんと動いているか、エラーがないかなどはリッチリザルトテストで確認しましょう。

entry418 6

プラグインを使ってGoogleAnalyticsを追加(2021年4月6日追記!)

プラグインを使ってGoogleアナリティクスで計測できるようにします。

gatsby-plugin-google-analyticsじゃなく、gatsby-plugin-google-gtagを使います!

※ 通常のGAタグのIDを使い、GA4と連携させます。
コマンド
npm install gatsby-plugin-google-gtag

プラグインをインストールしたら、gatsby-node.jsに追記します。

Googleアナリティクスはプラグインの一番上の方に追記してください。

トラッキングIDは直接書くこともできますが、Netlifyであれば環境変数で設定する方がベターです。

gatsby-node.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-google-gtag`,
      options: {
        trackingIds: [
          process.env.GOOGLE_ANALYTICS_TRACKING_ID,//トラッキングID
          process.env.GOOGLE_ADSENSE_ID,//Adsenseもまとめて入れられる!!
        ],
        pluginConfig: {
          head: true,//headerに追記
        },
      },
    },
  ],
}

環境変数はNetlifyの管理画面から設定したいプロジェクトを選び、deploy build > environmentを開きます。

あとはEnvironment valiables(環境変数)を登録するだけです。

Netlifyの管理画面からプロジェクトを選択し、deploy build environmentから設定

process.env.プラス設定した変数名で呼び出せます。

process.env.GOOGLE_ANALYTICS_TRACKING_ID


念の為analyticsのトラッキングコードIDの調べ方です。

Analyticsの管理画面から確認できます。

analyticsのトラッキングコードからIDのみ登録

ここまでできたらあとはディプロイしたらanalyticsでトラッキングできるようになります。

Search consoleと連携

search consoleも連携しましょう。メタタグにgoogle-site-verificationを追加するのがカンタンです。

メタタグはsearch consoleの管理画面 > 設定 > 所有者の確認 >「HTMLタグ」から調べることができます。

search consoleの管理画面 > 設定 > 所有者の確認から「HTML タグ」
<meta name="google-site-verification" content="" />
seo.js
// ~ 省略 ~

return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      titleTemplate={pageName}
      meta={[

    // ~ 省略 ~

        {
          name: `google-site-verification`,
          content: ``,
    },

        // ~ 省略 ~

プラグインを使ってサイトマップを出力

最後にサイトマップの出力をしましょう。
これでsearch consoleから、いつでもサイトマップを取得できるようになります。

gatsby-plugin-sitemap

コマンド
npm install gatsby-plugin-sitemap

siteMetadata内のsiteUrlに必ずドメインを設定してください。

gatsby-config.js
module.exports = {
  siteMetadata: {
    siteUrl: `https://ginneko-atelier.com`,  },
  plugins: [`gatsby-plugin-sitemap`]
}

私のケースではページネーションで生成されたページや404をサイトマップから除外しました。

ルートに出力するように設定しました。

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-sitemap`,
      options: {
        output: `/`,
        excludes: [
          `/blogs/page/*`,
          `/404?(.*)`,
          `/blogs/*/page/*`,
          `/blogs/tags/*/page/*`,
        ],
      },
    }
  ]
}

メインのサイトマップファイルはsitemap-index.xmlとして出力されます。

ここにsitemap-0.xml、sitemap-1.xmlという感じで追加されていきます。

Search Consoleからサイトマップを登録しておきます。

サチコから読み込んでおく

まとめ・SEO情報が更新されたらブログを書くのが楽しくなる!

昨年は突貫でかなりやばいコードを書いていて申し訳な気持ちですが少しまともになりましたw。

FBやTwitterでのシェアしてOGP画像が表示されるようになりました。

皆さんのコーディングライフの一助となれば幸いです。

次の記事は「CSSコンポーネントでオリジナルページを作ろう!!」です。

最後までお読みいただきありがとうございました。

お読みいただきありがとうございます。
「銀ねこアトリエ」をより良いブログにするために是非応援してください!

銀ねこアトリエを応援する