NewtとQwik Cityを利用してブログを作成する

最終更新日:

Table of contents

  1. 記事内で使用している主なソフトウェアのバージョン
  2. 概要
  3. 1. Qwikのセットアップ
  4. 2. Newtのセットアップ
  5. 2-1. Appを追加する
  6. 2-2. スペースUID・App UID・モデルUIDを確認する
  7. 2-3. Newt CDN API Tokenを作成する
  8. 3. リクエストの準備
  9. 3-1. 環境変数の設定
  10. 3-2. newt-client-jsのインストール
  11. 3-3. APIクライアントの作成
  12. 4. 一覧ページの作成
  13. 4-1. 言語の設定をする
  14. 4-2. 投稿の型を定義する
  15. 4-3. 投稿一覧の取得メソッドを作成する
  16. 4-4. 投稿一覧を表示する
  17. 5. 詳細ページの作成
  18. 5-1. 投稿詳細の取得メソッドを作成する
  19. 5-2. 投稿詳細を表示する
  20. 6. Cloudflare Pagesにデプロイする
  21. 6-1. Cloudflare Pages Adapterを設定する
  22. 6-2. axiosにadapterを設定する
  23. 6-3. GitHubのリポジトリを作成し、Cloudflare Pagesと接続する

このチュートリアルでは、Newtと Qwik City を利用して、ブログを作成する手順を紹介します。
具体的には、Newtで管理しているコンテンツの一覧ページと詳細ページを作る手順を紹介した後、Cloudflare Pages にデプロイして、サーバーサイドレンダリング(SSR)でページを表示します。

記事内で使用している主なソフトウェアのバージョン

  • Qwik(@builder.io/qwik): 1.2.11
  • Qwik City(@builder.io/qwik-city): 1.2.11
  • newt-client-js(newt-client-js): 3.2.6
  • axios-fetch-adapter(@vespaiach/axios-fetch-adapter): 0.3.1

概要

Qwik Cityでプロジェクトを作成し、Newtのコンテンツ情報を取得できるようにします。
コンテンツの一覧ページ(パス: /)と詳細ページ(パス: /articles/:slug 。slugがarticle-1の場合は /articles/article-1)を作成し、まずローカル環境で表示できるようにします。
続いて、Cloudflare Pagesにデプロイし、サーバーサイドレンダリングでページを表示します。

1. Qwikのセットアップ

はじめに、Qwikのセットアップを行います。qwik-create-cli-build を利用することで、簡単にQwikのプロジェクトを作成できます。
以下のどちらかのコマンドを入力します。

npm create qwik@latest
# or
yarn create qwik

コマンドを入力すると、以下の質問を聞かれるので、お好きな設定を選びましょう。

  • どこにプロジェクトを作成するか(ここでは ./qwik-blog と入力)
  • スターターの選択(ここでは Empty App を選択)
  • dependenciesをインストールするか(ここでは No を選択)
  • gitリポジトリを初期化するか?(ここでは Yes を選択)

以下のように表示されます。

$  npm create qwik@latest
┌  Let's create a  Qwik App  ✨ (v1.2.11)
│
◇  Where would you like to create your new project? (Use '.' or './' for current directory)
│  ./qwik-blog
│
●  Creating new project in  /Users/foo/bar/qwik-blog  ... 🐇
│
◇  Select a starter
│  Empty App
│
◇  Would you like to install npm dependencies?
│  No
│
◇  Initialize a new git repository?
│  Yes
│
◇  App Created 🐰
│
◇  Git initialized 🎲
│
○  Result ─────────────────────────────╮
│                                                                   │
│  🦄  Success!  Project created in qwik-blog directory             │
│                                                                   │
│  🤍 Integrations? Add Netlify, Cloudflare, Tailwind...            │
│     npm run qwik add                                              │
│                                                                   │
│  📄 Relevant docs:                                                │
│     https://qwik.builder.io/docs/getting-started/                 │
│                                                                   │
│  💬 Questions? Start the conversation at:                         │
│     https://qwik.builder.io/chat                                  │
│     https://twitter.com/QwikDev                                   │
│                                                                   │
│  👀 Presentations, Podcasts and Videos:                           │
│     https://qwik.builder.io/media/                                │
│                                                                   │
│  🐰 Next steps:                                                   │
│     cd qwik-blog                                                  │
│     npm install                                                   │
│     npm start                                                     │
├─────────────────────────────────╯
│
└  Happy coding! 🎉

