📖

Nuxt3+CloudflarePages+Newtでブログのプレビュー機能を作ろうとしてハマった件

2023/03/20に公開

はじめに

筆者は最近、Nuxt3で遊んでいます。
今回は、Nuxt3+Newtでブログを作成し、それをCloudflarePagesへデプロイしようとした際に発生した問題に盛大にハマり2日間を費やした話を備忘録として残します。

ことの始まり

筆者は最近「Nuxt3で作ったJamstackなサイトでブログを運用する場合、プレビュー機能の実装をどうすべきか?」について悩んでいました。ヘッドレスCMSによっては簡易的なプレビュー機能を持つものもありますが、やはりWordPressを使用しているときと同じ使用感で、本番のデザインでプレビューしたいと思ったためです。

調べるうちに 「プレビュー用のページをSSRで実装する」 という方法にたどり着きました。
こうすることで、記事取得のためのAPIキーを隠蔽することが可能であると…

こうしてプレビュー用のページをSSRで実装してみることにしました。

Nuxt3+Newtでブログを作成する方法

こちらについては、すでにNewtの公式ドキュメントが存在するため割愛します。
以下のドキュメントをご覧ください。
https://www.newt.so/docs/tutorials/get-contents-in-nuxt

下書き記事を取得するための下準備

非公開の下書き記事を取得するには、上記のチュートリアルで設定しているNewt CDN API Tokenでは出来ません。追加でNewt API Tokenの設定を行います。

.env
NUXT_NEWT_SPACE_UID=hogehoge
NUXT_NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx
+ NUXT_NEWT_API_TOKEN=xxxxxxxxxxxxxxx
nuxt.config.ts
 export default defineNuxtConfig({
   runtimeConfig: {
     newt: {
       spaceUid: "",
       cdnApiToken: "",
+      apiToken: "",
     },
   },
 });

CloudflarePages向けの設定(第一のハマりポイント)

nuxt.config.ts
+ const { NUXT_NEWT_SPACE_UID,
+         NUXT_NEWT_CDN_API_TOKEN,
+         NUXT_NEWT_API_TOKEN
+       } = process.env;

 export default defineNuxtConfig({
   runtimeConfig: {
     newt: {
+      spaceUid: NUXT_NEWT_SPACE_UID,
+      cdnApiToken: NUXT_NEWT_CDN_API_TOKEN,
+      apiToken: NUXT_NEWT_API_TOKEN,
-      spaceUid: "",
-      cdnApiToken: "",
-      apiToken: "",
     },
   },
 });

下書き記事のプレビュー用ページを作成する

ページを作成する

プレビュー用のページを作成します。
ここではpages直下に直接preview.vueを作成しました。

npx nuxi add page preview
preview.vue
<script lang="ts" setup></script>

<template>
  <div>
    Page: Foo
  </div>
</template>

<style scoped></style>

未公開の記事を取得する処理を追加する

NewtAPIを使用することで、未公開状態の記事を取得することが出来ます。
ここでは、/preview?slug=hogehogeのようにクエリパラメータでslugが渡された場合、該当する記事をNewtAPIを用いて取得する処理となっています。

preview.vue
 <script lang="ts" setup>
 // Articleは冒頭のチュートリアルで作成したものです
+ import { Article } from "~~/types/article";

+ const route = useRoute();
+ const slug = route.query.slug?.toString() ?? "";

+ const { data } = await useAsyncData("articles", async () => {
+   const { $newtApiClient } = useNuxtApp();
+   return await $newtApiClient.getFirstContent<Article>({
+     appUid: "blog",
+     modelUid: "article",
+     query: {
+       slug,
+       select: ["_id", "title", "slug", "body"],
+     },
+   });
+ });
+ const article = data.value;

 </script>

 <template>
+   <main class="main">
+     <h2>{{ article?.title }}</h2>
+     <!-- eslint-disable-next-line vue/no-v-html -->
+     <div v-html="article?.body" />
+   </main>
-   <div>
-     Page: Foo
-   </div>
 </template>

 <style scoped></style>

