Netlify FunctionsからContentfulAPIリクエストを送ろう【Nuxt.js】
  • 2022.02.19に公開
  • ブログ構築TS
  • 8. NetlifyFunctionsを使った検索機能
  • No.8 / 9

今回達成すること

Contentful APIリクエストを行うFunctionsプロジェクトを作成し、検索機能を完成させます。

記事の最後にはデプロイを行い、本番環境の挙動を確認します。

2022-02-18 19-07-21

検索ページの実装を行う

検索ページに投げられたクエリを取得し、Netlify FunctionsにAPIリクエストを投げるよう設定します。

pages/search.vue
<template>
  <v-container>
    <ui-page-title
      :title="pageTitle"
      :sub-title="pageSubTitle"
    />

    <ol>
      <li
        v-for="(post, i) in posts"
        :key="`post-${i}`"
      >
        {{ post.fields.title }}
      </li>
    </ol>
  </v-container>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { BlogPost } from '~/store/types'

@Component
export default class SearchPage extends Vue {
  layout (): string {
    return 'app'
  }

  pageTitle: string = this.$my.routePageTitle(this.$route.name)
  posts: BlogPost[] = []

  /*
    string型 && 空白ではない場合、クエリの値を返す
    '/search?q=%20'の場合、クエリは空白のstring型と判定されるので空白の場合はfalseを返すようtest()で判定
  */
  get q (): false | string {
    const q: string | (string | null)[] = this.$route.query.q
    return (typeof q === 'string') && (!/^\s+$/.test(q)) && q
  }

  // 検索件数
  get pageSubTitle (): string {
    return (this.q)
      ? `${this.q} の検索結果 ${this.posts.length}件`
      : '検索キーワードを入力してください'
  }

  // postのコンテンツタイプ名を返す
  get postContentType (): string {
    return this.$accessor.post.content.type.post
  }

  /*
    Nuxt ssr & staticの挙動
    asyncData()はgenerateコマンド時に実行され本番環境では稼働しない
    そこでページ内で再実行可能なcreated()でAPIリクエストを行う
  */
  async created (): Promise<void> {
    // 検索APIリクエスト
    // await this.getContentful()
  }

  // Netlify FuncrionsからContentfulにAPIリクエストを行う
  async getContentful (): Promise<void> {
    // クエリが存在する場合
    if (this.q) {
      await this.$axios.$post(
        '/.netlify/functions/search-contentful',
        {
          contentType: this.postContentType,
          query: this.q
        }
      )
        .then(({ items }: { items: BlogPost[] }) =>
          (this.posts = items)
        )
    }
  }
}
</script>
  • get q () ... クエリ文字、もしくはfalseを返す。

    • $route.query.q ... NuxtではURLのクエリを$route.query.<プロパティ名>で取得できる。
    • && ... 左辺がtrueの場合、右辺を実行。 全てがtrueを返す場合、最後の右辺のクエリ文字を返す。
  • get postContentType () ... 下記記事のpost/index.tsで用意したコンテンツタイプの文字列を取得。

    VuexにContentfulAPIレスポンスを保存してVueファイルに表示しよう

  • getContentful () ... Netlify FunctionsにAPIを投げるメソッド。

created()内でAPIリクエストを行う理由

nuxt.config.jsssr: true, target: 'static'モードの場合、本番環境のasyncData()generateコマンド時に実行され、それ以後は実行されません。

なので、created()内でFunctionsにリクエストを行わないと、検索機能が正常に動きません。

注意していただきたいのは、開発環境のasyncData()はサーバーでもクライアントでも実行されます。

デプロイした時に何故か動かなくなるエラーにはまるので、NuxtのモードでasyncData()の挙動は変わると覚えておきましょう。

検索フォームからページ遷移してみよう

検索フォームから検索ページへ遷移してみましょう。

  1. フォームの値が
  2. URLのクエリ?=aaaになり
  3. 検索結果に表示されました。

2022-02-18 13-04-23

ContentfulにリクエストするFunctionsを作成する

ContentfulにAPIリクエストを行うFunctionsプロジェクトを作成しましょう。

プロジェクト名はsearch-contentfulとします。

% yarn functions:create search-contentful

# 以下を選択
❯ TypeScript 
❯ [hello-world] Basic function that shows async/await usage, and response formatting 

...
✔ Installed dependencies for search-contentful
✨  Done in 26.19s.

