title

QwikCity + CloudflarePages + Newtでブログを作る

2023/12/6

FrontendCloudflareQwik

Newt公式のチュートリアルを参考にブログを作成する過程でハマったところやチュートリアル外での作業などを紹介します。


概要

この記事では途中ハマったポイントや、追加で行った雑多な事などをメモします。

また、基本的には公式チュートリアルにデプロイする前後のところまで完了している状態を前提にしています。

記事を読む前に

コードブロックなどは所々省略されていたりするので、コピペでは動かないかもしれません。もし参考にされる方が居たら、記事執筆時点のブランチを参考にしてください。

Github - ephy.dev

CloudflarePagesへのデプロイ

内容をなぞるだけで一旦はデプロイ前までいけますが、Newtのライブラリがfetch()に対応していないなどの理由からaxios-fetch-adapterを導入する辺りでビルド時にコケるかもしれないです。自分が遭遇したのは以下のエラーです。

Module not found: Package path ./lib/core/settle is not exported from <略>

axiosはセキュリティ関係のアップデートが近年あったように記憶していますが、axios-fetch-adapterは特にメンテナンスされてないようです。

原因はaxios-fetch-adapterのindex.jsでaxios関係のimportがうまく行えておらず、最新のaxiosだと/libからではなく/unsafeからでないと呼べなかったりする様子です。

また、forkして少し書き換えて見ましたが、isStandardBrowserEnv()等の実装はすでに/utilsにはなく、どのみち動きませんでした。

import settle from "axios/lib/core/settle"; // axios/unsafe/core/settleというように呼ぶ
import buildURL from "axios/lib/helpers/buildURL";
import buildFullPath from "axios/lib/core/buildFullPath";
import { isUndefined, isStandardBrowserEnv, isFormData } from "axios/lib/utils";

issueも上がっていて、困っている人は去年から居るっぽいですね。

とりあえず、たまたま見つけたフォークリポジトリのものを拝借することにして、無事ビルドとデプロイが通りました。

konfig-axios-fetch-adapter

信用できるかは不明なので各自別のものを探したり、axios-fetch-adapterをforkして自分で動くように細かな対応をするなど必要です。


CloudflarePages

独自ドメイン設定、アクセス制御、リダイレクト設定を行います。

また、CloudflarePages へデプロイする際に以下のエラーが出るときが何度かありました。

✘ [ERROR] Received a malformed response from the API

これについては、すこし時間をおいて再デプロイを何度かすると問題なく通るので焦らずにちょこちょこと再デプロイしてみてください。

末尾スラッシュ自動付与の解除(trailingSlash)

末尾スラッシュの自動付与が個人的に好きではないのでオフにしました。

作業内容としては、vite.config.js内のプラグインプロパティにある、qwikCity()にtrailingSlash: falseを渡すようにしています。

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({ trailingSlash: false }), qwikVite(), tsconfigPaths()],
    // 省略
  };
});

その他にも設定できる項目はあるので、ドキュメントを参照してみてください。

Qwik - Vite

SPAとprefetch

QwikはLinkを使用するとSPA的な挙動になります。また、リンク先を事前に読み込むprefetchオプションがあります。QwikのLinkにprefetchを設定すると、リンクをホバーなどした時点で次のページの読み込みを始めます。

aタグでは通常のページ遷移が行われますが、LinkにすることでSPAになるのは面白いですね。ただ、実際使ってみるとちょっと挙動が難しかったり、使い方が正しくなかったのか動作がもっさりしたりします。このあたりについてはより理解度を高めながら使わないとだめかも知れません。

Qwik - Link Prefetch

import { Link } from "@builder.io/qwik-city";

<Link prefetch href="/blog">
  Blog
</Link>;

Qwikアイコン追加

デザインを整えていく上で、何種類かアイコンが欲しかったので追加します。

npm install @qwikest/icons
import { BsGithub } from "@qwikest/icons/bootstrap";
// 中略
<BsGithub />;

Bootstrap系に加え、Octicons(Github系)やHeroicons(Tailwind系)のアイコンも選べるので、公式のドキュメントを見てみてください。

