404 Not Found

404 Not Found


nginx

第1フェーズ総合演習—ShopMetricsランディングページ

第1フェーズの総合演習は「期末レビュー」です—最初の6レッスンで学んだすべてを統合し, 実際に機能するランディングページを完成させます。

1. 学ぶ内容


2. Bobの第1フェーズ検収ストーリー

(1) 悩み:7レッスン終えても, 完全なページが構築できない

Aliceは最初の6レッスンを終えましたが, 各概念を個別には理解していても, 組み合わせると混乱してしまいます—ルーティングはどうコントローラに繋がるのか?コントローラはどうBladeにデータを渡すのか?レイアウト継承とコンポーネントのどちらを選ぶべきか?Bobは言いました。「水泳の練習と実際に水に入ることの違いだよ—7回の理論講義を見ても, 25メートル泳いでいないのと同じだ。」

(2) 総合演習による解決策

このレッスンでは, ルート, ビュー, コントローラ, Bladeコンポーネント, 環境設定を結びつけて, ゼロから完全に機能するShopMetricsランディングページを構築します—これがあなた初の実践プロジェクトになります。

BASH
# このレッスン完了後, 以下が完成します:
# - ヒーローセクション付きランディングページ
# - プランカード付き料金ページ
# - サイドバー付きダッシュボードレイアウト
# - 再利用可能なBladeコンポーネント
# - 動作するデータベース接続

(3) 成果

総合演習を完了した後, Aliceは独立してShopMetricsランディングページを構築し, 5ページすべてが検収テストに合格しました。Bobは言いました。「『理解すること』と『実装すること』の差は, たった1つの総合演習だ。」


3. ルーティング設計図の設計

(1) ページ構成

100%
graph LR
    A["/ (ホーム)"] --> B["/pricing (料金)"]
    A --> C["/about (概要)"]
    A --> D["/contact (お問い合わせ)"]
    A --> E["/dashboard (ダッシュボード)"]
    E --> F["/dashboard/shops"]
    E --> G["/dashboard/analytics"]

(2) ルート定義

PHP
// routes/web.php
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/pricing', [PricingController::class, 'index'])->name('pricing');
Route::get('/about', [AboutController::class, 'index'])->name('about');
Route::get('/contact', [ContactController::class, 'create'])->name('contact.create');
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store');

Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () {
    Route::get('/', [DashboardController::class, 'index'])->name('index');
    Route::resource('shops', Dashboard\ShopController::class);
});
ページ ルート コントローラ ミドルウェア
ホーム GET / HomeController なし
料金 GET /pricing PricingController なし
概要 GET /about AboutController なし
お問い合わせ GET+POST /contact ContactController なし
ダッシュボード GET /dashboard DashboardController auth

(1) ▶ サンプル:すべてのコントローラを作成

BASH
php artisan make:controller HomeController
php artisan make:controller PricingController
php artisan make:controller AboutController
php artisan make:controller ContactController
php artisan make:controller Dashboard/DashboardController
php artisan make:controller Dashboard/ShopController --resource

出力:

TEXT
# コマンド実行成功

4. レイアウトとコンポーネントの設計

(1) レイアウト階層

TEXT
layouts/app.blade.php        ← メインレイアウト (ナビバー + フッター)
├── layouts/dashboard.blade.php  ← ダッシュボードレイアウト (サイドバー + コンテンツ)
│   └── dashboard/analytics.blade.php
└── home/index.blade.php     ← ランディングページ

(2) コンポーネント一覧

コンポーネント 種別 ファイル 目的
<x-navbar> 匿名 components/navbar.blade.php サイト全体のナビゲーションバー
<x-footer> 匿名 components/footer.blade.php サイト全体のフッター
<x-hero> 匿名 components/hero.blade.php ホームページヒーローエリア
<x-plan-card> 匿名 components/plan-card.blade.php 料金カード
<x-stat-card> 匿名 components/stat-card.blade.php ダッシュボード統計カード
<x-sidebar> 匿名 components/sidebar.blade.php ダッシュボードサイドバー

