「Gatsby x TypeScript」でブログを作りました (3. サードパーティーの使う上での豆知識)

Gatsbyでブログを作った人の最初の記事と言えば、だいたい「Gatsbyでブログ作りましたー」でしょう。なので許してください。

「Gatsby x TypeScript」でブログを作る時に役に立った情報などを自分用のメモというか棚卸しというかまとめていきたいと思います。

このブログ一見シンプルに見えて、ワードプレスで作られたブログと引けを取らないくらい実はいろんな機能が盛り込まれてます。

  • Disqusによるブログコメント投稿機能
  • Algoliaによる全文検索機能
  • Mailgunによる自動返信メール(お問い合わせ時)
  • Google reCAPTCHA v3 (サーバーサイドバリデーションもあり)
  • Google Analytics
  • Iframelyによる記事へのiframeの埋め込み機能
  • JSONLDを使った構造化マークアップ(SEO対策)

全4回に分けて、gatsbyのプラグインで一瞬で実装が終わったものもあればそうでないものもあるので実装する上で参考になったサイトと要点を軽くまとめなながら紹介できたらいいかと思います。

「Gatsbyでのブログ作成手順」は他のブログにあるので手順に関しては紹介しません。

この投稿では 3.サードパーティーの使う上での豆知識 について書こうと思います。

使い方は他のブログ記事で紹介されてますからね。

Algoliaを使う上で知っておきたい事

Algoliaを知らない人のために

codesandboxでの作例がここにあります。これでイメージを固めてください。

世界中の有名サイトで使われている全文検索エンジンです。

環境変数を使ってindexの更新を制御する

gatsybyでアルゴリアを使う時、gatsby-plugin-algoliaプラグインを使って、だいたいこんな感じで設定すると思うのですが、