作成したプロジェクトに移動して、開発サーバーを立ち上げます。

$ cd qwik-blog
$ yarn install
$ yarn start

http://localhost:5173 にアクセスして、以下のような画面が表示されることを確認します。

qwikcity-blog1.png

2. Newtのセットアップ

次にNewtにコンテンツとAPIトークンを用意し、コンテンツの取得を行うための準備を行います。

2-1. Appを追加する

「Appを追加」をクリックして「テンプレートから追加」を選択します。

Appを追加する

表示されるテンプレートの中から「Blog」を選択して、「このテンプレートを追加」をクリックします。
Appテンプレート

テンプレートが追加されると、「投稿データ」「タグデータ」「著者データ」が追加されます。

quick-start03.jpg

2-2. スペースUID・App UID・モデルUIDを確認する

スペースUIDは「スペース設定」から確認できます。

quick-start0402.jpg
quick-start0503.jpg

上記の例だと、スペースUIDは sample-for-docs となります。
この値は3-1で環境変数として定義します。

また「Blog」テンプレートを追加した場合、App UIDは blog、「投稿データ」モデルUIDは article となります。
これらの値は、4-3や5-1で投稿情報を取得する際に利用します。

2-3. Newt CDN API Tokenを作成する

続いて、APIリクエストに必要なトークンを発行します。
スペース設定 > APIキー のページからNewt CDN API Tokenを作成します。

quick-start06.jpg

名前と取得対象を決めて「作成」を押します。

quick-start07.jpg

ここで作成したトークンの値は3-1で環境変数として定義します。

3. リクエストの準備

Newtの SDK を利用することで、NewtのAPIをより簡単に利用できます。
ここではSDKを利用して、NewtのAPIクライアントを作成します。

3-1. 環境変数の設定

Qwik Cityの環境変数には Build-time variablesServer-side variables の2種類があります。
「Build-time variables」はビルド時に読み込まれ、ブラウザでもサーバーサイドでも利用可能な環境変数です。先頭に PUBLIC_ を付ける必要があります。
「Server-side variables」はサーバーサイドでのみ利用可能な環境変数です。ビルド時には読み込まれず、ブラウザでも利用できません。プライベートな変数の場合はこちらを利用します。

このチュートリアルでは、サーバーサイドレンダリング(SSR)を行うため、「Server-side variables」を利用します。静的生成(SSG)ではないため、ビルド時に環境変数がわかる必要もありません。

.env.local ファイルを作成し、2-2で確認したスペースUID、2-3で作成したトークンの値を定義します。以下を、実際の値で置き換えて定義してください。

NEWT_SPACE_UID=sample-for-docs
NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx

上記のように定義しておくと、RequestEvent オブジェクトから、requestEvent.env.get('NEWT_SPACE_UID')requestEvent.env.get('NEWT_CDN_API_TOKEN') として利用できます。

3-2. newt-client-jsのインストール

次に newt-client-js をインストールします。

npm install newt-client-js
# or
yarn add newt-client-js

3-3. APIクライアントの作成

CDN APIのクライアントを作成する関数を用意します。
スペースUIDとトークンの値を引数として渡すことで、クライアントを取得できるようにします。
ここではCDN APIを利用するので、apiType には cdn を指定します。

// src/libs/newt.ts

import { createClient } from 'newt-client-js'

export const generateClient = (spaceUid: string, token: string) => {
  return createClient({
    spaceUid,
    token,
    apiType: 'cdn',
  })
}

これで、NewtにAPIリクエストを送るための準備ができました。

4. 一覧ページの作成

4-1. 言語の設定をする

src/entry.ssr.tsxContainers Attributes で設定されている lang 属性を修正します。ここでは日本語 ja を指定します。

(省略)

export default function (opts: RenderToStreamOptions) {
  return renderToStream(<Root />, {
    manifest,
    ...opts,
    // Use container attributes to set attributes on the html tag.
    containerAttributes: {
-     lang: 'en-us',
+     lang: 'ja',
      ...opts.containerAttributes,
    },
  })
}

