Last updated on

Webフォントを自己ホストする手順と理由:ダウンロード〜body 適用、Skill 化まで

  • Webフォント
  • Vite
  • WordPress
  • Sass
  • Cursor

Webフォントを使うときの自分のやり方をはっきり書いておきたい、というのがこの記事の第一の目的です。理解を自分の中で整理するのと同時に、似たスタックの人のたたき台になればうれしい、という位置づけです。
第二に、設置の流れを毎回迷わない順序(ダウンロード → 圧縮 → CSS での定義 → preload → 変数 → body)にそろえます。

案件やサイトが増えると、「どこで圧縮し、どこに書けば本番まで一貫するか」を思い出すコストが残ります。ここでは特定の非公開リポジトリや社内ドキュメントに依存しない形で、再現しやすい手順の型だけに絞ります。

CDN を使わず自分のオリジンで配信する理由だけは、この記事の主役ではないので文末の Q&A(補足) にまとめています。


手順の流れ(全体)

次の順でそろえると、抜け漏れを確認しやすいです。

  1. ダウンロード(入手・ライセンス確認)
  2. 圧縮(WOFF2 化と配置)
  3. 読み込み@font-face でフォントを定義する)
  4. preload(必要なものだけ先読み)
  5. カスタムプロパティ登録:root などに font-family 用の変数)
  6. body で読み込み(本文に var(--…) で適用)

以下、各ステップです。

フォントまわりの置き場所(一例)

Vite + Sass のような構成を例に、よくあるパスだけ示します。ディレクトリ名は自分の規約でよくsrc/ が無い構成なら読み替えてください。

.
├── raw/fonts/                    ← 例:入手した TTF/OTF(圧縮の入力)
└── src/
    ├── assets/
    │   ├── fonts/                ← 公開用に置く WOFF2(ビルドで bundle へ)
    │   └── sass/
    │       └── base/             ← @font-face・カスタムプロパティ、body への指定
    └── (HTML の部品)
        └── (ヘッド共通など)     ← preload を書く場所の例(WP なら header.php など)
置き場所(例)フォントまわりで何を置くか
raw/fonts/ライセンス確認済みの TTF / OTF など(圧縮の入力。Git に含めない運用でもよい)
src/assets/fonts/pyftsubset で用意した WOFF2

@font-face・カスタムプロパティ・body(など)への font-family 指定は src/assets/sass/base/ にまとめると追いやすいです(ファイルの分割はプロジェクトの規約で)。

Vite と src の補足: ビルド前のアセットは src 配下、成果物は dist に出る構成が一般的です。dist へ WOFF2 だけコピーする運用もありますが、src/assets/fonts/ を経由して CSS の url() から読み込むと、出力ファイル名にコンテンツハッシュが付くことがあり、ブラウザキャッシュの更新がしやすいです。サブセットの中身だけ差し替えたときのように見た目のファイル名が同じでも実体が変わる場合、その差がハッシュに反映されるので運用しやすくなります。preloadhref は、ビルド後の公開パスや、WordPress と Vite を併用する場合はビルド成果物との対応関係に合わせて解決してください。


1. ダウンロード(入手・ライセンス)

  • 利用ライセンスとWeb 埋め込み・自己ホストが許容されているかを確認します。
  • 元ファイルは、圧縮コマンドが読める形式のまま、上の一覧raw/fonts/ に置いておきます(Git に含めるかはプロジェクトで決めます)。

2. 圧縮

置き場所の例raw/fonts/ にあるソース用フォントを入力に、fonttoolspyftsubsetWOFF2 を生成し、src/assets/fonts/ などの公開側の置き場へ出力します。用途に応じて「全グリフを残す」と「載せたい文字だけに絞る」の二通りになります。

前提(初回のみ): Python 3 と fonttoolspyftsubset を含む)が必要です。

pip install "fonttools[woff]"

圧縮でどんなツールを使っているか

Python 製の fonttools です。コマンドラインには pyftsubset が付き、--flavor=woff2 で WOFF2 として出力できます。入力は TTF/OTF など OpenType が一般的で、レイアウト用の機能を --layout-features='*' でできるだけ維持する形にしています。「全グリフ」「文言に含まれるグリフだけ(サブセット)」 の切り替えは、この記事では次の2つの pyftsubset の例にまとめます。

圧縮コマンドの例

以下は共通して --flavor=woff2--layout-features='*' を付けています。--output-file のパスは、公開用に載せる src/assets/fonts/(など)へ向けます。

1)全グリフを残す場合--glyphs='*' で、入力フォントのグリフを残したまま WOFF2 化します。

pyftsubset "入力.ttf" \
  --output-file="出力.woff2" \
  --flavor=woff2 \
  --layout-features='*' \
  --glyphs='*'

2)載せたい文字だけに絞る場合(サブセット)--text="…" に現れるグリフだけが出力に含まれます。日本語では、サイトに載せる文字がこの文字列から漏れると表示欠け(豆腐)になりうるので、必要な文字を漏れなく列挙してください。

pyftsubset "入力.ttf" \
  --output-file="出力.woff2" \
  --flavor=woff2 \
  --layout-features='*' \
  --text="使用する文字列"