(1) ▶ サンプル:Bladeコンポーネントの作成

BASH
# コンポーネントファイルを手動作成
mkdir -p resources/views/components
touch resources/views/components/navbar.blade.php
touch resources/views/components/footer.blade.php
touch resources/views/components/hero.blade.php
touch resources/views/components/plan-card.blade.php
touch resources/views/components/stat-card.blade.php
touch resources/views/components/sidebar.blade.php

出力:

TEXT
# コマンド実行成功

5. コアレイアウトとコンポーネントの実装

(1) メインレイアウト

HTML
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'ShopMetrics') — E-commerce Analytics</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50 text-gray-900">
    <x-navbar />
    <main>
        @yield('content')
    </main>
    <x-footer />
</body>
</html>

(2) ナビゲーションバーコンポーネント

HTML
<!-- resources/views/components/navbar.blade.php -->
<nav class="bg-white shadow-sm" x-data="{ open: false }">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
            <a href="{{ route('home') }}" class="flex items-center font-bold text-xl text-blue-600">
                ShopMetrics
            </a>
            <div class="hidden md:flex items-center space-x-6">
                <a href="{{ route('pricing') }}" class="text-gray-600 hover:text-gray-900">Pricing</a>
                <a href="{{ route('about') }}" class="text-gray-600 hover:text-gray-900">About</a>
                @auth
                    <a href="{{ route('dashboard.index') }}" class="btn-primary">Dashboard</a>
                @else
                    <a href="{{ route('login') }}" class="text-gray-600 hover:text-gray-900">Login</a>
                    <a href="{{ route('register') }}" class="btn-primary">Sign Up</a>
                @endauth
            </div>
            <button @click="open = !open" class="md:hidden">
                <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path x-show="!open" d="M4 6h16M4 12h16M4 18h16"/>
                    <path x-show="open" d="M6 18L18 6M6 6l12 12"/>
                </svg>
            </button>
        </div>
        <div x-show="open" class="md:hidden py-2 space-y-2">
            <a href="{{ route('pricing') }}" class="block py-2 text-gray-600">Pricing</a>
            @auth
                <a href="{{ route('dashboard.index') }}" class="block py-2 btn-primary">Dashboard</a>
            @else
                <a href="{{ route('login') }}" class="block py-2 text-gray-600">Login</a>
                <a href="{{ route('register') }}" class="block py-2 btn-primary">Sign Up</a>
            @endauth
        </div>
    </div>
</nav>

(3) ヒーローコンポーネント

HTML
<!-- resources/views/components/hero.blade.php -->
@props(['title', 'subtitle', 'cta_text' => 'Get Started', 'cta_route' => 'register'])

<section class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-20">
    <div class="max-w-7xl mx-auto px-4 text-center">
        <h1 class="text-4xl md:text-6xl font-bold mb-6">{{ $title }}</h1>
        <p class="text-xl md:text-2xl mb-8 text-blue-100">{{ $subtitle }}</p>
        <a href="{{ route($cta_route) }}" class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition">
            {{ $cta_text }}
        </a>
    </div>
</section>

(1) ▶ サンプル:料金カードコンポーネント

HTML
<!-- resources/views/components/plan-card.blade.php -->
@props(['plan', 'popular' => false])

<div class="bg-white rounded-xl shadow-lg p-8 {{ $popular ? 'ring-2 ring-blue-500 relative' : '' }}">
    @if($popular)
        <span class="absolute -top-3 left-1/2 -translate-x-1/2 bg-blue-500 text-white px-4 py-1 rounded-full text-sm font-semibold">
            Most Popular
        </span>
    @endif
    <h3 class="text-xl font-bold mb-2">{{ $plan['name'] }}</h3>
    <div class="mb-6">
        <span class="text-4xl font-bold">${{ $plan['price'] }}</span>
        <span class="text-gray-500">/month</span>
    </div>
    <ul class="space-y-3 mb-8">
        @foreach($plan['features'] as $feature)
            <li class="flex items-center">
                <svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
                    <path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
                </svg>
                {{ $feature }}
            </li>
        @endforeach
    </ul>
    <a href="{{ route('register') }}" class="{{ $popular ? 'btn-primary' : 'btn-secondary' }} w-full text-center block">
        Choose {{ $plan['name'] }}
    </a>
