Laravelでブログサイトを作る 10. テンプレートの作成

はじめに
今回は、サイトのデザインを作成します。
プログラム作ってからだと、「あれ足りない」「これ足りない」という事態が起きやすいので、先に、各URLへアクセスした際に、ただページを表示させるだけのものを作ります。
LaravelはBladeというテンプレートを採用しており、これを使用して構築していきます。
Bladeに限りませんが、継承機能がありますので、これを利用して構築していきます。
おおまかな構成は、以下のようにします。
+ resources/views
+ app.blade.php -> 全体の共通部分
+ front
| + app.blade.php -> 公開ページの共通部分
| + article.blade.php -> 記事1件のページ
| + list.blade.php -> 記事一覧のページ(親子カテゴリ絞り込み等でも同じテンプレートを使う)
+ admin
+ app.blade.php -> 管理ページの共通部分
+ articleEdit.blade.php -> 記事の編集ページ
+ articleList.blade.php -> 記事の一覧ページ
+ parent.blade.php -> 親カテゴリの一覧・編集ページ
+ child.blade.php -> 子カテゴリの一覧・編集ページ
Controller
TOPページのルーティングは、以下のようになっている。
Route::group(['prefix' => '/', 'namespace' => 'App\\Http\\Controllers\\'], function() {
Route::get('/', 'ArticleController@list')->name('top');
});
なので、TOPページにアクセスした時には、app/Http/Controllers/ArticleController.phpのlistメソッドが実行されます。
ArticleController.phpの内容を、以下のようにします。テンプレートを読み込んで表示するだけです。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
/**
* 一覧画面表示
*/
public function list()
{
// view()で引数としている文字列は、resources/views以下のテンプレートファイルを表しています。
// 「.」(ドット)でディレクトリが表現されるので、上記の場合は、
// `resources/views/front/articleList.blade.php`
// を表示します。
return view('front.articleList');
}
}
テンプレート・全体の共通部分
サイト全体の共通部分を作成します。
対象は、resources/views/app.blade.phpです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
{{--
***@yield*** は、子孫テンプレートで設定した値が入ります。
例えば、タイトルタグ内に`@yield('title')`と記述することで、
子孫テンプレートでそれぞれに設定して出し分けることが可能となります。
--}}
<title>サイトタイトル | @yield('title')</title>
{{--
***asset()*** は、https(またはhttp)からはじまる、public以下のURLを生成してくれます。
この方法で記述を統一することで、「画像やJS、CSSファイルをAWS S3に置く」などの
変更が起きた際、`env`を書き換えるだけで済むので、大変便利です。
--}}
<link rel="stylesheet" href="{{ asset('/css/style.css') }}">
<script type="text/javascript" src="{{ asset('/js/jquery.min.js') }}" charset="UTF-8"></script>
<script type="text/javascript" src="{{ asset('/js/main.js') }}" charset="UTF-8"></script>
<script src="https://kit.fontawesome.com/207c617789.js" crossorigin="anonymous"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
</head>
<body>
<header>
<h1><a href="/"><img src="{{ asset('/img/header.png') }}" alt="サイトタイトル" /></a></h1>
</header>
<main>
@yield('paging')
@yield('main')
<aside>
@yield('aside')
</aside>
@yield('paging')
</main>
<footer>
フッターテキスト
</footer>
</body>
</html>
テンプレート・公開ページの共通部分
先程のサイト全体(親)に対し、公開ページの共通部分(子)を作成します。
ファイルは、resources/views/front/app.blade.phpです。
{{--
***@extends*** は、継承元となるテンプレートを指定します。
ここでは、全体の共通部分`resources/views/app.blade.php`が対象です。
--}}
@extends('app')
{{--
***@section*** は、上位テンプレートで`@yield`を使用した箇所に挿入する内容を記述します。
@sectionから@endsectionまでの内容が対象となります。
以下の例では、`@yield('aside')`に出力されるサイドメニューの内容を記述しています。
--}}
@section('aside')
<section class="side">
<h3>検索</h3>
<div id="searchWrapper">
<form action="{{ route('list_search') }}" method="post" name="searchForm">
{{ csrf_field() }}
<input type="text" id="searchTxt" name="search" placeholder="サイト内検索" />
<a href="#" id="searchBtn"><i class="fas fa-search"></i></a>
</form>
</div>
</section>
@endsection
テンプレート・個別ページ
最後に、個別ページ(孫)を作成します。
ファイルは、resources/views/front/articleList.blade.phpです。
{{--
継承元のテンプレートファイルを指定します。
ルールは、Controller同様に「.」(ドット)でディレクトリの区切りです。
以下の場合は、`resources/views/front/app.blade.php`を表しています。
--}}
@extends('front.app')
{{--
@sectionは、一行で記述することも可能です。
以下のように、
--}}
@section('title', '記事一覧')
@section('paging')
<nav>
<ul class="pagination">
<li><a href="/" rel="prev" aria-label="« Previous"><<</a></li>
<li><a href="/">1</a></li>
<li><a href="/">2</a></li>
<li><a href="/">3</a></li>
<li class="active" aria-current="page"><span>4</span></li>
<li class="disabled" aria-disabled="true" aria-label="Next »">
<span aria-hidden="true">>></span>
</li>
</ul>
</nav>
@endsection
@section('main')
<article>
<section class="article">
<h2><a href="#">記事タイトル</a></h2>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<a href="#" class="more">続きを読む</a>
</section>
<section class="article">
<h2><a href="#">記事タイトル</a></h2>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<a href="#" class="more">続きを読む</a>
</section>
<section class="article">
<h2><a href="#">記事タイトル</a></h2>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<a href="#" class="more">続きを読む</a>
</section>
<section class="article">
<h2><a href="#">記事タイトル</a></h2>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<a href="#" class="more">続きを読む</a>
</section>
<section class="article">
<h2><a href="#">記事タイトル</a></h2>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<p>これはテストです。</p>
<a href="#" class="more">続きを読む</a>
</section>
</article>
@endsection
以上で、テンプレートの表示は完了です。
あとは、public/css/style.cssを編集してデザインを整えておきましょう。
http://192.168.56.10/で確認できます。
テンプレート・管理画面の共通部分
なお、管理画面の共通テンプレートは以下のようにしました。
違いとしては、管理画面用のCSSとJavaScriptを読み込むようにしています。
あとは、サイドメニューに各管理機能のリンクを設置しております。
@extends('app')
@section('title')
@yield('adminTitle')
@endsection
@section('main')
{{-- 管理画面専用のCSSとJavaScript --}}
<link rel="stylesheet" href="{{ asset('/css/admin.css?v=').'?'.time() }}">
<script type="text/javascript" src="{{ asset('/js/admin.js?v=').'?'.time() }}" charset="UTF-8"></script>
@yield('adminMain')
@endsection
@section('aside')
<section class="aside">
<h3>メニュー</h3>
<ul>
<li><a href="{{ route('admin_parent') }}">親カテゴリ管理</a></li>
<li><a href="{{ route('admin_child') }}">子カテゴリ管理</a></li>
<li><a href="{{ route('admin_article_list') }}">記事一覧</a></li>
<li><a href="{{ route('admin_article_edit') }}">記事新規作成</a></li>
</ul>
</section>
@endsection
Laravelでブログサイトを作る 09. ルーティング