また、src/root.tsx にある lang 属性も修正します。

(省略)

  return (
    <QwikCityProvider>
      <head>
        <meta charSet="utf-8" />
        <link rel="manifest" href="/manifest.json" />
        <RouterHead />
      </head>
-     <body lang="en">
+     <body lang="ja">
        <RouterOutlet />
        <ServiceWorkerRegister />
      </body>
    </QwikCityProvider>
  )
})

4-2. 投稿の型を定義する

投稿の型 Article を定義します。
このチュートリアルでは、_idtitleslugbody のみを使うため、以下のように定義しておきます。

// src/types/article.ts

export interface Article {
  _id: string
  title: string
  slug: string
  body: string
}

4-3. 投稿一覧の取得メソッドを作成する

Qwik Cityはファイルシステムベースのルーティングを採用しており、src/routes ディレクトリの配下にファイルを作成すると、自動的にルートとして利用できるようになります。
例えば、以下のようにルーティングされます。

  • src/routes/blog/index.tsx/blog
  • src/routes/blog/first-post/index.tsx/blog/first-post

※ ルーティングの詳細については、Qwik Cityの Routing のドキュメントをご確認ください。

ここではトップページ(パス: /)で投稿一覧を表示したいので、src/routes/index.tsx のファイルを修正します。

Qwik Cityでは routeLoader$ を利用して、外部のCMSやデータベースからデータの取得を行います。
また、routeLoader$RequestEvent を通じて環境変数を取得できます。routeLoader$(async ({ env }) のように引数に env を指定し、env.get('NEWT_SPACE_UID')env.get('NEWT_CDN_API_TOKEN') とすると、環境変数を取得できます。

// src/routes/index.tsx

import { routeLoader$ } from '@builder.io/qwik-city'
import { generateClient } from '../libs/newt'

export const useArticles = routeLoader$(async ({ env }) => {
  const spaceUid = env.get('NEWT_SPACE_UID') || ''
  const token = env.get('NEWT_CDN_API_TOKEN') || ''
  const client = generateClient(spaceUid, token)
})

続いて、投稿一覧を取得するために、SDKが提供している getContents メソッドを利用します。getContentsはNewtのコンテンツ一覧を取得するためのメソッドです。getContentsのパラメータに Article の型を渡すことで、返却される items の型として Article[] が指定されます。
また、selectパラメータを利用して、取得するフィールドを _idtitleslugbody のみに制限します。

// src/routes/index.tsx

import { routeLoader$ } from '@builder.io/qwik-city'
import { generateClient } from '../libs/newt'
import type { Article } from '~/types/article'

export const useArticles = routeLoader$(async ({ env }) => {
  const spaceUid = env.get('NEWT_SPACE_UID') || ''
  const token = env.get('NEWT_CDN_API_TOKEN') || ''
  const client = generateClient(spaceUid, token)

  const { items: articles } = await client.getContents<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      select: ['_id', 'title', 'slug', 'body'],
    },
  })

  return articles
})

4-4. 投稿一覧を表示する

次に、投稿一覧を表示します。src/routes/index.tsx は以下のようになります。
useArticles() で取得した articlesReadonly<Signal<Article[]>> という型になっているので、articles.value のようにすると投稿一覧の情報を取得できます。

// src/routes/index.tsx

import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { generateClient } from '../libs/newt'
import type { DocumentHead } from '@builder.io/qwik-city'
import type { Article } from '~/types/article'

export const useArticles = routeLoader$(async ({ env }) => {
  const spaceUid = env.get('NEWT_SPACE_UID') || ''
  const token = env.get('NEWT_CDN_API_TOKEN') || ''
  const client = generateClient(spaceUid, token)

  const { items: articles } = await client.getContents<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      select: ['_id', 'title', 'slug', 'body'],
    },
  })

  return articles
})

export default component$(() => {
  const articles = useArticles()
  return (
    <main>
      <ul>
        {articles.value.map((article) => {
          return (
            <li key={article._id}>
              <a href={`articles/${article.slug}`}>{article.title}</a>
            </li>
          )
        })}
      </ul>
    </main>
  )
})

export const head: DocumentHead = {
  title: 'Newt・Qwik Cityブログ',
  meta: [
    {
      name: 'description',
      content: 'NewtとQwik Cityを利用したブログです',
    },
  ],
}