</div>

出力:

TEXT
// 実行成功

6. コントローラの実装

(1) ▶ サンプル:HomeControllerとPricingController

PHP
// app/Http/Controllers/HomeController.php
class HomeController extends Controller
{
    public function index(): View
    {
        $features = [
            ['icon' => 'chart', 'title' => 'Real-time Analytics', 'desc' => 'Track sales, orders and revenue in real-time.'],
            ['icon' => 'users', 'title' => 'Multi-tenant', 'desc' => 'Separate data for each e-commerce store.'],
            ['icon' => 'bell', 'title' => 'Smart Alerts', 'desc' => 'Get notified when key metrics change.'],
        ];
        return view('home.index', compact('features'));
    }
}

// app/Http/Controllers/PricingController.php
class PricingController extends Controller
{
    public function index(): View
    {
        $plans = [
            ['name' => 'Starter', 'price' => 29, 'features' => ['5 Shops', '1K Orders/month', 'Basic Analytics', 'Email Support']],
            ['name' => 'Pro', 'price' => 79, 'features' => ['25 Shops', '10K Orders/month', 'Advanced Analytics', 'Priority Support', 'API Access']],
            ['name' => 'Enterprise', 'price' => 199, 'features' => ['Unlimited Shops', 'Unlimited Orders', 'Custom Analytics', 'Dedicated Support', 'API + Webhooks', 'SSO']],
        ];
        return view('pricing.index', compact('plans'));
    }
}

出力:

TEXT
// 実行成功

(2) ▶ サンプル:DashboardController

PHP
// app/Http/Controllers/Dashboard/DashboardController.php
class DashboardController extends Controller
{
    public function index(): View
    {
        $user = auth()->user();
        $tenant = $user->tenant;

        $stats = [
            ['label' => 'Total Revenue', 'value' => '$' . number_format($tenant->revenue ?? 0), 'trend' => 12.5],
            ['label' => 'Active Shops', 'value' => $tenant->shops()->count(), 'trend' => 3.2],
            ['label' => 'Orders Today', 'value' => $tenant->orders()->today()->count(), 'trend' => -2.1],
        ];

        return view('dashboard.index', compact('tenant', 'stats'));
    }
}

出力:

TEXT
// 実行成功

7. 環境設定とデータベース

BASH
# .env — ShopMetrics用MySQL設定
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=shopmetrics
DB_USERNAME=root
DB_PASSWORD=secret

# アプリキーの生成とマイグレーションの実行
php artisan key:generate
php artisan migrate

# 認証スキャフォールド用Breezeのインストール
composer require laravel/breeze --dev
php artisan breeze:install blade
npm install && npm run build
手順 コマンド 機能
1 key:generate APP_KEYの生成
2 migrate データテーブルの作成
3 breeze:install ログイン/登録ページの生成
4 npm run build フロントエンドアセットのコンパイル

8. 総合例:ShopMetricsランディングページのホーム

HTML
<!-- ============================================
     総合例: ShopMetricsランディングページ
     対象: レイアウト, コンポーネント, ルート, データ渡し
     ============================================ -->

<!-- resources/views/home/index.blade.php -->
@extends('layouts.app')

@section('title', 'ShopMetrics — E-commerce Analytics Platform')