はじめに
ルーティングは、どのURLでアクセスされた時に、どのコントローラの、どのメソッドを呼ぶかを指定します。
URL設計で挙げたURLを、routes/web.phpに記述していくことで設定します。
不要なものを削除(コメントアウト)
routes/web.phpには、動作確認の際に見たwelcomページと、認証機能を導入した際に追加されたdashboardページについての記述が存在しているはずです。
これらは、今後使用する予定が無いので、コメントアウトしてしまいます。
/*
Route::get('/', function () {
return view('welcome');
});
Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
*/
管理ページ
管理側はログインしていることがアクセス条件となります。
※認証機能についてはコチラで解説しています。'middleware' => 'auth'の記述を加えることで、簡単に実現できます。
また、Route::groupで、admin以下のアクセスについての設定をまとめることができます。
管理側のページ1件ずつに上記の記述を書くのは面倒なので、こうしてまとめれば簡単で、かつ「このグループは管理用」というのが分かりやすいです。
// 管理画面(認証後に触れるページ)
Route::group(
[
'prefix' => 'admin',
'middleware' => 'auth',
'namespace' => 'App\\Http\\Controllers\\'
],
function() {
// 親カテゴリ管理:一覧(入力欄込)表示
Route::get('parent', 'ParentCategoryController@list')->name('admin_parent');
// 親カテゴリ管理:更新処理
Route::post('parentUpdate', 'ParentCategoryController@update')->name('admin_parent_update');
// 子カテゴリ管理:一覧(入力欄込)表示
Route::get('child', 'ChildCategoryController@list')->name('admin_child');
// 子カテゴリ管理:更新処理
Route::post('childUpdate', 'ChildCategoryController@update')->name('admin_child_update');
// 記事:入力フォーム
Route::get('articleEdit/{id?}', 'ArticleController@edit')->name('admin_article_edit');
// 記事:更新処理
Route::post('articleUpdate', 'ArticleController@update')->name('admin_article_update');
// 画像アップロード
Route::post('articleImageUpload', 'ArticleController@image')->name('admin_article_image_upload');
// 記事一覧
Route::get('articleList', 'ArticleController@adminList')->name('admin_article_list');
// 記事一覧(親カテゴリ絞り)
Route::get('articleList/parent/{parent}', 'ArticleController@adminParentList')->name('admin_article_list_parent');
// 記事一覧(子カテゴリ絞り)
Route::get('articleList/child/{child}', 'ArticleController@adminChildList')->name('admin_article_list_child');
// 記事一覧(検索)
Route::post('articleList/search/', 'ArticleController@adminSearchList')->name('admin_article_list_search');
// 記事プレビュー
Route::post('articlePreview', 'ArticleController@preview')->name('admin_article_preview');
// 「/admin/」にアクセスされたら、記事一覧へ
Route::get('/', 'ArticleController@adminList')->name('admin_top');
});
小ネタ
prefixの値を変更することで、管理画面のURLを外部から推測されづらいようにします。
たとえは、'prefix' => 'qawsedrftgyhujikolp',としておくと、管理画面のURLは「https://ドメイン/qawsedrftgyhujikolp/」となるので、URLを直接指定されるようなアクセスを防ぐことができます。
フロントページ
それぞれに->name('XXXXX')を付けることで、後から利用しやすくします。
詳しくはbladeテンプレートを作成する際に記載しますが、href="{{ route('XXXXX') }"のように記述することで、URLを生成してくれるのです。
例えば、「親カテゴリ一覧のURLを/p/{id}にしよう」となった時、routes/web.phpを修正し、nameの方はそのままにしておけば、出現箇所すべてを修正する必要はなくなるのです。
// フロントページ
Route::group(['prefix' => '/', 'namespace' => 'App\\Http\\Controllers\\'], function() {
// 一覧(http://ドメイン/)
Route::get('/', 'ArticleController@list')->name('top');
// 記事1件表示(http://ドメイン/content/XX)
Route::get('content/{id}', 'ArticleController@content')->name('content');
// 親カテゴリ別記事一覧(http://ドメイン/parent/XX/)
Route::get('parent/{id}', 'ArticleController@parentL')->name('list_parent');
// 子カテゴリ別記事一覧(http://ドメイン/child/XX/)
Route::get('child/{id}', 'ArticleController@childList')->name('list_child');
// 検索結果一覧(http://ドメイン/search/)
Route::post('search/', 'ArticleController@searchList')->name('list_search');
});
Laravelでブログサイトを作る 08. テーブル生成