パラメーターがない場合のレスポンスメソッドを追加する

http-request-handler.tsnoRequestBody()を追加します。

リクエストに検索クエリが付与されていない場合、400のレスポンスを返すメソッドです。

netlify/lib/http-request-handler.ts
export default class HttpRequestHandler {
  ...
  // 追加
  // リクエストのパラメータが存在しない場合のレスポンス
  // statusCode Doc: https://docs.commercetools.com/api/errors
  noRequestBody () {
    return {
      statusCode: 400,
      body: 'No request body'
    }
  }
}

Conetentful APIにリクエストを行う

search-contentful.tsを編集します。

プラグインファイルのcontentful.tsをインポートし、ContentfulへAPIリクエストを投げます。

netlify/functions/search-contentful/search-contentful.ts
import { Handler, HandlerEvent, HandlerContext } from '@netlify/functions'
import { BlogCategory, BlogPost, BlogTag } from '~/store/types'

import lib from '~/netlify/lib'
import client from '~/plugins/contentful'

type Item = BlogCategory | BlogPost | BlogTag
interface RequestBody {
  contentType: string
  query: string
}

export const handler: Handler = async (event: HandlerEvent, _context: HandlerContext) => {
  const requestOrigin: string | undefined = event.headers.origin

  // CORS対応
  if (event.httpMethod === 'OPTIONS') {
    return lib.httpRequest.checkCors(requestOrigin)
  // Not POST 対応
  } else if (event.httpMethod !== 'POST') {
    return lib.httpRequest.notAllowedRequestMethod()
  // event.body: null 対応
  } else if (!event.body) {
    return lib.httpRequest.noRequestBody()
  }

  const headers = lib.httpRequest.corsHeaders(requestOrigin)

  let items: Item[] = []
  const requestBody: RequestBody = JSON.parse(event.body)

  /*
    await client.getEntries({}).catch()でエラーを取得すると、catchを通過し200が返ってしまう。
    そこでtry, catchでエラーを取得しcatch内で処理を止める。
  */
  try {
    console.log('requestBody:', requestBody)

    await client.getEntries({
      content_type: requestBody.contentType,
      query: requestBody.query
      /*
        公開日降順で取得したコンテンツを並び替える場合
        default: item.sys.updatedAtの降順
      */
      // order: '-fields.publishDate'
    })
      .then((entry: { items: Item[] }) =>
        (items = entry.items)
      )
  } catch (error: any) {
    const e: any = JSON.parse(error.message)
    // Functionsのログ出力用
    console.log('Error:', e)
    console.log('StatusCode:', e.status)
    console.log('StatusText:', e.statusText)
    console.log('ErrorDetails:', e.details.errors)

    return {
      statusCode: e.status
    }
  }

  return {
    statusCode: 200,
    headers,
    body: JSON.stringify({
      items
    })
  }
}
  • else if (!event.body) { ... } ... event.bodyにはaxiosから投げられた検索キーワードが入っている。値が存在しない場合はnoRequestBody()メソッドで400エラーを返す。
  • JSON.parse(event.body) ... contentTypequeryプロパティを取得するために、オブジェクトに変換。
  • client.getEntries({})
    • query ... Contentfulリクエスト時にqueryプロパティを付与すると、content_typeで指定したモデルから、キーワードが一致するコンテンツを検索する。
    • order ... コンテンツの並び替えを行う場合に使用。Contentfulのデフォルトは、更新順に並び替えられる。

上記の実装で、Contentful APIが検索キーワードに一致するコンテンツを検索し、items配列に返してくれます。

try, catchでエラー処理を行う理由

await client.getEntries({})でエラーをキャッチすると、処理が終了せずに通過し、200のレスポンスが返ってきます。

// NG
await client.getEntries({
  content_type: requestBody.contentType,
  query: requestBody.query
})
  .then((entry: { items: Item[] }) =>
    (items = entry.items)
  )
  .catch((error: any) => {
    const e: any = JSON.parse(error.message)
    // ここのreturnを通過してしまう
    return {
      statusCode: e.status
    }
  })

try, catch 構文でreturnを返すと、そこで処理が終了し、エラーのステータスコードを正しく返します。

// OK
try {
  ...
} catch (error: any) {
  const e: any = JSON.parse(error.message)
  console.log('Error:', e)
  console.log('StatusCode:', e.status)
  console.log('StatusText:', e.statusText)
  console.log('ErrorDetails:', e.details.errors)

  // ここで処理が止まる
  return {
    statusCode: e.status
  }
}