@section('content')
    <!-- ヒーローセクション -->
    <x-hero
        title="Track Your E-commerce Performance"
        subtitle="Real-time analytics for multi-tenant e-commerce platforms. Monitor revenue, orders, and customer insights — all in one dashboard."
        cta_text="Start Free Trial"
        cta_route="register"
    />

    <!-- 特徴セクション -->
    <section class="py-20 bg-white">
        <div class="max-w-7xl mx-auto px-4">
            <h2 class="text-3xl font-bold text-center mb-12">Why ShopMetrics?</h2>
            <div class="grid md:grid-cols-3 gap-8">
                @foreach($features as $feature)
                    <div class="text-center p-6">
                        <div class="w-12 h-12 bg-blue-100 rounded-lg mx-auto mb-4 flex items-center justify-center">
                            <span class="text-blue-600 text-2xl">
                                @if($feature['icon'] === 'chart')📈@elseif($feature['icon'] === 'users')👥@else🔔@endif
                            </span>
                        </div>
                        <h3 class="text-xl font-semibold mb-2">{{ $feature['title'] }}</h3>
                        <p class="text-gray-600">{{ $feature['desc'] }}</p>
                    </div>
                @endforeach
            </div>
        </div>
    </section>

    <!-- CTAセクション -->
    <section class="py-16 bg-gray-100">
        <div class="max-w-4xl mx-auto text-center px-4">
            <h2 class="text-3xl font-bold mb-4">Ready to grow your business?</h2>
            <p class="text-gray-600 mb-8">Join 500+ merchants who trust ShopMetrics for their analytics.</p>
            <div class="flex gap-4 justify-center">
                <a href="{{ route('register') }}" class="btn-primary">Start Free Trial</a>
                <a href="{{ route('pricing') }}" class="btn-secondary">View Pricing</a>
            </div>
        </div>
    </section>
@endsection

❓ よくある質問

Q ランディングページに認証は必要ですか?
A ホームページ, 料金ページ, 概要ページは認証不要で誰でもアクセスできます。ダッシュボードはauthミドルウェアで保護されています。Breezeがログイン/登録ルートを自動的に処理しています。
Q レイアウト継承とコンポーネントはどう使い分けるべきですか?
A ページレベルの構造 (ナビバーやフッターでコンテンツを包むなど)にはレイアウト継承を使用し, 再利用可能なUIフラグメント (カード, ボタン, フォームなど)にはBladeコンポーネントを使用してください。レイアウトが「骨格」を定義し, コンポーネントが「肉」を埋めます。
Q Tailwind CSSが動作しない場合はどうすればよいですか?
A npm run dev (開発モードのホットリロード)またはnpm run build (本番ビルド)を実行していることを確認し, tailwind.config.jscontent設定にすべてのBladeファイルのパスが含まれているか確認してください。
Q Alpine.jsのx-showはVueのv-showと同じですか?
A 似た目的を持ちますが, Alpine.jsは軽量 (15 KB)のスタンドアロンライブラリでビルドプロセスが不要です。Vueのv-showはVueインスタンスが必要です。簡単な操作にはAlpine.js, 複雑なシングルページアプリケーションにはVueを使用してください。
Q BreezeとJetstreamの違いは何ですか?
A Breezeは軽量な認証スキャフォールド (Blade, Inertia, Livewireから1つ選択)で, コードが少なく理解しやすいです。Jetstreamはより機能豊富なソリューション (チーム管理や2FAを含む)ですが, より複雑です。まずBreezeから始めることをお勧めします。
Q 複数のコントローラ間で共有データを処理するにはどうすればよいですか?
A View Composerを使用して共有データ (ナビバーのストア一覧など)を自動注入するか, Serviceクラスを作成してコントローラ間でロジックを共有してください。基底コントローラクラスでデータを共有しないでください。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐):ShopMetricsランディングページの5ページ (ホーム/料金/概要/お問い合わせ/ダッシュボード)を完成させ, すべてのルートがアクセス可能, レイアウト継承が正常に動作, ナビバーのリンクが正しいことを確認してください。

  2. 応用問題 (⭐⭐):料金ページに月額/年額の切り替え機能を追加してください (Alpine.jsを使用)。年額価格は20%オフとし, 支払いオプションの切り替え時にカード価格がアニメーションで推移するようにしてください。

  3. チャレンジ (⭐⭐⭐):ダッシュボードサイドバーに動的メニューを実装してください—ユーザーのロール (admin/tenant_owner/analyst)に基づいて異なるメニュー項目を表示し, View Composerを使ってデータベースから権限設定を取得してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%