Qwik - Icons

Qwik特有の状態管理とかお作法

状態管理は基本的に以下のような感じになっています。

  • useSignal() 単一プロパティに対して使用
  • useStore() オブジェクトに対して使用

Reactで言うところのuseState()ですね。

特に、useStore()にその状態に対してのメソッドを埋め込むのはQwikらしさがありますね。

import { $, useStore } from "@builder.io/qwik";
const state = useStore<CountStore>({
  count: 0,
  increment: $(function (this: CountStore) {
    this.count++;
  }),
}); // onClick$={() => state.increment()} のように使用

上記の例に近い話で、onClick$()などにはQRL化できる状態でないとメソッドを渡すことは出来ず怒られます。

Captured variable in the closure can not be serialized because it's a function named "hogeMethod".
You might need to convert it to a QRL using $(fn)
import { $ } from "@builder.io/qwik";
// NG
const hogeMethod = () => console.log("hoge");
// onClick$={() => hogeMethod()}

// OK
const fugaMethod = $(() => console.log("fuga"));
// onClick$={() => fugaMethod()}

他にもuseCompoted$()やuseResource$()などありますが、今回は使用しないのでまたいつか深堀りしてみたいと思います。

Qwik - State

QwikCityでの画像の取り扱い

最終的にストレージサービスやCDN経由になってしまう気がしなくもないのですが、「そういう機能もある」という程度に触っておきたいと思います。

Newt から得ている情報はそのままですが、サイト用のプロフィール画像とかはどうやって取り扱うのかと思ったらかなりいい感じでした。src/media/images/profile.pngといったようにsrcの配下に画像を配置し、以下のように読み込みます。

import ProfileImage from "~/media/images/profile.png?jsx";

<ProfileImage style={{ width: "128px", height: "128px" }} />;

webp化とか最適な表示分けなど全部自動でやってimgタグに変換してくれるのでいい感じだと思いました。

Qwik - Image Optimization

Reactを書いてる人向けのお役立ちドキュメント

ドキュメントに React との対比ページがあります。

最近だとjsxが当たり前に使われるようになったり、構文もReact意識されていたりな感じなんでしょうかね。実際のところ、Reactを採用しているプロダクトは圧倒的に増えているように思いますし、その辺り意識しないと見向きもされない現実があるのかも知れません。

Qwik -React Cheat Sheet

TailwindCSSの導入

いろいろな選択肢が最近はありますが、とりあえずTailwindCSSにしてみました。

npm run qwik add tailwind

global.cssに以下が追加されます

@tailwind base;
@tailwind components;
@tailwind utilities;

prettierにclassのソート、.cssファイルでのCSS Propertyに対するソート、importに対するソートをしてくれるプラグインを追加します。

npm install -D prettier prettier-plugin-tailwindcss prettier-plugin-css-order prettier-plugin-organize-imports
plugins:
  - prettier-plugin-tailwindcss
  - prettier-plugin-css-order
  - prettier-plugin-organize-imports

vscodeを使用していてprettierが自動整形してくれない!!という時は、cmd+shift+PからFormat Documentを実行して prettierを指定してください。vscodeの"editor.formatOnSave": trueも忘れずに。

Qwik - Tailwind

PostCSS

Newtから受け取った記事データに対してCSSを当てたいわけですが、通常のCSSだとNest出来ないのでPostCSSの調整をします。

npm install -D postcss
npm run qwik add postcss

PostCSSを追加するだけだとtailwindが警告してくるみたいなので、設定を修正します。

Qwik - Using with Preprocessors#Nesting

[vite:css] Nested CSS was detected, but CSS nesting has not been configured correctly.
Please enable a CSS nesting plugin *before* Tailwind in your configuration.
See how here: https://tailwindcss.com/docs/using-with-preprocessors#nesting
module.exports = {
  plugins: {
    "postcss-import": {},
    "tailwindcss/nesting": "postcss-nesting",
    "postcss-preset-env": {
      stage: 3,
      features: {
        "nesting-rules": true,
      },
    },
    tailwindcss: {},
    autoprefixer: {},
  },
};