設計について
設計はデータベース定義にて行っております。
これを、migrationに落とし込み、テーブルを作成します。
Laravelのmigrationについてはコチラを参考に。
親カテゴリmigrationファイル
createするテーブルのカラムについては、upメソッド内で設定します。
主キーを、デフォルトの$table->id();ではなく、increments('id')としています。
これは、$table->id()を使ってしまうと、BIGINT型になってしまうからです。
BIGINTの方が大量の上限値が上ですが、符号ありで考えても
INT型:約21億
BIGINT型:約922京(1000兆の922倍)
なので、個人運営サイトでわざわざBIGINTにするほどではないかなぁ、という考えです。
(とはいえ、ギリギリを詰めなければならない状況でもないので、MEDIUMINT型にもしていません)
対象ファイル:database/migrations/XXXX_XX_XX_XXXXXX_create_parent_categories_table.php
// ...省略...
public function up()
{
Schema::create('parents', function (Blueprint $table) {
$table->increments('id'); // 主キー(自動採番のINT型)
$table->text('name'); // カテゴリ名
$table->integer('number')->index(); // 表示順
$table->softDeletes(); // ソフトデリートのためにNULL値可能なdeleted_at TIMESTAMPカラム追加
$table->timestamps(); // NULL値可能なcreated_atとupdated_atカラム追加
});
}
// ...省略...
子カテゴリmigrationファイル
対象ファイル:database/migrations/XXXX_XX_XX_XXXXXX_create_child_categories_table.php
// ...省略...
public function up()
{
Schema::create('parents', function (Blueprint $table) {
$table->increments('id'); // 主キー(自動採番のINT型)
$table->text('name'); // 子カテゴリ名
$table->integer('number')->index(); // 表示順
$table->integer('parent'); // 親カテゴリID
$table->softDeletes(); // ソフトデリートのためにNULL値可能なdeleted_at TIMESTAMPカラム追加
$table->timestamps(); // NULL値可能なcreated_atとupdated_atカラム追加
});
}
// ...省略...
記事migrationファイル
対象ファイル:database/migrations/XXXX_XX_XX_XXXXXX_create_articles_table.php
// ...省略...
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->increments('id'); // 主キー(自動採番のINT型)
$table->text('title'); // 記事タイトル
$table->text('body'); // 本文
$table->integer('parent')->index(); // 親カテゴリID
$table->softDeletes(); // ソフトデリートのためにNULL値可能なdeleted_at TIMESTAMPカラム追加
$table->timestamps(); // NULL値可能なcreated_atとupdated_atカラム追加
});
}
// ...省略...
記事-子カテゴリmigrationファイル
対象ファイル:database/migrations/XXXX_XX_XX_XXXXXX_create_article_children_table.php
// ...省略...
public function up()
{
Schema::create('parents', function (Blueprint $table) {
$table->increments('id'); // 主キー(自動採番のINT型)
$table->integer('article')->index(); // 符記事ID
$table->integer('children')->index(); // 子カテゴリID
$table->softDeletes(); // ソフトデリートのためにNULL値可能なdeleted_at TIMESTAMPカラム追加
$table->timestamps(); // NULL値可能なcreated_atとupdated_atカラム追加
});
}
// ...省略...
テーブル作成
サーバに接続し、以下のコマンドを実行します。
$ php artisan migrate
先述の4ファイルそれぞれで、MigratingとMigratedが表示され、テーブルが作成されます。
MySQLでテーブル一覧を表示すると、作成されていることが確認できます。
$ mysql -u root -p
Enter password: [コメント]設定したパスワードを入力(過去記事では`NEW_pass_123`を設定)
mysql> use blog
mysql> show tables;
+------------------------+
| Tables_in_blog |
+------------------------+
| article_children |
| articles |
| children |
| failed_jobs |
| migrations |
| parents |
| password_resets |
| personal_access_tokens |
| sessions |
| team_invitations |
| team_user |
| teams |
| users |
+------------------------+
13 rows in set (0.00 sec)
Modelの編集
app/Models以下にあるファイルを、テーブルの構成に合わせて編集します。
記事(Article.php)をサンプルにします。
- 論理削除のため、SoftDeletesを追加
- 主キーのカラム名を追加
- INSERTやUPDATE時に、入力値を保存するカラムを指定
- 日時関係のカラムを指定
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; // 【追加】1.論理削除用
class Article extends Model
{
use HasFactory;
use SoftDeletes; // 【追加】1.論理削除用
// 【追加】2.主キー
protected $primaryKey = 'id';
// 【追加】3.入力値用カラム指定
protected $fillable = ['title', 'body', 'parent'];
// 【追加】2.日時系カラム
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
}
Laravelでブログサイトを作る 06. Laravelに認証機能を導入