ここでは、getArticles で取得した投稿一覧を表示しています。
また、メタデータを設定するために head をエクスポートしています。詳細についてはQwik Cityの Pages のドキュメントをご確認ください。

http://localhost:5173/ にアクセスして、以下のように投稿一覧が表示されれば成功です。

qwikcity-blog2.png

5. 詳細ページの作成

5-1. 投稿詳細の取得メソッドを作成する

Qwik Cityで 動的ルーティング を設定するためには、src/routes/blog/[slug]/index.tsxsrc/routes/user/[username]/index.tsx のように、角括弧を使って動的なパラメータを定義します。

ここでは /articles/:slug/articles/article-1 など)のパスで投稿の詳細を表示したいので、src/routes/articles/[slug]/index.tsx のファイルを作成します。

投稿一覧の取得と同様に、まず環境変数を取得し、Newtクライアントを作成します。
続いて、指定したスラッグのコンテンツを取得するために getFirstContent メソッドを利用しています。

動的パラメータへのアクセスも Request Event を経由してできるため、routeLoader$(async ({ env, params, fail }) のように指定することで、params.slug で動的パラメータ slug を取得できます。
さらに failメソッド を使うことによって、投稿が見つからなかった場合は404のステータスを返すようにしています。

// src/routes/articles/[slug]/index.tsx

import { routeLoader$ } from '@builder.io/qwik-city'
import { generateClient } from '~/libs/newt'
import type { Article } from '~/types/article'

export const useArticle = routeLoader$(async ({ env, params, fail }) => {
  const spaceUid = env.get('NEWT_SPACE_UID') || ''
  const token = env.get('NEWT_CDN_API_TOKEN') || ''
  const client = generateClient(spaceUid, token)

  const article = await client.getFirstContent<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      slug: params.slug,
      select: ['_id', 'title', 'slug', 'body'],
    },
  })
  if (!article) {
    return fail(404, {
      errorMessage: 'Not found',
    })
  }

  return article
})

5-2. 投稿詳細を表示する

次に、投稿詳細を表示します。
src/routes/articles/[slug]/index.tsx は以下のようになります。

// src/routes/articles/[slug]/index.tsx

import { component$ } from '@builder.io/qwik'
import { routeLoader$ } from '@builder.io/qwik-city'
import { generateClient } from '~/libs/newt'
import type { DocumentHead } from '@builder.io/qwik-city'
import type { Article } from '~/types/article'

export const useArticle = routeLoader$(async ({ env, params, fail }) => {
  const spaceUid = env.get('NEWT_SPACE_UID') || ''
  const token = env.get('NEWT_CDN_API_TOKEN') || ''
  const client = generateClient(spaceUid, token)

  const article = await client.getFirstContent<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      slug: params.slug,
      select: ['_id', 'title', 'slug', 'body'],
    },
  })
  if (!article) {
    return fail(404, {
      errorMessage: 'Not found',
    })
  }

  return article
})

export default component$(() => {
  const article = useArticle()
  if (article.value.errorMessage) {
    return <h1>{article.value.errorMessage}</h1>
  }

  return (
    <>
      <h1>{article.value.title}</h1>
      <div dangerouslySetInnerHTML={article.value.body} />
    </>
  )
})

export const head: DocumentHead = ({ resolveValue }) => {
  const article = resolveValue(useArticle)
  return {
    title: article?.title,
    meta: [
      {
        name: 'description',
        content: '投稿詳細ページです',
      },
    ],
  }
}

※ bodyの表示で利用されている dangerouslySetInnerHTML はXSSの危険性があるため、利用には注意が必要です。ここでは、Newtで管理している投稿情報を表示するものであり、不特定多数のユーザーが入力できるものを表示するわけではないため、安全なものとして利用しています。

また、head の値を動的に生成するため、resolveValue を使用しています。resolveValue を使用すると、routeLoader$ で取得した値を head 関数の中で利用できます。
詳細については、Qwikの Dynamic head のドキュメントをご確認ください。

これで、投稿詳細についての設定も完了です。
http://localhost:5173/articles/article-3/ にアクセスして、以下のように投稿詳細が表示されれば成功です。