これでプレビューを行う準備はできました。
次にNewt側の設定です。

Newt側でプレビュー時の挙動を指定する

Newtのモデル設定で以下のとおりサイト上でプレビューを行うよう設定します。
この際、クエリパラメータとしてslugを渡すように指定します。

とりあえずローカルで確認してみる

プレビューを行う準備ができたので、ローカルでアプリを起動して確認します。

npm run build

NewtでBlogテンプレートを使用してスペースを作成した場合、デフォルトで記事が3つ作成されます。
それぞれ、slugがarticle-1~3となっているので、article-3を指定してリンクを直接開きます。(通常はNewtの記事編集画面からプレビューボタンを押すと飛ぶ想定です。)
http://localhost:3000/preview?slug=article-3

もし以下の画像のように表示されたら成功です!

CloudflarePagesで確認する

次にCloudflarePagesへデプロイするための準備に入ります。
通常はGithubとCloudflarePagesを連携させるところですが、今回はCloudflareのCLIツールWranglerを使用してローカルから直接デプロイします。

Wranglerの導入する

今回はグローバルにインストールします。

npm install -g wrangler

これでwranglerコマンドが使用できるようになったはずです。

CloudflareのIDでログインする

wrangler login

ブラウザの画面が開くのでCloudflareのIDでログインし許可します。

wrangler.tomlを作成する

Nuxt3の公式サイトに記載のとおりwrangler.tomlを作成します。

wrangler.toml
name = "playground"
main = "./.output/server/index.mjs"
workers_dev = true
compatibility_date = "2022-09-10"
account_id = ""
route = ""

[site]
bucket = ".output/public"

CloudflarePages向けにビルド

Nuxtの設定ファイルにCloudflarePages向けにビルドを行うように設定します。

nuxt.config.ts
 export default defineNuxtConfig({
+  nitro: {
+    preset: "cloudflare-pages",
+  },
   runtimeConfig: {
     newt: {
       spaceUid: NUXT_NEWT_SPACE_UID,
       cdnApiToken: NUXT_NEWT_CDN_API_TOKEN,
       apiToken: NUXT_NEWT_API_TOKEN,
     },
   },
 });

設定完了後、アプリをビルドします。

npm run build

ビルド完了後、ターミナルに以下のようなメッセージが出力されます。

Terminal
√ You can preview this build using npx wrangler pages dev dist/
√ You can deploy this build using npx wrangler pages publish dist/

wranglerで確認する

通常のnpm run devではなく、CloudflarePagesを想定した動作確認を行うため、以下のとおりコマンドを入力して起動します。

npx wrangler pages dev dist/

http://localhost:8788/preview/?slug=article-3
previewの後ろに/が入っていますが、これはCloudflarePagesの現在の仕様のようです。
/が入らないようにする方法について議論が行われていますが、現時点であまり進展はないようです…そのうち解決されるものと期待しています。

ブラウザで開いて確認すると…

あれ…表示されない!?
さっきは表示されてたじゃん!!Why!?!?

なぜ表示されないのか?

なぜnpm run devでは表示されたのに、npx wrangler pages dev dist/では表示されないのか。ここから筆者は迷走しました。CloudflarePagesが悪いのか?Nitroの問題なのか?自分の作り方がなにか間違っているのか…

エラーログは以下のとおりでした。

