ゲーム感覚で楽しみながら世界の国旗を暗記できるWebアプリケーションです。
-
クイズモード: 国旗を見て国名を選ぶ、または国名を見て国旗を選ぶ2種類のクイズ形式で、スコアを競います。
- キーボードショートカット対応: Ctrl+Enter でクイズ開始、矢印キーで選択、Enterで回答送信
- 難易度設定: 簡単(4択)、普通(6択)、難しい(8択)
- 出題範囲設定: 全世界、アフリカ、アジア、ヨーロッパ、北アメリカ、南アメリカ、オセアニア
- 画像プリロード: 次の問題の画像を事前読み込みし、スムーズな問題移行を実現
-
学習モード: フラッシュカード形式で、国旗、国名、首都、大陸、地図、概要、国旗の由来をじっくり学習できます。
- キーボードショートカット: 矢印キーで前後移動、スペースキーで表裏切り替え
- 長文はマウスホバー/タッチでポップアップ表示
- 画像の最適化: 即座に読み込まれる高速表示
-
ランキング機能: クイズで獲得したスコアを地域別・期間別・形式別で競い合えます。
- 地域別ランキング: 全世界、アフリカ、アジア、ヨーロッパ、北アメリカ、南アメリカ、オセアニア
- 期間別ランキング: 日次ランキング(当日のスコア)、全期間トップ5
- 形式別ランキング: 国旗→国名、国名→国旗で別々のランキング
- トップ3にはメダル表示(🥇🥈🥉)
- ニックネームの自動保存(localStorage)
-
多言語対応: 日本語・英語の切り替えに対応
- Vue.js (v3, Composition API)
- Vite
- Pinia
- Vue Router
- Tailwind CSS
- 再利用可能コンポーネント: DRY原則に基づいた設計
- 共通UIコンポーネント: LanguageSelector, LoadingSpinner, ErrorMessage, AppButton
- クイズ設定コンポーネント: RegionSelector, QuizFormatSelector
- 学習用コンポーネント: FlagCard, CountryDetailCard
- レスポンシブデザイン: モバイルファーストの設計
- スマホとPCで最適化された異なるレイアウト
- ランキング: PC版はテーブル、スマホ版はカード形式
- フォーム: 画面サイズに応じた適切なサイズとスペーシング
- バリデーションエラーの画面内表示
- CI/CD: GitHub Actions による自動テスト・デプロイパイプライン
- Hono
- Cloudflare Pages Functions
- Cloudflare D1 (データベース)
- Wikipedia (国旗画像、地図画像、国旗の由来、概要など)
- データ取得には wikijs を使用
- Wikidata (首都情報: P36、大陸情報のフォールバック: P30)
- Wikidata APIを直接使用し、言語別ラベルを取得
- 大陸情報は主にWikipediaのカテゴリ(
Category:〇〇の国/Countries in 〇〇)から取得し、取得できない場合のみWikidataにフォールバック
npm installWikipedia と Wikidata から国旗や国の情報を取得し、アプリケーションが使用する public/countries.ja.json と public/countries.en.json を生成します。
注意: この処理は外部APIにアクセスするため、時間がかかる場合があります(約30分〜1時間)。
- 国名/国旗ページマッピングの生成(任意)
npx tsx scripts/extract-flag-page-names.mts... (truncated for brevity in this query; update uses full README content) ...
- 国データの生成と画像ダウンロード
npm run batch:create-data各国について以下の情報を取得します:
- 国旗画像(`public/flags/` にダウンロード)
- 地図画像(`public/maps/` にダウンロード)
- 首都(Wikidata P36 から取得、言語別ラベル)
- 大陸(まずWikipediaのカテゴリHTMLから取得、失敗時はWikidata P30にフォールバック)
- カテゴリパターン: 日本語版 `Category:〇〇の国`、英語版 `Countries in 〇〇`
- 複数の大陸カテゴリに属する国(オーストラリア、パナマなど)に対応するため優先順位を設定
- 概要(Wikipedia の要約セクション)
- 国旗の由来(日本語版は「[国名]の国旗」ページ、英語版は日本語ページ内のリンクから自動検出)
処理は各国ごとに保存されるため、中断しても再実行時に続きから再開できます。
**特定の国のみ更新する場合**:
```bash
npm run batch:create-data "アメリカ"
```
国名の一部を指定することで、その国のみデータを更新できます。
**英語版の国旗の由来取得について**:
英語版では、日本語Wikipediaページ内の `https://en.wikipedia.org/wiki/Flag_of` で始まるリンクを自動的に検出して取得します。
これにより、国名のバリエーション("the", "Republic of" など)を気にすることなく、正確な英語版国旗ページにアクセスできます。
ローカルのD1データベースにテーブルを作成します。
npx wrangler d1 migrations apply world-flags-learning-db --local初回実行時に確認を求められた場合は Y を入力してください。
このコマンドで以下のテーブルが作成されます:
ranking_daily: 日次ランキング(nickname, region, format, date ごとに UNIQUE)ranking_all_time: 全期間ランキング(region と format ごとのベストスコア)
フロントエンド(Vite)とバックエンド(Wrangler)の開発サーバーを同時に起動します。
npm run dev起動後、ブラウザで http://localhost:5173 にアクセスしてください。
このプロジェクトでは Vitest を使用して包括的なテストを実行します。
# 全テストを実行(ウォッチモード)
npm test
# 全テストを1回だけ実行
npm run test:run
# UIでテストを実行
npm run test:ui
# カバレッジレポート付きでテストを実行
npm run test:coverageこのプロジェクトでは Biome を使用した高速なリンティングとフォーマットを行っています。
# コードのリントチェック
npm run lint
# リンターのエラーを自動修正
npm run lint:fix
# コードのフォーマット
npm run format✅ 113テスト、100%合格率
-
ストアのテスト (32テスト)
- Countries Store: データ取得、言語切り替え、フィルタリング (14テスト)
- Quiz Store: クイズ設定、問題生成、回答処理、スコア計算 (11テスト)
- Ranking Store: ランキング取得、スコア送信 (7テスト)
-
コンポーネントのテスト (55テスト)
- Home: 言語選択、ナビゲーション (8テスト)
- QuizSetup: 入力バリデーション、XSS対策、localStorage連携 (14テスト)
- Study: カード操作、地域フィルタリング、キーボードショートカット (22テスト)
- LazyImage: 遅延読み込み、Intersection Observer API、画像ロード (11テスト)
-
ユーティリティのテスト (16テスト)
- バリデーション: ニックネーム検証、セキュリティチェック、XSS対策
-
APIのテスト (10テスト)
- Server: スコア計算、入力検証、セキュリティチェック
- GitHub Actions による自動テスト実行(プルリクエスト時)
- テスト成功後の自動デプロイ(main/master ブランチへのマージ時)
詳細は テスト仕様書 を参照してください。
このプロジェクトでは、コミット前に自動的にコード品質チェックとフォーマットを実行します。
- 自動実行:
git commitすると、ステージングされたファイルに対して自動的にBiomeが実行されます - 自動修正: リンターが検出した問題は自動的に修正されます
- エラー防止: エラーがある場合、コミットはブロックされます
詳細は Pre-commit Hooks 設定ガイド を参照してください。
# リンターチェック
npm run lint
# リンターの自動修正
npm run lint:fix
# フォーマット
npm run formatこのアプリケーションは Cloudflare Pages にデプロイされます。
npx wrangler loginnpx wrangler d1 create world-flags-learning-db出力された database_id を wrangler.toml に設定してください。
npx wrangler d1 migrations apply world-flags-learning-db --remotenpm run deployCloudflare ダッシュボードで以下の設定を行います:
- https://dash.cloudflare.com/ にアクセス
- Workers & Pages → world-flags-learning を選択
- Settings タブ → Functions → D1 database bindings
- Add binding をクリック
- Variable name:
DB - D1 database:
world-flags-learning-dbを選択
- Variable name:
- Save をクリック
# 本番環境へデプロイ(ビルド + デプロイ)
npm run deploy
# プレビュー環境へデプロイ
npm run deploy:previewデプロイが完了すると、以下のような URL が表示されます:
https://[deployment-id].world-flags-learning.pages.dev
この URL にアクセスしてアプリケーションの動作を確認してください。
- プルリクエスト時: 自動的にテストを実行(マージ前の品質チェック)
- main/master ブランチへのマージ時: テスト成功後に自動デプロイ
- 国データの自動更新: 毎週日曜日に自動実行、または手動で実行可能
このワークフローにより、コードの品質を保ちながら安全にデプロイできます。
GitHub Actions により、Wikipedia と Wikidata から国データを定期的に更新できます:
自動実行: 毎週日曜日 0:00 (UTC) に自動的に実行
手動実行:
- GitHubリポジトリの Actions タブを開く
- Update Country Data ワークフローを選択
- Run workflow ボタンをクリック
このワークフローは以下を実行します:
npm run batch:create-dataによる国データの生成public/ディレクトリの変更を自動コミット(コミットメッセージ: "update by gha on {日時}")- 変更がある場合、自動的に Cloudflare Pages へデプロイ
- Cloudflare API トークンを取得: https://dash.cloudflare.com/profile/api-tokens
- Cloudflare アカウント ID を取得:
npx wrangler whoami - GitHub リポジトリの Settings → Secrets and variables → Actions
- 以下の2つの Secret を追加:
CLOUDFLARE_API_TOKEN: Cloudflare API トークンCLOUDFLARE_ACCOUNT_ID: Cloudflare アカウント ID
詳細は GitHub Actions 自動デプロイ設定ガイド を参照してください。
.
├── functions/ # Cloudflare Pages Functions (バックエンドAPI)
├── migrations/ # D1 データベースのマイグレーションファイル
├── public/ # 静的ファイル (国データJSON、ダウンロードされた国旗・地図画像など)
│ ├── flags/ # ダウンロードされた国旗画像
│ └── maps/ # ダウンロードされた地図画像
├── scripts/ # データ生成などのバッチスクリプト
└── src/
├── assets/ # 画像などのアセット
├── components/ # 共通コンポーネント
├── router/ # Vue Router の設定
├── server/ # (現在は未使用) Honoサーバーの旧格納場所
├── store/ # Pinia ストア (状態管理)
├── views/ # 各画面のVueコンポーネント
├── App.vue # アプリケーションのルートコンポーネント
├── main.ts # アプリケーションのエントリーポイント
└── style.css # グローバルスタイル
- このリポジトリにあった
public/_headersは Netlify 固有のフォーマットです。Cloudflare Pages では自動的に適用されないため、効果がありません。Netlify 用ファイルを削除し、Cloudflare 側で同等の設定を行ってください。
- オリジン(ビルド出力/S3 等)で Cache-Control やセキュリティヘッダーを付与する(最も簡単で確実)。
- Cloudflare ダッシュボードの Rules → Transform Rules / Response Header Modification でパスごとにヘッダーを追加・上書きする。
- Cloudflare Workers を使ってレスポンスを受け取りヘッダーを書き換える(細かい制御が必要な場合)。
-
/assets/*
- Cache-Control: public, max-age=31536000, immutable
-
/flags/, /maps/
- Cache-Control: public, max-age=2592000
-
/*.json
- Cache-Control: public, max-age=86400
-
/sw.js
- Cache-Control: public, max-age=3600
-
全ページ(セキュリティヘッダー)
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: geolocation=(), microphone=(), camera=()
- X-XSS-Protection: 1; mode=block (注:多くのモダンブラウザでは非推奨であり、Content-Security-Policy(CSP)を併用することを推奨します)
- https://dash.cloudflare.com/ にログインし、Pages サイトを選択します。
- Rules → Transform Rules(または Response Header Modification)を開きます。
- 条件(例: Path starts with /assets/)を作成し、レスポンスヘッダーを追加/上書きします。
- より柔軟な制御が必要なら Worker を作成してヘッダーを差し替える処理を実装します。
下記はオリジンからのレスポンスに対してパスごとに Cache-Control を上書きし、全レスポンスにセキュリティヘッダーを追加する最小限の例です。必要に応じて README に載せるか、リポジトリに scripts/worker.js として追加してください。
addEventListener("fetch", event => { event.respondWith(handle(event.request)); });
async function handle(request) { const url = new URL(request.url); const res = await fetch(request); const headers = new Headers(res.headers);
if (url.pathname.startsWith("/assets/")) { headers.set("Cache-Control", "public, max-age=31536000, immutable"); } else if (url.pathname.startsWith("/flags/") || url.pathname.startsWith("/maps/")) { headers.set("Cache-Control", "public, max-age=2592000"); } else if (url.pathname.endsWith(".json")) { headers.set("Cache-Control", "public, max-age=86400"); } else if (url.pathname === "/sw.js") { headers.set("Cache-Control", "public, max-age=3600"); }
headers.set("X-Frame-Options", "DENY"); headers.set("X-Content-Type-Options", "nosniff"); headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); headers.set("Permissions-Policy", "geolocation=(), microphone=(), camera=()"); headers.set("X-XSS-Protection", "1; mode=block");
return new Response(res.body, { status: res.status, statusText: res.statusText, headers }); }
- Cloudflare のキャッシュ設定(Edge Cache TTL、Cache Rules)とオリジン側の Cache-Control の組合せに注意してください。オリジンで正しく設定するのが最も確実です。
- X-XSS-Protection は多くの最新ブラウザで非推奨です。可能であれば CSP を導入してください。