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

最終更新日:

Table of contents

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

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

  • Qwik(@builder.io/qwik): 1.3.5
  • Qwik City(@builder.io/qwik-city): 1.3.5
  • newt-client-js(newt-client-js): 3.3.0

概要

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.3.5)
◇  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 (Qwik City + Qwik)
◇  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
$ npm install
$ npm 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で作成したトークンの値を定義します。以下を、実際の値で置き換えて定義してください。

.env.local
1NEWT_SPACE_UID=sample-for-docs
2NEWT_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/lib/newt.ts
1import { createClient } from 'newt-client-js'
2
3export const generateClient = (spaceUid: string, token: string) => {
4  return createClient({
5    spaceUid,
6    token,
7    apiType: 'cdn',
8  })
9}

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

4. 一覧ページの作成

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

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

src/entry.ssr.tsx
(省略)

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 属性も修正します。

src/root.tsx
(省略)

  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
1export interface Article {
2  _id: string
3  title: string
4  slug: string
5  body: string
6}

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
1import { routeLoader$ } from '@builder.io/qwik-city'
2import { generateClient } from '../lib/newt'
3
4export const useArticles = routeLoader$(async ({ env }) => {
5  const spaceUid = env.get('NEWT_SPACE_UID') || ''
6  const token = env.get('NEWT_CDN_API_TOKEN') || ''
7  const client = generateClient(spaceUid, token)
8})

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

src/routes/index.tsx
1import { routeLoader$ } from '@builder.io/qwik-city'
2import { generateClient } from '../lib/newt'
3import type { Article } from '~/types/article'
4
5export const useArticles = routeLoader$(async ({ env }) => {
6  const spaceUid = env.get('NEWT_SPACE_UID') || ''
7  const token = env.get('NEWT_CDN_API_TOKEN') || ''
8  const client = generateClient(spaceUid, token)
9
10  const { items: articles } = await client.getContents<Article>({
11    appUid: 'blog',
12    modelUid: 'article',
13    query: {
14      select: ['_id', 'title', 'slug', 'body'],
15    },
16  })
17
18  return articles
19})

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

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

src/routes/index.tsx
1import { component$ } from '@builder.io/qwik'
2import { routeLoader$ } from '@builder.io/qwik-city'
3import { generateClient } from '../lib/newt'
4import type { DocumentHead } from '@builder.io/qwik-city'
5import type { Article } from '~/types/article'
6
7export const useArticles = routeLoader$(async ({ env }) => {
8  const spaceUid = env.get('NEWT_SPACE_UID') || ''
9  const token = env.get('NEWT_CDN_API_TOKEN') || ''
10  const client = generateClient(spaceUid, token)
11
12  const { items: articles } = await client.getContents<Article>({
13    appUid: 'blog',
14    modelUid: 'article',
15    query: {
16      select: ['_id', 'title', 'slug', 'body'],
17    },
18  })
19
20  return articles
21})
22
23export default component$(() => {
24  const articles = useArticles()
25  return (
26    <main>
27      <ul>
28        {articles.value.map((article) => {
29          return (
30            <li key={article._id}>
31              <a href={`articles/${article.slug}`}>{article.title}</a>
32            </li>
33          )
34        })}
35      </ul>
36    </main>
37  )
38})
39
40export const head: DocumentHead = {
41  title: 'Newt・Qwik Cityブログ',
42  meta: [
43    {
44      name: 'description',
45      content: 'NewtとQwik Cityを利用したブログです',
46    },
47  ],
48}

ここでは、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
1import { routeLoader$ } from '@builder.io/qwik-city'
2import { generateClient } from '~/lib/newt'
3import type { Article } from '~/types/article'
4
5export const useArticle = routeLoader$(async ({ env, params, fail }) => {
6  const spaceUid = env.get('NEWT_SPACE_UID') || ''
7  const token = env.get('NEWT_CDN_API_TOKEN') || ''
8  const client = generateClient(spaceUid, token)
9
10  const article = await client.getFirstContent<Article>({
11    appUid: 'blog',
12    modelUid: 'article',
13    query: {
14      slug: params.slug,
15      select: ['_id', 'title', 'slug', 'body'],
16    },
17  })
18  if (!article) {
19    return fail(404, {
20      errorMessage: 'Not found',
21    })
22  }
23
24  return article
25})

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

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

src/routes/articles/[slug]/index.tsx
1import { component$ } from '@builder.io/qwik'
2import { routeLoader$ } from '@builder.io/qwik-city'
3import { generateClient } from '~/lib/newt'
4import type { DocumentHead } from '@builder.io/qwik-city'
5import type { Article } from '~/types/article'
6
7export const useArticle = routeLoader$(async ({ env, params, fail }) => {
8  const spaceUid = env.get('NEWT_SPACE_UID') || ''
9  const token = env.get('NEWT_CDN_API_TOKEN') || ''
10  const client = generateClient(spaceUid, token)
11
12  const article = await client.getFirstContent<Article>({
13    appUid: 'blog',
14    modelUid: 'article',
15    query: {
16      slug: params.slug,
17      select: ['_id', 'title', 'slug', 'body'],
18    },
19  })
20  if (!article) {
21    return fail(404, {
22      errorMessage: 'Not found',
23    })
24  }
25
26  return article
27})
28
29export default component$(() => {
30  const article = useArticle()
31  if (article.value.errorMessage) {
32    return <h1>{article.value.errorMessage}</h1>
33  }
34
35  return (
36    <>
37      <h1>{article.value.title}</h1>
38      <div dangerouslySetInnerHTML={article.value.body} />
39    </>
40  )
41})
42
43export const head: DocumentHead = ({ resolveValue }) => {
44  const article = resolveValue(useArticle)
45  return {
46    title: article?.title,
47    meta: [
48      {
49        name: 'description',
50        content: '投稿詳細ページです',
51      },
52    ],
53  }
54}

※ 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 を選択します。

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

6-2. createClientのfetchオプションを設定する

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

Fetch APIを利用するために、Newtのクライアントに fetch として globalThis.fetch を設定します。

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

src/lib/newt.ts
import { createClient } from 'newt-client-js'

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

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

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

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

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

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

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

NewtMade in Newt