# 海外大学検索サービス「College Spark」
URL: https://college-spark.com/
## 概要
アメリカ・オーストラリア・カナダ・ニュージーランドの大学情報を検索できるWebサービス。**企画・設計・実装・インフラ・SEO・コンテンツ・運用保守まで全工程を1人で担当**。約1年4ヶ月にわたり継続開発し、直近半年も週次で改善をリリースし続けている。サブスクリプション課金まで実装し、現在も運用中。
## 技術スタック
- **Backend**: Ruby on Rails 8.0, Ruby 3.3, PostgreSQL(本番)/ SQLite(開発)
- **Frontend**: Bootstrap 5.3 + 独自デザインシステム(SCSS/dartsass), Hotwire(Turbo/Stimulus), JavaScript ES6+
- **Infrastructure**: Render.com, GitHub Actions(CI/CD)
- **Payment**: Stripe(Checkout / サブスクリプション / Webhook署名検証)
- **Authentication**: Google OAuth 2.0(OmniAuth)
- **Testing**: Minitest(353テスト / 688アサーション)
- **External API**: College Scorecard API, Wikimedia Commons API
- **SEO**: Schema.org 構造化データ(JSON-LD), sitemap_generator, Google Search Console 運用
---
## 取り組み詳細①:検索エンジン経由・AI経由の流入を伸ばすSEO/GEOエンジニアリング
### 課題
- 検索エンジンのアルゴリズム変動により、ある時期から検索流入が大きく落ち込んだ。原因が不明なまま施策を打っても効果検証ができない状態だった
- 多言語対応の過程で `?locale=en` 付きの重複URLが大量にインデックスされ、評価が分散していた
- 近年は検索結果がAI要約(AI Overview / 生成AIの回答)に吸収され、従来のSEOだけでは引用・露出されない構造になっていた
### 工夫
- **データに基づく原因特定**: Google Search Console の実測データ(表示・クリック・順位・カバレッジ)をCSVで時系列分析し、「48時間でほぼ全クエリの順位が落ちるスイッチ型の下落=アルゴリズムによる分類」と特定。推測ではなく一次データで切り分けた
- **重複URLの解消**: 英語版を `noindex` 化し、`canonical`・`hreflang` を整理。サイトマップを実在URLのみに絞り込み(8,900→約1,600)、クロール対象を収束させた
- **GEO(生成AI最適化)への着手**: 大学ページ・ガイドページに **Schema.org 構造化データ(FAQPage / Organization など、計93ビュー)** を実装。偏差値・学費などの数値をAIが抽出しやすいマークアップに整形し、出典・更新日を明示してE-E-A-T(専門性・権威性・信頼性)を強化
- **クラスタ戦略**: 留学ガイド16ページ+大学プロフィールを内部リンクで接続し、検索意図ごとの導線を設計
### 成果
- アルゴリズム下落の原因をデータで確定し、**重複インデックス問題を本番環境で解消**(カバー率98%まで収束)
- 構造化データをサイト全体に展開し、生成AIに引用されうる情報設計へ移行
- GSCを定常監視し、施策の効果を数値で検証し続ける運用体制を構築
---
## 取り組み詳細②:6,400校以上の複合検索機能とパフォーマンス改善
### 課題
- College Scorecard APIから6,400校以上の大学データを取得する必要があったが、**APIレスポンスのフィールド名がDBスキーマと異なる**ため、手動でマッピング定義が必要だった
- 「学費」「SAT/ACT」「専攻」「地域」「学校規模」など**10以上の条件を組み合わせて検索**するため、クエリが複雑化。初期実装では検索に**3〜5秒**かかっていた
- 外部APIのレート制限があり、6,400校のデータを効率的に取得・更新する仕組みが必要だった
### 工夫
- **データ取得の自動化**: Rakeタスクを64本作成し、API連携・データ更新・本番反映を自動化。バッチ処理でレート制限を回避しつつ、差分更新で既存データとの整合性を担保
- **検索最適化**: 頻出条件(州・学費範囲・学校タイプ)に**複合インデックス**を設定。N+1問題はeager loadingで解消
- **スコープによるクエリ分離**: 各フィルター条件をActiveRecordスコープとして分離し、組み合わせを柔軟に対応。可読性と保守性を両立
### 成果
- 検索速度を**3〜5秒から1秒程度に改善**
- 6,400校以上のデータを正確に反映し、継続的に更新可能な仕組みを構築
- 複合条件検索でもストレスなく動作するUXを実現
---
## 取り組み詳細③:Stripeによるサブスクリプション課金とプラン設計
### 課題
- 無料機能だけでは収益化できず、有料プランを導入する必要があった
- 決済まわりは**セキュリティ要件が厳しく**、Webhookの改ざんやアカウント削除時のサブスク残存といった事故を防ぐ必要があった
- 無料/有料でアクセスできる機能を出し分ける認可ロジックが必要だった
### 工夫
- **3段階プラン(Free / Plus / Premium)**を設計し、Stripe Checkoutで決済フローを実装
- **Webhook署名検証**(`Stripe::Webhook.construct_event`)を必須化し、CSRF保護の例外はWebhookのみに限定。署名検証なしの受信を禁止
- **アカウント削除時にサブスクリプションを確実に解約**する処理を実装し、課金事故を防止
- Plus限定の検索フィルターなど、プラン別の機能出し分けを認可ロジックで制御
### 成果
- 決済〜解約〜認可を一気通貫で実装し、サブスク課金を本番稼働
- 署名検証・削除時解約により、決済関連の事故リスクを設計段階で排除
---
## その他の実装
- **多言語検索**: 正規表現 `\p{Hiragana}|\p{Katakana}|\p{Han}` で日本語入力を判定し検索先を動的に切替。`UniversityTranslation` を `condition_id` × `locale` の複合ユニーク制約で設計し、1,511件の日本語大学名を整備。中韓など将来拡張も可能
- **コンテンツ運用の仕組み化**: 1,511校の大学プロフィール(紹介文・著名卒業生)を執筆規約に沿って制作。事実確認とライティング規約違反を **pre-commit hook で自動チェック**する品質ゲートを構築
- **scout(事前登録/リードジェン)機能**: 専用LP・事前登録フォーム・管理画面・CSV出力・重複登録防止を実装
- お気に入り・比較・閲覧履歴(ユーザーごとのデータ管理)
- 管理画面(ニュース/ブログ/相談のCRUD、Basic認証+ロールベース認証、ログインロックアウト)
- Action Mailerによるメール通知
## セキュリティ対策
- CSRF/XSS対策、認証・認可の適切な実装、Stripe Webhook署名検証
- 環境変数による機密情報管理、独自ドメインメールへの移行
- 認証はGoogle OAuthに一本化。パスワード保管・リセット・ロックアウト等の自前運用をなくし、1人運用での保守負荷とセキュリティリスクを同時に下げる判断
- リリース前の自己監査でデバッグ用エンドポイントを検出し、本番公開前に除去
## 品質管理
- モデル/コントローラ層を中心にテスト整備(353テスト / 688アサーション)
- RuboCopによるコード品質維持、pre-commit hookによるコンテンツ品質ゲート
## 出したバリュー
- 企画〜課金〜SEO〜運用まで**全工程を1人で完遂**
- 6,400校・1,500プロフィールを扱う検索&コンテンツシステムを設計・運用
- **アルゴリズム変動をデータで原因特定し、構造化データ/GEOで対策**する、推測に頼らない問題解決
- アメリカ大学留学経験を活かした実ユーザー視点のUX設計