はじめに
前回の作業を完了し、http://192.168.56.10/にアクセスすると、Laravelのwelcome画面が表示されることを前提に進めます。
導入
認証には、Laravel Jetstreamを使用します。
インストールは、composerとnpmを使用して行います。
# [コメント]サーバ内の作業ディレクトリへ移動します
$ cd /home/vagrant/blog
# [コメント]インストール実行
$ composer require laravel/jetstream
$ php artisan jetstream:install livewire --teams
$ npm install
$ npm run dev
# [コメント]必要なテーブルを作成します
$ php artisan migrate
ユーザの登録
http://192.168.56.10/registerにアクセスすると、登録画面が表示されます。

名前、メールアドレス、パスワードを入力し、「REGISTER」ボタンをクリックすれば登録は完了です。
http://192.168.56.10/loginからログインできるか試してみましょう。
2要素認証
2要素認証(メールアドレスとパスワードの知識要素と、スマートフォンを使った所有要素)でのログイン制御が可能です。それも、かなり簡単に。
1. アプリインストール【スマートフォン側で作業】
公式が案内している、「Google Authenticator application」をスマートフォンにインストールします。
Android:Google認証システム
iPhone:Google Authenticator
名前は違いますが、どちらも同様のアプリ、のはずです。(私がiPhoneを所有していないので、そちらは未確認です)
インストールしただけではまだ何もできないので、そのままスマートフォンは一旦置いておきます。
2. サイトのユーザ設定を変更【PC側で作業】
ログイン後、ダッシュボードの右上から「Profile」を選択します。

