zfb
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

サンプル: Corporate Website

CSS Modules と手書きのグローバル CSS でスタイリングした静的生成の zudo-react サイト。zudo-wind を無効にした例

このページで扱うこと

ヒーロー・サービス・会社紹介・問い合わせのセクションを持つコーポレートサイトを、zudo-react で純粋な SSG として構築したサンプルです。CSS Modules のデモでもあり、zfb.config.ts で wind: false を指定し、各コンポーネントが自分専用の *.module.css を隣に持ちます。スタイリング の実例版として読んでください。

ライブデモ: zfb-example-corporate-website.takazudomodular.com

リポジトリ: Takazudo/zfb-example-corporate-website

何を示すか

  • CSS Modules だけで完結させる — 6 つのコンポーネント、6 つの *.module.css があり、ソースにユーティリティクラスはありません。

  • zudo-wind を無効にする — wind: false により、出力されるスタイルシートにはこのプロジェクトが書いた CSS だけが残ります。生成ユーティリティも生成リセットも追加されません。

  • 衝突しないスコープ付きクラス名 — .card は hero.module.css と services.module.css の両方で宣言されていますが、両者が干渉することはありません。

  • グローバル CSS とスコープ付き CSS の同居 — 1 枚のプレーンな global.css がデザイントークンと軽いリセット、そして手書きの .skip-link を 1 つだけ担い、各コンポーネントのスタイルはすべて *.module.css が持ちます。

  • JavaScript が 1 バイトも出ないページ — アイランドを使っていないため、ビルド結果は HTML 1 枚とスタイルシート 1 枚だけで、<script> タグはどこにもありません。

  • アセット専用の Cloudflare Worker — wrangler.toml には [assets] テーブルがあり、main キーは意図的に置いていません。

使用技術

項目このデモでの内容
フレームワークzfb 組み込みの zudo-react JSX ランタイム
zfb バージョン@takazudo/zfb と @takazudo/zfb-runtime をどちらも 3.1.0 に固定
スタイリングCSS Modules と手書きのグローバル CSS — zfb.config.ts で wind: false
レンダリング純粋な SSG — アイランド、サーバールート、クライアント JavaScript なし
Cloudflare 側Workers Static Assets の アセット専用構成(main キーなし)
バインディングなし — AI・KV・D1・R2・Durable Objects いずれも未使用
アダプター@takazudo/zfb-adapter-cloudflare は依存に 含まれていない
その他の依存devDependencies に @types/node・typescript・wrangler

Cloudflare Pages ではない

このリポジトリは Pages から移行済みで、旧 *.pages.dev のホストはすでに死んでいます。正規の URL は上記のカスタムドメインで、custom_domain = true を持つ [[routes]] エントリによって割り当てられています。

必要な準備と設定

Cloudflare アカウントなしでもローカルでは動きます。 クローンして pnpm install、pnpm build、pnpm preview だけです。用意すべきものは何もありません。バインディングも Worker シークレットもマイグレーションもシードデータも不要です。ライブデモは読み取り専用のブローシャーサイトで、問い合わせフォームは action="#" に post するだけで何も保存しません。

Cloudflare が要るのはデプロイのときだけです。 リポジトリシークレットは CLOUDFLARE_ACCOUNT_ID と CLOUDFLARE_API_TOKEN の 2 つ、API トークンには 3 つの権限が必要です。アカウント · Workers Scripts(Edit)、アカウント · Account Settings(Read)、ゾーン · Workers Routes(Edit)。カスタムドメインを取り付けるのは最後のゾーン権限で、これが欠けるとアップロード自体は成功するのにルート設定の段階で失敗し、サイトは *.workers.dev からしか見えなくなります。Cloudflare に触れる CI ジョブはトークン未設定なら自動的にスキップされるので、フォークやクローン直後でも赤くなりません。

Cloudflare 側の設定はこれだけです。

wrangler.toml
name = "zfb-example-corporate-website"
compatibility_date = "2024-12-01"

[assets]
directory = "./dist"
not_found_handling = "404-page"

main がないので Worker のコードは一切動きません。Cloudflare がエッジから dist/ をそのまま配信します。セットアップ手順の全体はリポジトリの docs/cloudflare-setup.md にあります。

仕組み

zudo-wind を無効にするのは設定 1 行

zfb.config.ts
import { defineConfig } from "zfb/config";

export default defineConfig({
  base: "/",
  wind: false,
});

この例は zfb の bare zfb/config import を使います。defineConfig では zfb check に必要な型シムを説明しています。

wind: false では zudo-wind による生成、ユーティリティの検証、候補クラスのスキャンが無効になります。生成ユーティリティも生成リセットも追加されず、このサイトのトークンとリセットは styles/global.css が担います。一方で CSS Modules のコンパイル、スコープ付きクラス名、アセットの出力はそのまま動き続けます。zudo-wind を無効にしても、zfb の CSS パイプラインは停止しません。

コンポーネントが自分のスタイルシートを読み込む

6 つのコンポーネントすべてが同じ形です。デフォルトインポートして、静的なメンバーアクセスで参照します。

components/hero/hero.tsx
import styles from "./hero.module.css";