[nuxt] [request error] [unhandled] [500] (e2.adapter || Jr.adapter) is not a function
  at dispatchRequest (C:\Users\hogehoge\Develop\blog_test\node_modules\axios\lib\core\dispatchRequest.js:56:44)
  at process.e (node:internal/process/task_queues:95:5)
  at n (C:\Users\hogehoge\Develop\blog_test\node_modules\newt-client-js\dist\cjs\newtClient.js:1:2393)
  at Object.getFirstContent (C:\Users\hogehoge\Develop\blog_test\node_modules\newt-client-js\dist\cjs\newtClient.js:1:3071)
  at val (C:\Users\hogehoge\Develop\blog_test\server\api\hello.ts:15:1)
  at Object.handler (C:\Users\hogehoge\Develop\blog_test\node_modules\h3\dist\index.mjs:1247:13)
  at r (C:\Users\hogehoge\Develop\blog_test\node_modules\h3\dist\index.mjs:1321:5)
  at Object.localFetch (C:\Users\hogehoge\Develop\blog_test\node_modules\unenv\runtime\fetch\index.mjs:9:13)
  at ServiceWorkerGlobalScope.[kDispatchFetch] (C:\Users\hogehoge\AppData\Roaming\npm\node_modules\wrangler\node_modules\@miniflare\core\src\standards\event.ts:385:13)
  at Server.<anonymous> (C:\Users\hogehoge\AppData\Roaming\npm\node_modules\wrangler\node_modules\@miniflare\http-server\src\index.ts:291:20)
GET /api/hello 500 Internal Server Error (249.86ms)

(e2.adapter || Jr.adapter) is not a function…?
e2.adapterとJr.adapterってなんだ?

筆者は様々検索しましたが有力な情報にはたどり着けず、流行りのChatGPTに聞いてみたりもしましたが分からずじまい…
途方に暮れながらログを眺めていたところ「at Object.getFirstContent (C:\Users\hogehoge\Develop\blog_test\node_modules\newt-client-js\dist\cjs\newtClient.js:1:3071)」に目が止まりました。

そう、このエラーの原因はNewtSDKだったのです。
これに気がつくのに2日かかりました…(^_^;)

筆者は早速、NewtSDKのGithubへ行きIssueを確認しました。
すると…
https://github.com/Newt-Inc/newt-client-js/issues/52

なんかそれっぽいIssueある!!

Nuxt3自体は、外部との通信を行う際などにaxiosを使用しなくなっています。
しかし、NewtSDKはaxiosを使っているようです。

そして、CloudflarePagesは axiosをサポートしていない という事実が判明しました。
(e2.adapter || Jr.adapter)はどうやらaxiosの関連だったようです。

筆者は上記のIssueのやり取りの内容を基にコードを修正しました。

adapterをインストールする

npm install @vespaiach/axios-fetch-adapter

adapterを使用するようにオプションで指定する

plugins/newt.server.ts
+ import fetchAdapter from "@vespaiach/axios-fetch-adapter";
  import { createClient } from "newt-client-js";

  export default defineNuxtPlugin(() => {
    const config = useRuntimeConfig();
    const newtClient = createClient({
      spaceUid: config.newt.spaceUid,
      token: config.newt.cdnApiToken,
      apiType: "cdn",
+     adapter: fetchAdapter,
    });

    const newtApiClient = createClient({
      spaceUid: config.newt.spaceUid,
      token: config.newt.apiToken,
      apiType: "api",
+     adapter: fetchAdapter,
    });

    return {
      provide: {
        newtClient,
        newtApiClient,
      },
    };
  });

再度wranglerで確認する

変更後、リビルドしてwranglerで確認します。

npm run build
npx wrangler pages dev dist/

http://localhost:3000/preview?slug=article-3

やった…!表示された!!

教訓:エラーログの確認は慎重に!

この間、Netlifyにしようかと問題を諦めて放棄することなどを視野にも入れていました。
ただ、改めて冷静になってログを眺めているときに気づいたわけです。

私はつい、先に先にと進みたい節があり、エラーが起きるとエラーメッセージを基に検索してしまいがちですが大事なのは 「どこで起きたのか?」 でした。
そこをおろそかにして、早く解決して次に進みたい!と焦った挙げ句、ドツボにはまっていったように思います。

ですので、先に進んで意図通り動いているのを見て脳汁を出したい気持ちは抑えて、まずはきちんとエラーログを丁寧に確認し、なんのエラーがどこで起きたのかを調査するのが大事だというのを改めて認識しました。

Discussion