FunctionsからContentfulへAPIリクエストを行おう

search.vuecreated()メソッドの内のコメントアウトを外します。

pages/search.vue
@Component
export default class SearchPage extends Vue {
  ...
  async created (): Promise<void> {
    // 検索APIリクエスト
    await this.getContentful()
  }
}

Functionsサーバーを起動し、

% yarn functions:dev

クエリに「a」を付けた下記URLに直接アクセスしてみましょう。

  • http://localhost:3000/search?q=a

現状、検索フォームからのリクエストは最初の1回以降動きません。

記事タイトルが表示されました。

2022-02-18 17-40-08

Functionsサーバーを確認すると、クエリが正しく投げられていますね。

ステータスコードも200が返ってきています。

2022-02-18 17-42-54

Functionsのエラーログを確認しよう

続いてエラーの挙動を確認します。

Contentfulのcontent_typeプロパティに+ 'a'を追加し、不正なコンテンツタイプを指定します。

netlify/functions/search-contentful/search-contentful.ts
...
export const handler: Handler = async (event: HandlerEvent, _context: HandlerContext) => {
 ...
  try {
    await client.getEntries({
      // + 'a'を追加
      content_type: requestBody.contentType + 'a',
      ...
    })
  } ...
}

ブラウザから、同じURLをリロードしてください。

Nuxt上ではネットワークエラーが発生し、Functionsサーバーにはエラーログが出力されます。

2022-02-18 17-47-40

ステータスコードは400が返ってきていますね。

エラーを通過せずに、処理が止まっていることが確認できました。

Contentfulのエラーレスポンスの詳細

Contentfulが返す具体的なエラーの内容はe.details.errorsに入っています。

console.log('ErrorDetails:', e.details.errors)

=> ErrorDetails: [ { name: 'unknownContentType', value: 'DOESNOTEXIST' } ]

nameに「不明なコンテンツタイプ」と出力されていますね。

今後エラーを解決する場合はe.details.errorsを確認してください。

エラー発生コードを正常に戻す

確認が取れたらエラー発生コードは元に戻します。

netlify/functions/search-contentful/search-contentful.ts
...
export const handler: Handler = async (event: HandlerEvent, _context: HandlerContext) => {
 ...
  try {
    await client.getEntries({
      // 元に戻す
      content_type: requestBody.contentType,
      ...
    })
  } ...
}

以上でContentfulへの検索APIリクエストの実装は完了です。

検索フォームのAPIリクエスト時に再レンダリングを行う

現在のままでは、一度検索を行った後に再度フォームに値を入力しても検索結果が変わりません。

2022-02-18 18-06-10

Nuxtのデフォルトの挙動は、URLのクエリが変わっただけではページの再レンダリングを行いません。

そこで、クエリの変化とともにページコンポーネントの再レンダリングを行うkey()メソッドを追加します。

pages/search.vue
@Component
export default class SearchPage extends Vue {
  layout (): string {
    return 'app'
  }

  // 追加
  /*
    returnの値が変化した場合にページコンポーネントを再レンダリングする
    Doc: https://nuxtjs.org/docs/components-glossary/key
  */
  key (route: { fullPath: string }): string {
    return route.fullPath
  }
  ...
}
  • route.fullPath ... ここにはクエリも含むパスが入っているので、クエリの変化とともに再レンダリングが実行される。

これで検索フォームの値が変化すると検索APIが実行されるようになりました。

検索フォームの値を初期化する

検索フォームに値を入力したままページ遷移すると値が残っていまいます。

2022-02-18 18-40-07

そこで、検索ページから離れたタイミングでフォームの値を初期化します。

検索ページから離れたタイミングを検知するには、VueRouterのbeforeRouteLeave()メソッドを使用します。

pages/search.vue
@Component
export default class SearchPage extends Vue {
  ...
  async created (): Promise<void> {
    ...
  }

  // 追加
  /*
    コンポーネントを描画するルートがナビゲーションから離れる直前に呼ばれる
    thisでインスタンスにアクセス可能
    Doc: https://router.vuejs.org/guide/advanced/navigation-guards.html#using-the-options-api
  */
  beforeRouteLeave (_to: any, _from: any, next: any) {
    // ページ遷移前にQueryを初期化する
    this.$accessor.getSearchQuery('')
    return next()
  }
  ...
}

