はじめに
こんにちは、森茂です。  
今回は、まだv0.19.2(2023年2月現在)ではありますが、従来のフレームワークとは異なったあたらしい思想のQwikとmicroCMSを利用したブログ作成の第一歩を紹介します。
また今回は、Qwik CityのStatic Site Adapterを利用してSSG(静的に生成された)サイトをCloudflare Pagesにデプロイするまでゴールとしています。
2023年5月3日追記
2023年5月2日にQwikはv1.0となりGAになりました。
この記事公開時からAPIや書き方など大きく変更があったためv1.0にあわせて一部を加筆、修正しています。
環境について
※ Qwikは2023年5月3日現在の最新版。今後のバージョンアップによっては実装が変更になる可能性がある点あらかじめご了承ください。
Qwikとは
QwikはBuilder.io社が作成したWebアプリケーションを構築するためのWebフレームワークです。昨今のWebフレームワーク同様、パフォーマンスを重要視していますがそのアプローチは他のフレームワークと異なり、とてもユニークなものとなっています。QwikはDelay Executionという考えのもとJavaScriptの実行をできるだけ遅らせることを前提としており、最初にはHTMLと約1kb程度のJSを配信し、必要なときだけJSを段階的にロードすることで、高いパフォーマンスを実現しています。
Delay Execution、Resumability & Serializationという考えについては下記公式のドキュメントも参照ください。
Qwik Cityとは
Qwikがコアフレームワークとして位置づけられているの対して、Qwik Cityはメタフレームワークとして位置づけられています。公式ドキュメントにも記載されている通り、Qwik CityはQwikにとって、ReactにとってのNext.js、VueにとってのNuxt、SvelteにとってのSvelteKitと考えるのがよさそうです。
Qwik CityではNext.jsと同様にファイルシステムベースのルーティングやネスト化できるレイアウト、リソースルートなど多くの機能が利用できます。
Qwik Cityのセットアップ
まずはQwik Cityのセットアップを行います。CLI実行の途中starterテンプレートの選択がありますが今回はBasic App(QwikCity)を選択します。
yarn create qwik
セットアップができたところで一度開発サーバーを起動してみましょう。
cd qwik-app-blog
yarn dev
$ vite --mode ssr
  VITE v4.3.4  ready in 503 ms
  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
  ❗️ Expect significant performance loss in development.
  ❗️ Disabling the browser's cache results in waterfall requests.
問題なく起動できたらhttp://localhost:5173/にテストページを確認しておきましょう。
microCMSの準備
microCMSでAPIを用意します。今回はmicroCMSのブログテンプレートを利用して進めていきます。

アカウント登録がまだの方はこちらのドキュメントを参考に、登録を行ってください。
また、APIへの接続を簡単に行えるようmicrocms-js-sdkをインストールします。
yarn add microcms-js-sdkAPIキーとサービスドメイン
あわせてAPIのエンドポイントと、APIキーも用意しておきましょう。ブログ一覧画面のAPIプレビューから確認するのがレスポンスの中身も確認できるので手軽で便利です。
取得したAPIキーとAPIのサービスドメイン名は環境変数ファイル.env.localに記載しておきます。Qwikでは環境変数の扱いを大きく2種類に分けています。
- Build-time variables ビルド時にバンドルされる変数、サーバーサイドだけでなくブラウザでも利用可能なため扱いには注意が必要
 - Server-side variables サーバーサイド実行時のみ利用される変数のためプライベートな値の扱いに向いている
 