gatsby-config.js
const queries = require('./src/utils/algolia')
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-algolia`,
      options: {
        appId: process.env.GATSBY_ALGOLIA_APPID,
        apiKey: process.env.GATSBY_ALGOLIA_API_KEY,
        indexName: process.env.GATSBY_ALGOLIA_INDEXNAME,
        queries
      }
    }
  ]
}

これでは yarn build する度にAlgoliaのindexを更新しようとしてしまいます。 投稿に更新がない時にindexの更新がかかってしまうのは無駄です。

そこでindexの更新を環境変数を使って制御する方法をお勧めします。 つまりこんな感じです。

gatsby-config.js
const queries = require('./src/utils/algolia')
module.exports = {
  plugins: [
    ...(process.env.GATSBY_ALGOLIA_UPDATE_INDEX === 'true' ? [
      {
        resolve: `gatsby-plugin-algolia`,
        options: {
          appId: process.env.GATSBY_ALGOLIA_APPID,
          apiKey: process.env.GATSBY_ALGOLIA_API_KEY,
          indexName: process.env.GATSBY_ALGOLIA_INDEXNAME,
          queries
        }
      }
    ] : [])
  ]
}

GATSBY_ALGOLIA_UPDATE_INDEX = true yarn build としないとindexが更新されないように なり、Algoliaへの無駄なリクエストを防ぐ事ができます。

queriesで取得してくる結果にはidが必ず必要

queries はこんな感じで自分は用意しているのですが、

src/utils/algolia.js
const fetchAllPosts = `
{
  allMarkdownRemark {
    edges {
      node {
        id
        fields {
          slug
        }
        frontmatter {
          title
          category
          subcategory
          tags
          createdOn
          updatedOn
        }
        excerpt
      }
    }
  }
}
`

const unnest = node => {
  const { fields, frontmatter, ...rest } = node

  return {
    ...fields,
    ...frontmatter,
    ...rest
  }
}

const queries = [
  {
    query: fetchAllPosts,
    transformer: ({ data }) =>
      data.allMarkdownRemark.edges.map(edge => edge.node).map(unnest)
  }
]

module.exports = queries

fetchAllPosts のクエリで id を書かなかったら、gatsby-plugin-algoliaが発行したクエリの結果を処理できずに以下のようなエラーが出ました。

Algolia: 1 queries to index
Algolia: query 0: executing query
⠹ onPostBuild
not finished onPostBuild - 0.317s
error Command failed with exit code 1.

本当はgatsbyレポーターがキャッチしてエラーより詳細なエラーを吐いてくれると思うのですが、そうなってなかったので この原因を調べるためにはコードを読むしかなかったです。

algoliaのindexでは必ずobjectIDが振られるようになってます。

algolia-required-objectID.png

コードを見てみたら理由が分かって、「id」を使って、indexに必要な「objectID」を生成していました。

https://github.com/algolia/gatsby-plugin-algolia/blob/ca6894feed667c00a4fcb452f0e800ec1d7e50f2/gatsby-node.js#L102-L105

gatsby-plugin-algolia/gatsby-node.js
    const objects = (await transformer(result)).map((object) => ({
      objectID: object.objectID || object.id,
      ...object,
    }));

「id」もしくは「objectID」が必要って事ですが、allMarkdownRemark には「id」しかないので「id」を取得するように書く必要があります。

Mailgunを使った自動メール送信機能を作る時に知っておきたい事

よくお問い合わせした時って、自動返信メールがきますよね? それをMailgunを使って実装する方法について軽くコツを紹介しておきます。

基本この記事を参考にして設定していけばいいです。

Mailgunの使い方に関してはこちらのQiitaの記事がわかりやすかったです。

ローカルで9000ポートにnetlify functionsサーバーを立てる方法

netlify functions を使って send-contact-email を実装をしていくのですがローカルでの動作確認でつまづくと思います。

こちらの記事では動作確認の時に

netlify functions:invoke send-contact-email --no-identity --payload '{"contactEmail" : "jenna@example.com", "contactName" : "Jenna", "message" : "hello world from a function!"}'

のように実行すればいいとあったのですが実際にやってみるとこんなエラーが出ます。

payload.json
{
  "name": "山田太郎",
  "email": "yukihirop@example.com",
  "subject": "ブログの記事で質問があります----",
  "message": "テストメッセージ\nテストメッセージ"
}
$ netlify functions:invoke send-contact-email --port 9000 --no-identity --payload ./payload.json
ran into an error invoking your function
FetchError: request to http://localhost:9000/.netlify/functions/send-contact-email failed, reason: connect ECONNREFUSED 127.0.0.1:9000
    at ClientRequest.<anonymous> (/Users/yukihirop/.nodenv/versions/12.7.0/lib/node_modules/netlify-cli/node_modules/node-fetch/lib/index.js:1455:11)
    at ClientRequest.emit (events.js:203:13)
    at Socket.socketErrorListener (_http_client.js:399:9)
    at Socket.emit (events.js:203:13)
    at emitErrorNT (internal/streams/destroy.js:91:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)
    at processTicksAndRejections (internal/process/task_queues.js:77:11) {
  message: 'request to http://localhost:9000/.netlify/functions/send-contact-email failed, reason: connect ECONNREFUSED 127.0.0.1:9000',
  type: 'system',
  errno: 'ECONNREFUSED',
  code: 'ECONNREFUSED'
}

それもそうで、send-contact-emailファンクションを9000ポートで動かしてないからです。 動かし方に関しては書いてなかったので実際に動かし方を書いておきます。

netlify-lambda を使います。

yarn -D add netlify-lambda を実行して netlify-lambda を入れます。

でインストールしたらこのようなスクリプトを書きます。

package.json
"scripts": {
  "netlify:send-contact-email:dev": "netlify-lambda serve src/functions/send-contact-email"
}

名前が少し長いですが、名前はなんでもいいです。ポートの指定をしなくても9000ポートになってます。 自分は、src/functions/send-contact-emailディレクトリにsend-contact-emailを作ってます。

$ yarn netlify:send-contact-email:dev
yarn run v1.19.0
$ netlify-lambda serve src/functions/send-contact-email
netlify-lambda: Starting server
Hash: e0f09b0eb79ec3dd76df
Version: webpack 4.43.0
Time: 5943ms
Built at: 2020-06-06 18:00:34
                Asset      Size  Chunks             Chunk Names
send-contact-email.js  1.11 MiB       0  [emitted]  send-contact-email
Entrypoint send-contact-email = send-contact-email.js
 [12] external "fs" 42 bytes {0} [built]
 [13] external "path" 42 bytes {0} [built]
 [23] external "crypto" 42 bytes {0} [built]
 [37] /Users/yukihirop/JavaScriptProjects/blog/node_modules/mailgun-js/lib/attachment.js 1.36 KiB {0} [built]
 [93] ./send-contact-email.js 2.02 KiB {0} [built]
 [94] /Users/yukihirop/JavaScriptProjects/blog/node_modules/dotenv/lib/main.js 2.93 KiB {0} [built]
 [95] /Users/yukihirop/JavaScriptProjects/blog/node_modules/mailgun-js/lib/mailgun.js 5.78 KiB {0} [built]
 [96] /Users/yukihirop/JavaScriptProjects/blog/node_modules/tsscmp/lib/index.js 1.15 KiB {0} [built]
 [98] /Users/yukihirop/JavaScriptProjects/blog/node_modules/mailgun-js/lib/request.js 10.7 KiB {0} [built]
[246] /Users/yukihirop/JavaScriptProjects/blog/node_modules/mailgun-js/lib/build.js 3.12 KiB {0} [built]
[250] /Users/yukihirop/JavaScriptProjects/blog/node_modules/mailgun-js/lib/schema.js 18.2 KiB {0} [built]
[251] ./md/index.js 1.95 KiB {0} [built]
[252] /Users/yukihirop/JavaScriptProjects/blog/node_modules/markdown-it/index.js 52 bytes {0} [built]
[307] /Users/yukihirop/JavaScriptProjects/blog/node_modules/markdown-it-br/index.js 1.8 KiB {0} [built]
[308] /Users/yukihirop/JavaScriptProjects/blog/node_modules/markdown-it-small/index.js 2.58 KiB {0} [built]
    + 295 hidden modules
Lambda server is listening on 9000

これで9000ポートにsend-contact-emailファンクションが動いているサーバーが立ち上がりました。

では先ほど実行できなかったコマンドを別のタブを開いて実行してみましょう。

ターミナルからお問い合わせをする

$ netlify functions:invoke send-contact-email --port 9000 --no-identity --payload ./payload.json
自動返信メッセージが正常に送信されました

この結果は正しく自動返信メールが送れたらそのように返すと自分で設定したのでそのように返ってきているわけですが、 成功してます。

send-contact-email.png

画面からお問い合わせする

画面から問い合わせをする時は、localhost:8000 から localhost:9000 にPOSTリクエストを送ることになるので 何も設定しなければcorsの問題が発生して動きません。

netlify-functions(send-contact-email)にリクエストを送る部分の実装
const autoSend = await axios.post("http://localhost:9000/.netlify/functions/send-contact-email", { name, email, subject, message })

この状態で問い合わせしたらcorsの問題が発生して問い合わせに失敗します。

contact-failed.png

devtool-cors.png

ではどうすればいいのかというと、proxyを使いましょう公式ドキュメントに書いてありました。しかもドンピシャでnetlify-functionsへのproxyの設定の仕方が書いてあります。まず必要なものをyarn addで入れて

yarn -D add http-proxy-middleware
gatsby-config.js
const { createProxyMiddleware } = require("http-proxy-middleware")

module.exports = {
  developMiddleware: app => {
    app.use(
      "/.netlify/functions/",
      createProxyMiddleware({
        target: "http://localhost:9000"
      })
    )
  },
}

これで解決します。

Google reCAPTCHA (v3) を使う上で知っておきたい事

codeep/react-recaptcha-v3を使ってクライアントサイドの実装は一瞬で終わります。問題はサーバーサイドです。

まぁサーバーサイドなしでtokenが発行されたら良しとしてあるブログも多々ありますが、サーバーサイドでtokenをverifyして scoreを出してそれでロボットかどうかを判定する機能なのであまりよくないでしょう。

サーバーサイドの実装はやはり、netlify functions を使って実装したが楽です。

実装例がfirebaseの例ですがgoogleblogの6.reCAPTCHAのレスポンスを検証するCloud Functionを作るにあったので紹介しておきます。

これに従ってやればいいです。

ただ一つ注意点があって、googleblogでは

exports.checkRecaptcha = functions.https.onRequest((req, res) => {
  // ここに処理をかく
}

となってますが、netlify functionsのラムダ関数なので形式は以下の形式にしないといけません。

src/functions/google-recaptcha-v3.js
exports.handler = async (event) => {
  // ここに処理をかく
}

Mailgunを使った自動送信メール機能の説明をした時に設定したようにローカルで動かすためのコマンドを用意しておいたがいいでしょう。

package.json
"scripts": {
  "netlify:google-recaptcha-v3:dev": "netlify-lambda serve src/functions/google-recaptcha-v3"
}

以上です。

Iframelyを使う上で知っておきたい事

Iframelyとはtwitterとかを埋め込むためのサービスですが、TypeScriptを使っている場合、 よく紹介されている方法が使えないので代わりの方法を紹介しておきます。

よく紹介されている方法|ブログに外部コンテンツを良い感じに埋め込むならIframelyがオススメ!|mono blog

ですがこの方法を使ってやると次のようなエラーが出てscriptタグでhttps://cdn.iframe.ly/embed.jsが読み込まれません。

TypeError: window.iframely.load is not a function

React.hooksを使ってこの問題を解決することができます。 要は、window.iframely を使わないようにすればいいだけです。

src/components/atoms/Iframely/index.tsx
import React, { useEffect } from 'react'

const IFRAMELY_URL = 'https://cdn.iframe.ly/embed.js'
const IFRAMELY_ID = 'iframely-embed-script'

// [ref]  
// https://github.com/tterb/gatsby-plugin-disqus/blob/master/src/components/Disqus.jsx#L39-L52
const Iframely = () => {
  useEffect(
    () => {
      const loadInstance = () => {
        if (typeof window !== 'undefined' && window.document) {
          const script = window.document.createElement('script')
          const parent = window.document.body
          script.async = true
          script.src = IFRAMELY_URL
          script.id = IFRAMELY_ID
          parent.appendChild(script)
          return script
        }
      }

      const cleanInstance = () => {
        const parent = window.document.body
        const script = window.document.getElementById(IFRAMELY_ID)
        if (script) {
          parent.removeChild(script)
        }
      }

      if (typeof window !== 'undefined' && window.document) {
        loadInstance()
      }

      return () => {
        cleanInstance()
      }
    },
    []
  )

  return <></>
}

export default Iframely

使い方は、iframelyを使いたい場所で <Iframely /> とするとbodyの中で<script async src="https://cdn.iframe.ly/embed.js" />が設定されてIframelyを使う事ができるようになります。

script-iframely.png

以上です。

まとめ

  • Algoliaを使う上で知っておきたい事

    • Algoliaのindex更新は環境変数で制御して無駄なリクエストを減らす方法を説明しました。
    • onPostBuildでAlgoliaがexit 1を返したら、データにidがない事を疑ったがいいと説明しました。
  • Mailgunを使った自動メール送信機能を作る時に知っておきたい事

    • ローカルホストで9000ポートにnetlify functionsサーバーを立てる方法を説明しました。
    • ターミナルから自動メール送信機能を使う方法を説明しました。
    • 画面から自動メール送信機能(お問い合わせ)を使う時に注意したい事を説明しました。

      • http-proxy-middlewareを使って9000ポートへproxyする設定が必要
  • Google reCAPTCHA(v3)を使う上で知っておきたい事

    • firebaseだけどgoogleblogにやり方がある事を説明しました。
    • netlify functionsなのでexportsする関数の形式の違いだけ注意点があると説明しました。
  • Iframelyを使う上で知っておきたい事

    • Gatsby x TypeScriptなブログではよく紹介されている方法は使えないと説明しました。

      • 代わりの方法に関してコード付きで説明しました。

以上です。最後は4.まとめです。

yukihirop

この記事を書いた人

フロントエンド兼バックエンドなWEBエンジニアです。 既存の考え方に囚われずに自由にツールを作るのが好きです。

いつかBioに「Creator of ...」と書けるようなプログラマを目指している人のブログ

関連記事