qwikcity-blog3.png

6. Cloudflare Pagesにデプロイする

続いて、Cloudflare Pagesへのデプロイを設定します。

6-1. Cloudflare Pages Adapterを設定する

Cloudflare Pages Adapter を利用しましょう。

以下のどちらかを実行します。

npm run qwik add cloudflare-pages
# or
yarn qwik add cloudflare-pages

実行すると、以下のように表示されます。
cloudflare-pages のアップデートを適用していいか聞かれるので、Yes を選択します。

┌  🦋  Add Integration  cloudflare-pages
│
◇  👻  Ready?  Add cloudflare-pages to your app?
│
│  🐬 Modify
│     - package.json
│     - .gitignore
│     - README.md
│
│  🌟 Create
│     - .node-version
│     - src/entry.cloudflare-pages.tsx
│     - public/_headers
│     - public/_redirects
│     - adapters/cloudflare-pages/vite.config.ts
│
│  💾 Install yarn dependency:
│     - wrangler ^3.0.0
│
│  📜 New yarn script:
│     - yarn build.server
│     - yarn deploy
│     - yarn serve
│
◇  Ready to apply the cloudflare-pages updates to your app?
│  Yes looks good, finish update!
│
◇  App updated
│
○  New scripts added ───╮
│                          │
│     - yarn build.server  │
│     - yarn deploy        │
│     - yarn serve         │
│                          │
├─────────────╯
│
○  🟣  Next Steps  ──────────────────────────────╮
│                                                                              │
│      Now you can build and deploy to Cloudflare Pages with:                  │
│                                                                              │
│       - yarn run build: production build for Cloudflare                      │
│       - yarn run deploy: it will use the Cloudflare CLI to deploy your site  │
│                                                                              │
├───────────────────────────────────────╯
│
└  🦄  Success!  Added cloudflare-pages to your app

✨  Done in 147.25s.

6-2. axiosにadapterを設定する

NewtのSDKでは axios を利用しており、axiosはデフォルトで XMLHttpRequest を利用します。しかし、Cloudflare PagesではXMLHttpRequestに対応しておらず、Fetch API を利用する必要があります。

Fetch APIを利用するために、Newtのクライアントに adapter として @vespaiach/axios-fetch-adapter を設定します。

npm install @vespaiach/axios-fetch-adapter
# or
yarn add @vespaiach/axios-fetch-adapter

src/libs/newt.ts は以下のようになります。

+import fetchAdapter from '@vespaiach/axios-fetch-adapter'
import { createClient } from 'newt-client-js'

export const generateClient = (spaceUid: string, token: string) => {
  return createClient({
    spaceUid,
    token,
    apiType: 'cdn',
+   adapter: fetchAdapter,
  })
}

また、このままだと Cannot use import statement outside a module のエラーが出るため、vite.config.ts を以下のように修正しましょう。
詳細はViteの 外部SSR のドキュメントをご確認ください。

import { defineConfig } from 'vite'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { qwikCity } from '@builder.io/qwik-city/vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
    preview: {
      headers: {
        'Cache-Control': 'public, max-age=600',
      },
    },
+   ssr: {
+     noExternal: '@vespaiach/axios-fetch-adapter',
+   },
  }
})

6-3. GitHubのリポジトリを作成し、Cloudflare Pagesと接続する

まず、GitHubのリポジトリを作成し、これまで作成したコードをプッシュします。
詳細はGitHubの リポジトリを作成する のドキュメントをご確認ください。

続いて、作成したリポジトリとCloudflare Pagesを接続します。
Cloudflare のアカウントを持っていない方は、登録をお願いします。

接続方法の詳細については、GitHubのリポジトリとCloudflare Pagesを接続して、ホスティングする のチュートリアルを参考にしてください。

「フレームワークプリセット」に「Qwik」を設定し、「環境変数」にご自身の環境変数を設定しましょう。
※ Node・npm・Yarnのバージョンを環境変数で指定しない場合、デフォルトのバージョン となります。NODE_VERSIONNPM_VERSIONYARN_VERSION を適宜ご指定ください。
qwikcity-blog4.jpg

デプロイが成功し、サイトが表示されれば成功です!
qwikcity-blog5.jpg

NewtMade in Newt