3. 読み込み(@font-face

Sass で @font-face を書きます。ビルダーが url() 内のアセットを解決する前提なら、ソース側の Sass から見たフォントへの相対パス(例では src/assets/sass/ 直下から ../fonts/...)に寄せると分かりやすいです。ディレクトリ構成はプロジェクトで読み替えてください。以下は 可変フォントを想定した例です。ウェイトが一枚のウェブフォントだけを使う場合は font-weight を単一の数値にするとよいです。

@font-face {
  font-family: "Noto Sans JP VF";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/NotoSansJP-VariableFont_wght.woff2") format("woff2");
}

ここまでで「ブラウザがフォントファイルを参照できる定義」が Sass 側にそろいます。


4. preload

自分の運用では、preload は次のように切ることが多いです。

  • ファーストビューで使うフォントだけ preload する
  • 本文用のフォントは通常 preload しない(容量が大きく、他リソースと帯域を奪い合うため)
  • preload するファイルは 1 ウェイト 1 ファイル程度に絞る

HTML を部品分割している場合は、共通のヘッド(EJS/Nunjucks などのレイアウト、または WordPress の header.php 等)へ preload を足します。hrefnpm run build 後の公開 URL に合わせます(ハッシュ付きファイル名になる設定なら、そのパスに合わせる)。

<link
  rel="preload"
  href="/assets/fonts/NotoSansJP-VariableFont_wght.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

WordPress テーマと Vite を併用する場合: 開発時と本番でアセット URL の出し方が変わることがあるため、preload の href は PHP で解決した絶対 URL を出すと安全です。@font-faceurl() はビルドが解決する想定のままでよいです。

<link
  rel="preload"
  href="<?php echo esc_url($font_preload_url); ?>"
  as="font"
  type="font/woff2"
  crossorigin
>

$font_preload_url は、自分のテーマでの ビルド成果物 URL の組み立て方に合わせて用意します。


5. カスタムプロパティの登録

:root(または Sass の :root { … } ブロック)に font-family 用のカスタムプロパティを置きます。@font-facefont-family と名前を対応させます。

:root {
  --base-font-family: "Noto Sans JP VF", system-ui, sans-serif;
}

6. body で読み込み(適用)

本文の body(またはデザイン上の基底セレクタ)へ font-family を適用します。見出しだけ別書体にするときも、別名のプロパティ(例: --heading-font-family)を足して参照するパターンに寄せられます。

body {
  font-family: var(--base-font-family);
}

手順をスキル化する(ミスを減らし、効率化する)

同じ種類の依頼を Cursor の Agent で繰り返すときは、自作の Skill順番リスト を書いておくと抜け漏れが減ります。自分用の font-setup-web はそのためのもので、中身は次の 6 段を、この順で踏む決まりになっていることが中心です。記事側のステップ見出し(1〜6)とは番号は一致しません。読み替えれば同じ経路です(例: Skill の 4 で 読み込み・変数・body を、基底 Sass でまとめて触る運用になりがち)。

  1. 前提の確認 — Python 3 と pip install "fonttools[woff]"。手元プロジェクトに README があれば先に読む。圧縮の節 の冒頭の前提とも重なる。
  2. 圧縮 — 全グリフかサブセットかを決める。置き場所の例 どおり raw/fonts/src/assets/fonts/ とする WOFF2 を出す(コマンドは 同じく圧縮の節pyftsubset)。
  3. 配置の確認 — 公開側の *.woff2 が、想定ディレクトリに実在するか確認する。
  4. 基底 Sasssrc/assets/sass/base/@font-facefont-display: swap、必要なウェイト情報を書き、読み込み のほか、この記事後半の :rootbody までを同レイヤまたはチーム規約どおりにつなぐ。Skill 側では どの Sass ファイルを開くかまで表で固定している。
  5. preload(FV のみ)preload の節 で述べている方針どおり、必要なぶんだけ共通ヘッドや header.php に張る。静的サイトではビルド後の /assets/fonts/… に合わせ、WordPress と Vite 併用では開発/本番で揺れない 絶対 URL の出し方をテーマに固定。枚数・ウェイト単位で載せ過ぎない。
  6. ビルド確認 — 可能なら npm run build で Sass と preload が破綻していないか、環境があれば表示やネットワークも見る。

Skill にだけ載せているものは、この記事とは別に、事前に読むドキュメントの一覧や、フォント名・ファイル名・パスを捏造しないことの一言などです。この記事は pyftsubset と Sass/HTML/WP の一般形に寄せ、Skill は 毎回触るファイルの順とパスに寄せる、と役割分担すると両方書きやすいです。


新しいフォントを足すときのチェックリスト

  1. ライセンスと納品範囲を確認する
  2. pip install "fonttools[woff]" のうえ、pyftsubsetWOFF2 を用意し、置き場所の例 の公開側フォルダ(例: src/assets/fonts/)に置く
  3. src/assets/sass/base/@font-face・カスタムプロパティ・body などへの適用を更新する
  4. FV 用だけ preload を張る(HTML の共通ヘッドまたは header.php など。ビルド後の URL に合わせる
  5. body などへの font-family 適用を確認する
  6. ビルドまたはステージングで表示とネットワークを確認する

Q&A(補足)

なぜ CDN ではなく自己ホストにするのか

配信先の整理として、フォントファイルは外部 CDN に寄せず、自分のオリジン(同一サイト)に置く前提にしています。

  • 外部 CDN にフォントを寄せると、リクエストや接続先が増え、プライバジーポリシーや利用地域の文脈で説明が増えたりしうる、という整理です。法解釈の断定はここではしません。
  • 制作側のデフォルトを自己ホストに寄せておくと、「この案件は CDN、あれは自分のサーバー」と脳内スイッチが減る、という運用向けの効果もあります。

まとめ

  • 手順は、ダウンロード → 圧縮 → @font-face → preload → カスタムプロパティ → body 適用、の順で確認します。
  • pyftsubset の細かいオプションは fonttools のドキュメントと、自分のフォントの種類に合わせて調べます。特定の私有リポジトリに手順が無い状態でも、この記事の型だけではじめられるようにしてあります。
  • 運用単位で効くのは、しばしば「公開どおりの順序を毎回同じにする」ことです。同じ悩みを持つ人の参考になればうれしいです。