UnoCSSのpresetWebFontsをAstroで使ってみる

created_at

ブログのガワを整えながら、中身(文章)の練習をしている者です。 今回はUnoCSSを用いてフォントの管理を色々してみたというお話です。

UnoCSSとは

UnoCSS is the instant atomic CSS engine, that is designed to be flexible and extensible. The core is un-opinionated and all the CSS utilities are provided via presets.

UnoCSSは、柔軟性と拡張性を備えた設計の即時原子CSSエンジンです。コアは特定の思想を持たず、すべてのCSSユーティリティはプリセットを通じて提供されます。

https://unocss.dev/guide/

Tailwind CSSやWindi CSSと似たような、HTMLの各要素にスタイルを示すクラスを細かく付けていくことで、CSSを書かなくてもスタイルを表現することができるというものです。

ただし、Tailwind CSSとの違いとして、ルールのカスタマイズ性の高さや必要なスタイル(プリセット)のみ選んで実装することができる点があります。

Tailwind CSSをやめてUnoCSSにしてみた

Tailwind CSSは今までよく使ってきました。デフォルトで用意されているスタイルだけでも十分に使えて、クラス名でスタイルを呼び出していけるのは便利ですよね。あんまりやりすぎるとクラス名が長くなってしまったりするので、規模感がでかい場合は管理方法を考えないといけないとは思いますが....

ただ、初期状態ですでに様々なスタイルが定義されているのもあり、ちょっと重いのかなと思う場面があります。Tailwind CSSとWindi CSS、UnoCSSのパフォーマンスに関して比較した記事を貼っておきます。 (この記事ではNext.jsで検証しているようです)

https://t-cr.jp/article/b6r1j6c3138b9g1

Tailwind CSSは、v4になってからViteのサポートがされたことにより、PostCSSを使わなくても利用できるようになっているようです。導入もかなり簡単になってますね!

v4の特徴を詳しく紹介されている記事があったので貼っておきます。

https://zenn.dev/miz_dev/articles/tailwind-css-v4

ここまで書くと、別にTailwind CSSでも良かったのでは?と思ってしまうのですが、今回利用した presetWebFont の機能は結構便利だったので結果オーライかなとw

(ちなみに)移行作業

移行作業自体はそこまで難しくないです。なぜなら、UnoCSSにはプリセットとして、Tailwind CSSのv4のスタイルをほぼカバーしたものが用意されているからです。これを設定ファイルで読み込んでしまえば、そこまで複雑じゃないものはすんなり表示できるようになると思います。

presetWebFontを使ってみる

UnoCSSでのWeb Fontに関するドキュメントは以下に書かれています。

https://unocss.dev/presets/web-fonts

で今回書いたconfigファイルはこんな感じです。

// uno.config.ts
import { defineConfig, presetWebFonts, transformerDirectives, presetWind4 } from 'unocss'
import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'

export default defineConfig({
  transformers: [
    transformerDirectives()
  ],
  presets: [
    presetWind4(),
    presetWebFonts({
      provider: 'fontsource',
      fonts: {
        main: 'M PLUS 2',
        monospace: 'M PLUS 1 Code'
      },
      processors: createLocalFontProcessor({
        cacheDir: 'node_modules/.cache/fonts',
        fontAssetsDir: 'public/assets/fonts',
        fontServeBaseUrl: '/assets/fonts',
      })
    }),
  ]
})

ちなみに、transformerDirectives() というのは、指定した要素に対して @apply のあとにクラス名を書いたスタイルを用意することで、各要素にクラスを書かずに一括してスタイルを適用するために読み込んでいます。

/* style.css */
article h1 {
  @apply text-2xl font-medium;
}

article h2 {
  @apply text-xl font-medium border-b border-neutral-500 pb-3;
}

article h3 {
  @apply text-lg;
}

自分はこんな感じで定義して、MarkdownのファイルがHTMLのDOMに動的に変換される部分に対してスタイルを付けています。

presetWebFonts({
  provider: 'fontsource',
  fonts: {
    main: 'M PLUS 2',
    monospace: 'M PLUS 1 Code'
  },
  processors: createLocalFontProcessor({
    cacheDir: 'node_modules/.cache/fonts',
    fontAssetsDir: 'public/assets/fonts',
    fontServeBaseUrl: '/assets/fonts',
  })
}),

presetWebFontsは、自前でフォントを用意することもできるのですが、providerに対応しているWebフォントを配布しているサービス(GoogleやFontsourceなど)を指定して、必要なフォント名を書くだけで利用できるようにしてくれます!便利!

mainmonospace などのプロパティ名は、 font-mainfont-monospace として利用できます!

さらにこの機能には外部から取得したフォントをキャッシュする機能があり、キャッシュ後はローカルからフォントファイルが呼び出せます!外部のサーバー(CDN)を介さない分、初回のロード時間の短縮も期待できそうですよね。

ローカルでは問題なかったが、デプロイ時に困った

このコンフィグを書いたうえで、開発モードでサーバーを立ててみるとあっさりキャッシュされたファイルが public/ に入りました。(設定では public/assets/fonts に配置するようにしているため)

いい感じだなーと思って、そのままローカルでビルドを試して astro preview で確認してもちゃんと表示されたので、そのままVercelにデプロイしてみました。

しかし、フォントは404になってしまいました。

原因は、ビルド開始時の public/ をそのまま dist/ にコピーして配置しているからでした。どうやらビルドを行う前に public/ にフォントファイルを置いておく必要があるようです。(もしかしたら、この問題はこのブログで利用しているAstro特有の問題かもしれないです。)

prebuildする

LLMとも少し相談しながら、事前にフォントをキャッシュする処理だけ動かしてしまえばいいのでは?という結論になりました。

そこで、以下のようなコードを作成し、 prebuild で実行するようにしてみました。

import { createGenerator } from 'unocss'
import presetWebFonts from '@unocss/preset-web-fonts'
import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'

const preset = presetWebFonts({
  provider: 'fontsource',
  fonts: {
    main: 'M PLUS 2',
    monospace: 'M PLUS 1 Code',
  },
  processors: createLocalFontProcessor({
    cacheDir: 'node_modules/.cache/fonts',
    fontAssetsDir: 'public/assets/fonts',
    fontServeBaseUrl: '/assets/fonts',
  }),
})

try {
  const uno = await createGenerator({ presets: [preset] })
  await uno.generate('', { preflights: true })
  console.log('Webfont cached: public/assets/fonts')
} catch (error) {
  console.error(`Failed to prefetch!: ${error}`)
  process.exit(1)
}
// package.json
"scripts": {
  "prebuild": "node scripts/prefetchFonts.mjs",
  "build": "astro check && astro build",
},

フォントに関して全く同じ設定を書いたうえで、 uno.generate() を呼び出すとフォントのキャッシュ処理だけ走り、 public/ にフォントファイルが生成されました!

これを実装したうえで再びデプロイしたところ、問題なくVercelでもフォントが適用された状態で表示されました!

さいごに

少しだけパフォーマンスをLighthouseで見てみたんですが、あんま変わってなかったかもです...w

でも面白かったので良しとしましょう。