今回はSSGを目的としており、生成時にのみ利用するためBuild-timeの環境変数として設定します。Build-time variablesとして利用するにはPUBLIC_を付与する必要があります。なおSSRなアプリケーションの中でmicroCMSを利用する際はServer-side variablesの利用をおすすめします。
PUBLIC_MICROCMS_SERVICE_DOMAIN=サービスドメイン名
PUBLIC_MICROCMS_API_KEY=APIキーコンテンツ取得用クライアントの作成
microCMSの準備ができたところでQwik CityからmicroCMSのコンテンツを取得するためのクライアントを作成します。src/libs/microcms.tsファイルとして型定義、クライアント、ブログ一覧の取得、ブログ詳細の取得を用意しておきます。
microcms-js-sdkにはMicroCMSQueries、MicroCMSImageやMicroCMSDateなどAPIで利用する定形の型があらかじめ用意されています。こちらもぜひご活用ください。
// src/libs/microcms.ts
import { createClient } from "microcms-js-sdk";
import type {
  MicroCMSQueries,
  MicroCMSImage,
  MicroCMSDate,
} from "microcms-js-sdk";
//ブログの型定義
export type Blog = {
  id: string;
  title: string;
  content: string;
  eyecatch?: MicroCMSImage;
} & MicroCMSDate;
if (!import.meta.env.PUBLIC_MICROCMS_SERVICE_DOMAIN) {
  throw new Error("PUBLIC_MICROCMS_SERVICE_DOMAIN is required");
}
if (!import.meta.env.PUBLIC_MICROCMS_API_KEY) {
  throw new Error("PUBLIC_MICROCMS_API_KEY is required");
}
// Initialize Client SDK.
export const client = createClient({
  serviceDomain: import.meta.env.PUBLIC_MICROCMS_SERVICE_DOMAIN,
  apiKey: import.meta.env.PUBLIC_MICROCMS_API_KEY,
});
// ブログ一覧を取得
export const getList = async (queries?: MicroCMSQueries) => {
  const listData = await client.getList<Blog>({
    endpoint: "blogs",
    queries,
  });
  return listData;
};
// ブログの詳細を取得
export const getDetail = async (
  contentId: string,
  queries?: MicroCMSQueries
) => {
  const detailData = await client.getListDetail<Blog>({
    endpoint: "blogs",
    contentId,
    queries,
  });
  return detailData;
};記事一覧ページの作成
準備ができたところで早速記事一覧のページを作成しましょう。
Qwik CityはNext.jsなどと同様にファイルシステムベースのルーティングになっており、src/routesディレクトリが対象となります。まずは記事一覧ページを用意するために、すでにあにsrc/routes/index.tsxを書き換えていきます。
データの取得
Qwik CityではData Loaderと呼ばれる機能があり、サーバーサイドでのデータ処理を追加できます。Next.jsのgetServerProps()、Remixのloadersと同じようなイメージです。また1つのコンポーネントに対して複数のData Loaderを利用できる点など特殊な部分ももっています。(loader$はv0.17.0以降で利用可能です)
また、Data Loaderの関数loader$()ではリアクティブな状態を管理するためにSignalの概念を利用しています。
記事一覧ページ
記事一覧を表示するページコンポーネントを作成します。(既存のindex.tsxを書き換えます)
// src/routes/index.tsx
import { component$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
import { getList } from "~/libs/microcms";
import { useServerTimeLoader } from "~/routes/layout";
// microCMSから記事一覧を取得する
export const useListLoader = routeLoader$(async () => {
 const { contents } = await getList();
 return contents;
});
export default component$(() => {
  // 外部のLoaderも利用できる
  const serverTime = useServerTimeLoader();
  const list = useListLoader();
  return (
    <div>
      <p>Server time: {serverTime.value.date}</p>
      <ul>
        {list.value.map((item) => (
          <li key={item.id}>
            <a href={`/blog/${item.id}`}>{item.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
});
http://localhost:5173/にアクセスしてみるとlayoutコンポーネントの中に記事一覧ページが配置されます。
記事詳細ページの作成
次に記事詳細ページを用意していきます。新規にsrc/routes/blog/[postId]/index.tsxを作成します。Next.jsと同じように[postId]の部分がparamsとしてページコンポーネントへ渡され利用できます。
またDocumentHeadの中でheadタグを動的に扱うことができ、その中でもData Loaderが利用できます。その際resolveValueを利用することで値の解決を待ってから生成を行うことができます。その他Data Loaderの値解決に親子関係を持たせたい場合はresolveValueが活用できます。
// src/routes/blog/[postId]/index.tsx
import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { routeLoader$ } from "@builder.io/qwik-city";
import { getDetail } from "~/libs/microcms";
import { useServerTimeLoader } from "~/routes/layout";
// microCMSから記事詳細を取得する
export const usePostLoader = routeLoader$(async ({ params, status }) => {
  if (!params.postId) {
    throw new Error("postId is required");
  }
  try {
    const post = await getDetail(params.postId);
    return post;
  } catch {
    // 記事がない場合はStatus Code 404を返す
    status(404);
  }
});
export default component$(() => {
  const serverTime = useServerTimeLoader();
  const post = usePostLoader();
  if (!post.value) {
    return <h1>Not Found.</h1>;
  }
  return (
    <div>
      <p>Server time: {serverTime.value.date}</p>
      <h2>{post.value.title}</h2>
      <div dangerouslySetInnerHTML={post.value.content}></div>
    </div>
  );
});
// 動的にheadを書き換える
export const head: DocumentHead = ({ resolveValue }) => {
  const post = resolveValue(usePostLoader);
  return {
    title: post?.title || "Welcome to Qwik",
    meta: [
      {
        name: "description",
        content: post?.title || "Qwik site description",
      },
    ],
  };
};
SSGの設定
開発サーバー上ではSSRで動作しているためページ内に用意しているServer timeは常に生成された時間に変化します。今回はCloudflare Pagesに静的ページとしてデプロイしたいため、ビルド時に静的なページを生成するSSGの設定を追加します。(Cloudflare Pages Adapterを利用することでSSRを利用したサイトをデプロイすることも可能です。)
Qwikにはいろいろなサービスへ接続するためのアダプターやスタイリング、テストツールなどを統合できるインテグレーション機能が組み込まれています。
Static Site Adapterのインストール
Static Site Adapterを利用することでSSGを実現できます。今回はyarn qwik addを実行してAdapter: Static site (.html files)(Static Site Generator)を選択します。
yarn qwik add
┌  🦋  Add Integration
│
◆  What integration would you like to add?
│  ○ Adapter: Cloudflare Pages
│  ○ Adapter: Azure Static Web Apps
│  ○ Adapter: Netlify Edge
│  ○ Adapter: Vercel Edge
│  ○ Adapter: Google Cloud Run server
│  ○ Adapter: Deno Server
│  ○ Adapter: Node.js Express Server
│  ○ Adapter: Node.js Fastify Server
│  ● Adapter: Static site (.html files) (Static Site Generator)
│  ○ Integration: Builder.io
│  ○ Integration: Cypress
│  ○ Integration: Storybook
│  ○ Integration: Auth.js (authentication)
│  ○ Integration: Playwright (E2E Test)
│  ○ Integration: PostCSS (styling)
│  ○ Integration: Prisma (Database ORM)
│  ○ Integration: Styled-Vanilla-Extract (styling)
│  ○ Integration: Tailwind (styling)
│  ○ Integration: Vitest (Unit Test)
│  ○ Integration: Partytown (3rd-party scripts)
│  ○ Framework: React
└
SSG用のルート設定
SSGのルートを動的に生成するために[postId]/index.tsxへ下記を追記します。
// src/routes/blog/[postId]/index.tsx
// ...
import type {
 DocumentHead,
 StaticGenerateHandler, // 追加
} from "@builder.io/qwik-city";
import { getDetail, getList } from "~/libs/microcms";  // getListを追加
// ...
// SSGのために動的パスを返す
export const onStaticGenerate: StaticGenerateHandler = async () => {
 const { contents } = await getList();
 const paths = contents.map((post) => {
  return post.id;
 });
 return {
  params: paths.map((postId) => {
   return { postId };
  }),
 };
};ビルド
設定が完了したところでビルドを試してみましょう。yarn build.serverコマンドがAdpterによって追加されているのでそちらを利用します。
yarn build.server
yarn run v1.22.19
$ vite build -c adapters/static/vite.config.ts
vite v4.3.3 building SSR bundle for production...
✓ 21 modules transformed.
server/build/q-1d777b71.css             1.67 kB
server/@qwik-city-plan.mjs             11.48 kB
server/entry.ssr.mjs                   52.30 kB
server/assets/index.qwik-6fb5845c.mjs  68.12 kB
Starting Qwik City SSG...
dist/index.html
dist/blog/qfmvon2h_6/index.html
SSG results
- Generated: 2 pages
- Duration: 454.6 ms
- Average: 227.3 ms per page
✓ built in 888ms
✨  Done in 1.59s.distディレクトリの中に生成されたhtmlファイルが書き出されればビルドの成功です。
Cloudflare Pagesへのデプロイ
ここまででブログの一覧画面と詳細画面を作ることができました。
あとは作りたいブログのデザインに合わせてスタイルを調整してみたり、microCMSのAPIスキーマをいじって項目を増やしてみたりしてみてください。
最後の仕上げとして作成したものをデプロイします。
今回はホスティングサービスとしてCloudflare Pagesを利用します。
https://pages.cloudflare.com/
まずはここまでの作業内容をGitHubに上げます。
GitHubのリポジトリを作成後、下記コマンドでプッシュしていきます。
git init
git add .
git commit -m 'initial commit'
git remote add origin your-repository // 自分のリポジトリを入力
git push -u origin main
そしてCloudflare Pagesにログイン後、「プロジェクトを作成 > Gitに接続」を選択して先ほど作成したリポジトリと連携します。
ビルド設定ではフレームワークとしてQwikを選択し、環境変数のセットを行います。(.env.localの内容)
Webhookの設定
現状ではGitHubにソースコードをPushするたびにビルド&デプロイがされますが、microCMSの記事を追加・更新してもビルドはされません。
そこで、microCMSのWebhook機能を使って、記事の追加・更新のたびにCloudflare Pagesでビルド&デプロイがされるように設定しましょう。
設定方法は下記の記事で紹介しているので参考にしてみてください。
https://blog.microcms.io/webhook-cloudflare-vercel/
設定後に新しく記事を公開してみて、Cloudflare Pagesのビルドが動き出したら成功です。
おわりに
Qwik CityとmicroCMSを利用したブログアプリケーションの第一歩について紹介させていただきました。Qwikが2023年5月2日にGAとなりました。とはいえまだまだ発展途上の部分もありプロダクション環境での利用は難しいかもしれませんが、ぜひ素振りがてらmicroCMSとの連携を試してみてください。