これでページ遷移とともに、検索フォームの値が初期化されるようになりました。

ページ遷移を検知するVueRouterメソッド

beforeRouteLeaveの他にも、ページ遷移前後で発火するメソッドがVueRouterには用意されています。

  • beforeRouteEnter ... ルート遷移直前。thisへのアクセス不可。

  • beforeRouteUpdate ... ルート変更直後。thisへのアクセス可。

    詳細: Navigation Guards | Vue Router

本番環境にデプロイしよう

Functionsプロジェクトのsearch-contentfulでは、プラグインファイルのcontentful.tsを使用しています。

このファイルにはCOntentful API Keyの環境変数が使用されているため、本番環境のサーバーにも同じ値を登録しなければなりません。

本番環境で使用するAPI Keyは

  • CTF_SPACE_ID
  • CTF_DELIVERY_API_KEY の2つです。

netlify env:setコマンドで環境変数を登録しましょう。

.envファイルに置いた値と同じものをセットしてください。

% yarn netlify env:set CTF_SPACE_ID <space id>
% yarn netlify env:set CTF_DELIVERY_API_KEY <delivery api key>

netlify env:listで本番環境のサーバーに登録した環境変数一覧が確認できます。

% yarn netlify env:list

.--------------------------------------------------------------------.
|                       Environment variables                        |
|--------------------------------------------------------------------|
|         Key          |                    Value                    |
|----------------------|---------------------------------------------|
| API_URL              | https://demo-blog-v2.netlify.app            |
| CTF_SPACE_ID         | <space id>                                  |
| CTF_DELIVERY_API_KEY | <delivery api key>                          |
'--------------------------------------------------------------------'

Functionsプロジェクトをデプロイし、本番環境の挙動も確認しましょう。

% yarn functions:deploy

ちゃんと動きましたね。

2022-02-18 19-07-21

Netlifyの「functions」メニューから該当のプロジェクトログを確認すると、ログが取れているのがわかります。エラーなく動きました。

2022-02-18 19-08-57

今回の作業は以上です。

% git add -A
% git commit -m "Add search-contentful for Functions project" -m "Finished contentful API request for search page"

まとめと次回

今回は検索機能の実装を行い、本番環境の動作確認を行いました。

それでは次回は、検索結果を分割表示する実装を行います。

あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる。そんな方に向けた単発・短期間メンターサービスを行っています。
独学プログラマのサービス
ブログ構築TSの投稿
1
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #01
Nuxt.jsをローカルPCに立ち上げよう
2
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #02
Nuxt.jsプロジェクトをGitHubにPushしよう
3
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #03
nuxt-property-decoratorのインストールとTypeScriptのセットアップ
1
  • Vuetifyセットアップ
  • /
  • #01
TypeScript環境のNuxt.jsにVuetifyを導入しよう
2
  • Vuetifyセットアップ
  • /
  • #02
VuetifyにカスタムCSSを追加してSASS変数を理解しよう
3
  • Vuetifyセットアップ
  • /
  • #03
VuetifyにカスタムSVGアイコンを追加しよう
1
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #01
Netlify CLIをインストールして本番環境のサイトを作成しよう
2
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #02
netlify.tomlを使ってNuxt.jsをNetlifyに手動デプロイしよう
1
  • Contentfulモデル構築
  • /
  • #01
Contentfulの料金とCommunityプランの無料枠を理解する
2
  • Contentfulモデル構築
  • /
  • #02
Contentfulへ新規会員登録、ロケールの変更、API Keyの発行を行う
3
  • Contentfulモデル構築
  • /
  • #03
Contentful ブログカテゴリーモデルを作成しよう
4
  • Contentfulモデル構築
  • /
  • #04
Contentful カテゴリーモデルに1対1で関連づくblogPostモデルを作成しよう
5
  • Contentfulモデル構築
  • /
  • #05
Contentful ブログ記事に1対多で関連づくplogTagモデルを作成しよう
6
  • Contentfulモデル構築
  • /
  • #06
Contentful カテゴリー・ブログ記事・タグコンテンツを作成しよう
1
  • Nuxt.js×Contentfulセットアップ
  • /
  • #01
Nuxt.js×Contentfulセットアップ。モジュールのインストールからAPI Keyの登録まで
2
  • Nuxt.js×Contentfulセットアップ
  • /
  • #02