const STATS = [
  { value: "120+", label: "Products shipped" },
  { value: "9 yrs", label: "Average team tenure" },
  { value: "4.9/5", label: "Client satisfaction" },
];

export default function Hero() {
  return (
    <section class={styles.hero}>
      <div class={styles.inner}>
        <div class={styles.copy}>
          <p class={styles.eyebrow}>Product engineering partner</p>
          <h1 class={styles.title}>
            We build digital products that <em class={styles.accent}>earn their keep</em>.
          </h1>
          {/* … */}
        </div>
      </div>
    </section>
  );
}

書き換えはビルド時に行われるため、styles.hero はビルド中に解決され、その仕組み自体はブラウザーに届きません。dist/ にモジュールごとの .css が出ることもなく、すべてのモジュールのルールが 1 枚のハッシュ付き dist/assets/styles-<hash>.css にまとめられます。

衝突がなくなり名前も再現する

hero.module.css と services.module.css はどちらもただの .card を宣言しています。ビルド後のスタイルシートでは、これは無関係な 2 つのセレクターになります。

.QAAyqq_card {}
.y8_AgG_card {}

接頭辞はファイルの内容でもマシン上の絶対パスでもなく、プロジェクトからの相対パスのハッシュです。ここから 2 つの帰結が生まれ、どちらも重要です。

  • 書き手はグローバルに一意な名前を考える必要がありません。このサイトには 6 つのモジュールにまたがって .inner の宣言が 6 つありますが、それぞれ別のスコープ付きクラスに解決されます。

  • ソースがバイト単位で同じなら、どのマシンでビルドしてもクラス名までバイト単位で一致します。CI でのリビルドはノート PC でのリビルドと同じ HTML と同じスタイルシートを生むため、ハッシュ付きのファイル名も安定し、変更のないデプロイで CDN キャッシュが無駄に捨てられることもありません。

グローバル CSS はグローバルのまま

スコープ化されるかどうかを決めるのは .module.css という接尾辞だけです。レイアウトはコンポーネントの import と並べて、プレーンなグローバルシートを読み込んでいます。

layouts/default.tsx
import type { Child } from "@takazudo/zfb/zudo-react";

import "../styles/global.css";
import SiteHeader from "../components/header/site-header";
import SiteFooter from "../components/footer/site-footer";

styles/global.css には、各モジュールが参照する :root のデザイントークン(--color-brand、--space-5、--text-3xl など)と軽いリセット、そして手書きのユーティリティが 1 つだけ入っています。.skip-link です。このクラスはハッシュ化されないままブラウザーへ届き、レイアウト側も styles.* の参照ではなく class="skip-link" という文字列リテラルで書いています。2 つの仕組みが同じファイルに同居していて、互いに干渉しません。

なお、TypeScript にモジュールの import を受け入れさせるには、アンビエント宣言が 1 つ必要です。このリポジトリでは styles/css-modules.d.ts に置いています。

styles/css-modules.d.ts
declare module "*.module.css" {
  const classes: Readonly<Record<string, string>>;
  export default classes;
}

出力は HTML 1 枚とスタイルシート 1 枚

layouts/default.tsx は <html> から <head>、<title> までドキュメント全体を、ただのサーバーコンポーネントとしてレンダリングします。アイランド を使っていないので、zfb build が出力するのは dist/index.html と dist/assets/styles-<hash>.css だけで、JavaScript はまったく出ません。ビルド後のページに <script> タグは 1 つもありません。

1 ページ構成であることの影響が 1 つだけ見えています。zfb はこのサイトに対して 404.html を出力しないため、未知のパスは現状そっけない 404 を返します。それでも not_found_handling = "404-page" は設定してあります。404 ページが用意された瞬間から実際のページを返すようになりますし、もう一方の選択肢である "single-page-application" は未知のパスすべてに index.html を HTTP 200 で返してしまうため、いずれにせよこちらが正しい設定です。

ローカルで動かす

pnpm install
pnpm build      # zfb build   -> dist/
pnpm preview    # zfb preview -> serves dist/
pnpm typecheck  # zfb check

pnpm dev(zfb dev)も使えます。しかもこのサンプルに限っては、バインディングを使う他のサンプルと違って機能上の注意点がありません。このサイトには Worker のリクエストスコープを必要とする部分がなく、リポジトリ自身も保証を明記しています。zfb dev と zfb build はどちらも、スコープ付きの CSS Modules クラス名を HTML に出力し、対応するスコープ付きのルールを配信される CSS に出力します。開発時に見えているページが、そのまま本番のページです。

ローカル固有の細かい点として、predev が zfb dev の前に rm -rf dist .zfb .zfb-build を実行します。古いビルド成果物が開発中のセッションを覆い隠すことはありません。

関連ドキュメント

  • スタイリング — zudo-wind、手書きのグローバル CSS、このデモが使っている CSS Modules のルール(:export や composes の非対応、ベア指定子の制限を含む)のリファレンスです。

  • 静的アセット — 出力される dist/assets/ がどう作られ、どう配信されるか。

  • アイランド — このサイトが意図的に使っていない機能です。

  • サンプル集 — 他の zfb サンプルサイト。

Revision History

作成更新