静的アセット
画像・SVG・フォント・favicon・robots.txt など、バイト単位でそのまま配信するファイルを zfb の public/ ディレクトリ経由でどう配信するか。
このページの内容
静的ファイル(画像・SVG・フォント・favicon・robots.txt・JSON マニフェスト、あらゆるバイナリ)を public/ ディレクトリ経由で配信する方法を説明します。URL の規約、dev/prod の一致保証、ファイル名がページと衝突したときの優先順位ルール、base マウントプレフィックスとの相互作用、そして代わりに TSX の import を使うべきケースを扱います。
zfb はコード以外のアセットを 1 つのディレクトリ public/ で扱います。ファイルを入れて絶対 URL で参照すれば、同じ URL が zfb dev・zfb preview・ビルドが出力する静的な dist/ のいずれでも動作します。インストールするプラグインも、書くべき import も、壊しうるバンドラのステップもありません。
規約
public/ 内のものはすべてサイトルートでそのまま配信されます。public というセグメントは URL に 現れません。
public/favicon.ico → /favicon.ico
public/logo.svg → /logo.svg
public/robots.txt → /robots.txt
public/img/hero.png → /img/hero.png
public/fonts/Inter.woff2 → /fonts/Inter.woff2サブディレクトリは保持されますが、トップレベルの public/ という名前は取り除かれます。/ へのリクエストは、dev では <project_ に、zfb build 後は dist/ に解決されます。
アセットの参照
絶対 URL を使ってください。アセットパスはレンダリングされた HTML に現れるものと一致します。
// pages/index.tsx
export default function Home() {
return (
<main>
<img src="/logo.svg" alt="Site logo" width={128} height={32} />
<link rel="icon" href="/favicon.ico" />
</main>
);
}CSS でも同じ方法が使えます。URL はブラウザが最終的にリクエストするものそのものです。
/* styles/global.css */
.hero {
background-image: url("/img/hero.png");
}
@font-face {
font-family: "Inter";
src: url("/fonts/Inter.woff2") format("woff2");
}静的アセットをモジュールとして import しない
zfb は public/ に対してバンドラを実行 しません。以下のようなパターン(Vite・webpack などのツールチェーンでよく見られるもの)はここでは動作しません。
// ❌ 静的ファイルにこれをしてはいけません。
import logoUrl from "../public/logo.svg";
import heroImg from "./hero.png";これらの import を URL に変換するアセットパイプラインは存在しません。代わりに絶対 URL の形式(src=)を使ってください。import は コード(islands が使う .ts・.tsx・.css モジュール)には依然として正しい答えですが、ブラウザにそのまま取得させたい画像・フォント・SVG のようなバイナリファイルには適しません。
CSS がストロークや塗りなどをスタイルできるよう SVG を JSX としてインライン化する必要が本当にある場合は、SVG マークアップを TSX コンポーネントにコピーしてください。それはコードの経路です。public/ はバイト単位そのままの経路です。
dev / prod の一致
dev サーバーとプロダクションビルドは URL の形について一致します。これは偶然ではなく保証です。
zfb dev—public/内のファイルは リクエストごとにディスクからライブで 配信されます。ページハンドラは、ページキャッシュのミスと<project>/.zfb-build/dev-pages/のミスのあと、<public_root>/<path>からの読み取りにフォールバックします。public/ディレクトリには URL プレフィックスもトップレベルのnest_serviceマウントもなく、ファイルはサイトルートに直接現れます。(注意: コンパイル済みの CSS と islands バンドルはdist/assets/から配信されますが、ルートごとの HTML はdist/ではなく.zfb-build/dev-pages/に書き出されます。)zfb build—copy_public_dir(crates/内)がzfb/ src/ commands/ build. rs public/配下のすべてのファイルをdist/<rel>へ再帰的にコピーします。エッジ CDN が配信する静的なdist/ツリーは、dev でブラウザが見たものと同じ形です。
つまり、ページに一度書いた <img src= は、条件分岐・環境チェック・withBase 風のヘルパーなしで両モードで動作します。
dev は `dist/` ではなく `public/` から配信する
public/ を dist/ に実体化(コピー)するのは zfb build だけです。zfb dev はpublic/ を dist/ にコピーしません — リクエストされた各静的ファイルを、その場でpublic/ から直接読み取ります。覚えておく価値のある帰結として、dev では読み取るべきdist/<静的ファイル> は存在しません。開発中に、public/ に置いたファイルを dist/ から 取りに行くツールがある場合、それは見つかりません。そのツールは代わりに public/ を指すように するか、先に zfb build を実行してください。これは zfb が dev で常に使ってきた配信直結 (serve-direct)モデルそのものです。逆だと思い込みやすいため、ここで明示しています。
dev の起動とライブリロード
配信直結モデルから直接導かれる dev サーバーの挙動が 2 つあります。どちらも開発中に何を期待できるかを左右するため、知っておく価値があります。
public/ は監視ルートではないので、静的アセットの編集はライブリロードされません。 dev ウォッチャーは pages/・content/・components/・layouts/・styles/・data/・設定ファイル・ツリー外のコレクションパスを追跡しますが、public/ は追跡しません。したがって public/ 配下のファイルを編集・追加・削除しても、ウォッチャーイベントは発生せず、ライブリロードも起こりません。そもそも必要ありません。ファイルはディスクからライブで配信されるため、次のリクエストが返すのはすでに新しいバイトです。ページをリロードする(またはアセットを再リクエストする)と変更が見えます。
静的アセットの変更では自動リロードされない
dev サーバーの実行中に public/ を変更しても、ブラウザのタブは自動リフレッシュされません。変更はディスク上で即座に反映されますが、それを拾うには手動リロード(またはアセット URL への新しいリクエスト)が必要です。これは zfb においてライブリロードを引き起こす編集だったことが一度もないため、これまで動いていたプロジェクトが壊れることはありません。ただ、public/ への保存でページがリフレッシュされると期待していた場合、それは起こりませんし、これまでも確実に起きたことはありませんでした。
ブートは public/ のサイズに比例しません。 zfb dev はプロジェクトを走査する前にリスナーを bind し、public/ は走査/監視するツリーから除外されます。大きな静的アセットディレクトリ — 数千枚の画像や、大きなシンボリックリンクツリー — があっても、サーバーが接続を受け付け始める瞬間が遅れることはもうありません。これは 静的アセット/監視ツリーのサイズ からの独立であって、プロジェクト全体の規模からの独立ではありません。初回レンダリング・CSS バンドリング・アイランドバンドルは、依然としてページ数・アイランド数・ソースファイル数に比例します。ブートの順序の全体像は dev モードのライフサイクル — ブートは bind ファースト を参照してください。
移行: ほとんどのプロジェクトで対応は不要
配信直結モデルは新しいものではありません。zfb dev は常に public/ をディスクから配信してきており、開発中に public/ を dist/ に実体化したことは一度もありません。移行すべき「dev から dist/ へのコピー」ステップは存在しません。消費者から見える事実は上記の 2 点だけです。public/ は監視ルートではない(ので静的アセットの変更はライブリロードされません — これらはもともと no-op のイベントだったので、これまで動いていたものは何も壊れません)こと、そして dev のブートが public/ のサイズに比例しなくなったことです。唯一確認すべきなのは、開発中に静的ファイルを dist/ から読み取るツールです。dev ではそのファイルは dist/ ではなく public/ にあります。それ以外については、対応は不要です。
優先順位: ページが public ファイルに優先する
pages/ ルートと同じ URL を持つ public/foo ファイルを持つことは可能です(通常は意図しないものですが)。zfb はこれを決定論的に解決します。
プラグインの dev ミドルウェア で
/を主張するものが最初に実行されます。foo ページキャッシュ —
pages/のレンダリング出力が次に優先されます。foo. tsx .zfb-build/dev-pages/ディレクトリ — dev の HTML ルート(dev パイプラインが書き出したルートごとのファイル)が次にチェックされます。/(CSS、islands バンドル)はassets/ * dist/assets/から配信されます。public/ディレクトリ — 上記すべてがミスした場合にのみ参照されます。それ以外は 404。
したがって、同名の TSX ページは常に public ファイルを覆い隠します。逆は不可能です。public/foo がルートを上書きすることはできません。ページも主張する URL に静的ファイルを置きたい場合は、どちらか一方をリネームしてください。
base との相互作用
zfb.config.ts が base プレフィックス(例: サブパス配下へのデプロイのための base: "/pj/site/")を設定すると、public/ 内のファイルもそのプレフィックスの下へ移動します。
config: base: "/pj/site/"
public/logo.svg → /pj/site/logo.svg (dev と prod)dev サーバーの serve_page フォールバックも、ビルド時の copy_public_dir も、このプレフィックスを尊重します。プロジェクトの他の部分と同じやり方で HTML 内のアセット URL を書いている限り(通常は markdown / TSX パイプラインがすでに実行しているリンクリライターを経由して)、プレフィックスは自動的に適用されます。
設定
このディレクトリは設定可能です。デフォルト以外を指すには zfb.config.ts に publicDir を追加します。
// zfb.config.ts
import { defineConfig } from "@takazudo/zfb/config";
export default defineConfig({
publicDir: "static",
});デフォルト: "public"。パスはプロジェクトルートからの相対で解決されます。ディレクトリが存在しない場合は黙って no-op になります。すべてのプロジェクトに必要なわけではありません。
public/ に入れないもの
public/ が適した置き場所:
サイト全体のアイコンと favicon(
favicon.ico・apple-touch-icon.png)Open Graph / ソーシャルシェア用画像
robots.txt・humans.txt・security.txtWeb アプリマニフェスト(
manifest.webmanifest)自前でホストするフォント
多くのページから絶対 URL で参照される装飾的な画像
public/ が適さない置き場所:
変換するソース画像(リサイズ・最適化・AVIF/WebP への変換)。zfb には組み込みの画像パイプラインがありません。変換が必要なら帯域外で実行し(例:
prebuildスクリプト経由)、最適化された出力をpublic/にチェックインするか、別のツールを使ってください。islands のコード依存。
"use client"島がインポートする TSX / JSX / TS / CSS は、島の隣に置いてバンドルすべきです。コードをpublic/に置くとバンドラを完全にスキップしてしまい、ブラウザはランタイムが実行できない生のソースを取得することになります。拡張子が示すものと異なる
Content-Typeが必要なファイル。zfb は Content-Type をファイル拡張子から導出します。オーバーライドが必要なら、代わりに TSX ページ経由でファイルをレンダリングしてください(Non-HTML Pages を参照)。
関連
Project structure:
public/— ディレクトリレイアウトの概観。Non-HTML Pages — ヘッダーを制御したい場合や、ページをコレクションデータに依存させたい場合に、
.xml・.json・.txtを TSX ページ経由でレンダリングする方法。Islands — クライアントサイド JS の経路。ここで説明した静的アセットの経路とは別物です。