Contentful APIリクエストの実行 Nuxt.jsにブログコンテンツを表示しよう
3
  • Nuxt.js×Contentfulセットアップ
  • /
  • #03
ContentfulAPIをNetlifyにデプロイしよう【Nuxt FullStaticのasyncDataとfetch】
1
  • Vuex×TypeScriptセットアップ
  • /
  • #01
Vuexの型付け vuex-module-decoratorsとnuxt-typed-vuexどちらを使用するか
2
  • Vuex×TypeScriptセットアップ
  • /
  • #02
nuxt-typed-vuexのインストールとセットアップ。Vuexの型定義と呼び出し方
3
  • Vuex×TypeScriptセットアップ
  • /
  • #03
VuexにContentfulの型定義ファイルとnuxtServerInitを追加しよう
4
  • Vuex×TypeScriptセットアップ
  • /
  • #04
VuexにContentfulAPIレスポンスを保存してVueファイルに表示しよう
1
  • コンテンツページ構築
  • /
  • #01
ブログアプリのページ設計とNuxt.jsの動的ルーティングについて理解しよう
2
  • コンテンツページ構築
  • /
  • #02
カテゴリーのコンテンツページを作成しよう【Nuxt.js×Contentful】
3
  • コンテンツページ構築
  • /
  • #03
カテゴリーに関連付くブログ記事一覧を表示しよう【Nuxt.js×Contentful】
4
  • コンテンツページ構築
  • /
  • #04
injectを使用して共通エラー処理メソッドを作成しよう【Nuxt×TypeScript】
5
  • コンテンツページ構築
  • /
  • #05
NuxtChildを使用してブログ記事ページを作成しよう【Nuxt.js×TypeScript】
6
  • コンテンツページ構築
  • /
  • #06
タグ一覧ページとタグ関連づく記事一覧を表示しよう【Nuxt.js×TypeScript】
7
  • コンテンツページ構築
  • /
  • #07
プライバシーポリシーページを作成しよう【Nuxt.js×TypeScript】
8
  • コンテンツページ構築
  • /
  • #08
@nuxtjs/i18nのインストールとセットアップ。ページタイトルの翻訳化【TypeScript】
1
  • NetlifyFunctionsを使った検索機能
  • /
  • #01
Netlify Functionsを使ってクエリを返す関数を作成しよう【Nuxt.js×TypeScript】
2
  • NetlifyFunctionsを使った検索機能
  • /
  • #02
Netlify Functionsプロジェクトをデプロイしよう【Nuxt.js×TypeScript】
3
  • NetlifyFunctionsを使った検索機能
  • /
  • #03
Nuxt.js × axiosセットアップ Netlify Functionsにリクエストを行う準備をしよう
4
  • NetlifyFunctionsを使った検索機能
  • /
  • #04
オリジン•CORS•プリフライトリクエストを理解する【Nuxt.js×Netlify Functions】
5
  • NetlifyFunctionsを使った検索機能
  • /
  • #05
Netlify Functionsを使ってフォームバリデーション機能を構築しよう【Nuxt.js】
6
  • NetlifyFunctionsを使った検索機能
  • /
  • #06
ツールバーに表示する検索フォームを作成しよう【Nuxt.js×TypeScript】
7
  • NetlifyFunctionsを使った検索機能
  • /
  • #07
検索ページを作成しよう【Vue propsとTypeScriptの書き方 解説】
8
  • NetlifyFunctionsを使った検索機能
  • /
  • #08
Netlify FunctionsからContentfulAPIリクエストを送ろう【Nuxt.js】
9
  • NetlifyFunctionsを使った検索機能
  • /
  • #09
検索ページに「もっと見る」ボタンを実装しよう【Nuxt.js×TypeScript】
1
  • ブログMarkdown対応
  • /
  • #01
@nuxtjs/markdownitのインストールとセットアップ【Nuxt.js×TypeScript】
2
  • ブログMarkdown対応
  • /
  • #02
Nuxt.js×markdown-it 外部リンクを別タブで開くプラグインを追加しよう
3
  • ブログMarkdown対応
  • /
  • #03
Nuxt.js×markdown-it 内部リンクをVueRouterで高速にページ遷移しよう
4
  • ブログMarkdown対応
  • /
  • #04
Nuxt.js×markdown-it アンカーリンクとブログ目次を自動生成しよう
独学プログラマ
独学でも、ここまでできるってよ。
CONTACT
Nuxt.js制作のご依頼は下記メールアドレスまでお送りください。