Qwik - PostCSS

eslintが*.config.jsに対してお怒りになっている場合

tailwind.config.js
prettier.config.js
postcss.config.js

記事用のCSS調整

/src/stylesディレクトリを作成してからglobal.cssを移動させて、root.tsxの読み込み先を修正します。

import "~/styles/global.css";

名前は何でもいいですが、ブロク記事部分のCSSを書くためのcssファイルを/src/stylesに作成します。

/* .markdownじゃなくてもOK */
.markdown {
  img,
  video {
    margin: 1em auto;
    max-width: 80%;
    height: auto;
  }
  /* 省略 */
}

CSSを作成したら、記事コンポーネントの方で読み込みます。記事を表示する部分に対しては.markdownのクラス配下となるようにしておきます。

import { component$, useStyles$ } from "@builder.io/qwik";
import blogBodyStyle from "~/styles/newt-blog-body.css?inline"; //?inlineをつける

export default component$(() => {
  const article = useArticle();
  useStyles$(blogBodyStyle);

  return (
    <>
      <div class="markdown" dangerouslySetInnerHTML={article.value.body} />
    </>
  );
});

CSP対応

src/routesに[email protected]を作成し、以下のような感じで設定します。ただ、CSP設定に関してはこれが正解というわけではないので、自身で外部から読み込むドメインなどの設定を行う必要があります。

以下の例では、youtube埋め込み系やnewtを許可したりしています。

import type { RequestHandler } from "@builder.io/qwik-city";
import { isDev } from "@builder.io/qwik/build";

export const onRequest: RequestHandler = (event) => {
  if (isDev) return;
  const nonce = Date.now().toString(36);
  event.sharedMap.set("@nonce", nonce);
  const csp = [
    ["default-src", "'self'", "'unsafe-inline'"],
    ["connect-src", "'self'", "data:", "blob:"],
    ["script-src", "'self'", "'unsafe-inline'", "https:", `'nonce-${nonce}'`, "strict-dynamic"],
    ["frame-src", "'self'", `'nonce-${nonce}'`, "*.youtube.com", "*.google.com"],
    ["img-src", "'self'", "*.newt.so", "*.ytimg.com"],
    ["media-src", "'self'", "*.newt.so"],
  ];

  event.headers.set("Content-Security-Policy", csp.map((k) => k.join(" ")).join("; "));
};

nonceに関しては一回限りのランダムな文字列を生成してinline-scriptなどに埋め込むやつです。root.tsxのServiceWorkerRegisterに渡すようにする必要があります。

export default component$(() => {
  const nonce = useServerData<string | undefined>("nonce");
  return (
    <QwikCityProvider>
      {/* 省略 */}
      <ServiceWorkerRegister nonce={nonce} />
      {/* 省略 */}
    </QwikCityProvider>
  );
});

Qwik - Content Security Policy

manifest.json のエラー

Cloudflare Pagesのアクセス制御をしていたりプレビュー機能を使ったりしていて、以下のようなエラーが出る場合の対応もしておきます。これはmanifest.jsonを取得したときに認証が通らずにエラーページなどが返されてしまって、それがhtml形式だったりする時に出るものです。

Manifest: Line: 1, column: 1, Syntax error.

manifest.jsonのところへcrossOrigin="use-credentials"を追加します。プレビューページへのアクセス時に使用している認証情報をmanifest.json取得時にも使用するという感じですね。

<link rel="manifest" href="/manifest.json" crossOrigin="use-credentials" />

ひとまず完成

ざざっと一通りやってみましたが、かなり快適に初期構築することができました。一旦構築しないことには何も始まらないので急ぎ足でしたが、今後も Qwik 関連の情報も追っていきたいと思います。

PageSpeed Insightsでの細かな怒られなど調整して無事100が並びました👏

これから画像など様々なコンテンツを追加したり、なにか改修するたびにスコアが下がりそうな気がしますが、しっかりと対応したいと思います。

PageSpeed Insightsのスコア