「Two Factor Authentication」のところで、ENABLEをクリックします。


3. アプリを起動し、QRコード読み込み【スマートフォン側で作業】
アプリを起動し、「QRコードをスキャン」を選択します。
後は、上記2の後に表示されたQRコードを、カメラで読み取ります。
「アカウント追加」ボタンを押しておけば、アプリにこのサイトが記憶されます。
これで、準備は完了です。
4. ログインしてみる【PC側で作業】
試しに、一度ログアウトして、もう一度ログインします。
メールアドレスとパスワードを入力してログインボタンを押すと

コードの入力画面が表示されます。

ここに、アプリで表示される6桁の数字を入力することで、初めてログインが完了するようになりました。
ユーザの登録をできないようにする
今回制作するのは、自分だけが管理画面にアクセスできれば良いので、登録画面へのアクセスをできなくしてしまいます。config/fortify.phpの最後の方にある、Features::registration(),の行をコメントアウトしてしまいます。
すると、登録画面にアクセスしても、404 NOT FOUNDになります。
他のメニューも、必要に応じてコメントアウトしても構わないでしょう。
'features' => [
//Features::registration(), // 登録。今回コメントアウトするもの。
Features::resetPasswords(), // パスワードリセット。
// Features::emailVerification(), // メール認証。デフォルトでコメントアウト済み。
Features::updateProfileInformation(), // プロフィール更新。
Features::updatePasswords(), // パスワードの更新。
Features::twoFactorAuthentication([ // 2段階認証
'confirmPassword' => true,
]),
],
