- 分類: 軟體文章
larablog 建構日誌:文章與 Controller
在 Voyager 建立文章分類、標籤以及文章項目
如何在網站上建立文章?以使用者的立場我想到的是:
- 有一個登入網站的頁面,在此輸入登入帳號及密碼後登入網站。
- 如果這個帳號具備建立文章的權限,那麼畫面中會有一個名為「建立文章」,或類似意思的連結或圖示,讓我點選後建立文章。
- 以常見的網站架構規劃,不同類型的文章會有對應的文章分類。
- 文章標籤讓使用者在編輯文章時依喜好加入。
- 在「建立文章」的頁面建立與文章有關的內容:選擇文章分類,輸入標題、文章內容(透過WYSIWYG 編輯器,或是先前設定好的 markdown 編輯器),選擇適合的標籤...等,最後點選「儲存」儲存文章。
- 寫好的文章會在網站前台顯示。
在〈Voyager 的 BREAD〉及〈在 Voyager 新增編輯器〉兩篇文章中我已經建立好與網站內容有關的欄位與選單項目,上面流程 1 - 5 項所需的運作機制已經完成,那麼就透過 Voyager 來建立網站內容吧。
以下內容是在「已建立好資料表 BREAD」的情況下進行的,如果尚未建立 BREAD 請參考前述文章連結。
新增文章分類
-
登入 Voyager 後點選側邊欄的「分類集」。
-
點選「添加」建立新分類。
-
在編輯過程中輸入分類相關資料,然後點頁面最下方的「保存」按鈕儲存。
-
完成後就會在分類集頁面看到新增的文章分類。
新增文章標籤
-
點選側邊欄的「標籤集」。
-
點選「添加」建立新標籤。
-
在編輯過程中輸入標籤相關資料,暫時不會使用的標籤在「狀態」部分要選擇「處理中」。最後點頁面最下方的「保存」按鈕儲存。
-
完成後就會在標籤集頁面看到新增的標籤項目。
新增文章內容
-
點選側邊欄的「文章集」。
-
點選「添加」建立新文章。
-
在編輯過程中輸入/選擇文章資料,「精選」選擇「是」的文章才會在首頁顯示。最後點頁面最下方的「保存」按鈕儲存。
-
完成後就會在文章集頁面看到新增的文章項目...第一篇文章就佔了畫面好大區塊,好像不大對。
點選側邊欄「工具 - BREAD」,點選資料表項目(本例是「posts」)右方的「編輯」。
將不需要顯示項目的「瀏覽」取消勾選後移至畫面最下方點選「發佈」儲存變更,回到文章集畫面檢視結果。
與文章有關的 Controller:PostController
有了基本的網站內容,接下來開始寫程式囉!為了讓自己和一起參與的人從檔案名稱就能推測該檔案的用途,因此建立 Controller 時建議名稱取跟「post」或「article」有關的名字,以大駝峰命名方式取名為 PostController
。
php artisan make:controller PostController
使用 php artisan
建立的 Controller 檔案會位於 app/Http/Controllers
資料夾,接著開啟編輯器(VS Code)編輯檔案,新增顯示文章的「renderBlogPage
」方法。
<?php
// /app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post;
class PostController extends Controller
{
// 以 id 為依據顯示單一頁面
public function renderBlogPage($id){
$post = Post::findOrFail($id);
dd($post);
return view('blog_page', compact('post'));
}
}
我將與文章有關的Post
Model 引入,利用 Eloquent 語法以 id
為依據尋找文章資料,之後再將查詢結果傳到 blog_page
這個 Blade template 檔案顯示,MVC 架構三大元素都湊齊了。
使用 dd
函式可以觀看輸出結果是否符合預期,在輸出至 View 之前先行判斷。
在 routes/web.php 增加路由
在 web.php
增加路由項目,讓瀏覽器輸入網址時可以執行 renderBlogPage
方法,為了方便辨識我在網址中加上 blog
用以識別部落格文章。
Route::get('/blog/{id}', 'App\Http\Controllers\PostController@renderBlogPage')->name('post');
文章全文的 Blade template
在renderBlogPage
方法中宣告會傳遞 post
集合到 blog_page
這個 View 相關檔案,延續上一篇文章修改 home.blade.php
的方式,將各 Blade template 檔案資源帶入 blog_page.blade.php
。
@extends('layouts.master') //繼承 layouts/master.blade.php 主版
@section('meta_description', $post->introtext) //meta description 填入文章引言
@section('title', $post->title) //title 標籤填入標題
@section('cover_image', Voyager::image($post->cover_image))
//og:image 部分填入文章封面圖片網址,如果圖片是在 Voyager 介面上傳管理
//請使用 Voyager::image() 函式呼叫。
@section('url', url()->full()) //og:url 使用 url() 函式填入頁面完整網址
@section('content') //填入文章本文
在 Blade template 檔案使用 Controller 傳過來的變數會用 {{ }}
包起來,讓 Laravel 知道這段敘述是 Blade template 的輸出而不是 HTML 碼。使用 {{ }}
輸出的內容會自動過濾 HTML 標籤,如果要輸出包含 HTML 標籤在內的完整內容則使用 {!! !!}。
你可能注意到上面的程式碼中 @section()
裡頭的變數並沒有使用{{ }}
或 {!! !!}
,這是因為 @section()
屬於 Blade template 的功能輸出範圍而不是 HTML 碼,不需使用{{ }}
或 {!! !!}
識別。
@section('content')
到 @stop
中間的內容承接主版 @yield('content')
區塊,填入顯示在頁面左方的主要內容:儲存在資料庫的網站文章資料。
內文顯示區塊各元素與資料庫欄位的對應如下所示:
-
封面圖片:對應
cover_image
欄位。 -
文章標題:對應
title
欄位。 -
作者名字:透過
user_id
欄位關聯users
資料表中的name
欄位 -
發佈時間:對應
created_at
欄位。 - 回應計數:與回應項目有關,之後實做回應功能時再調整,暫不變動。
-
文章本文:對應
content
欄位,因以 markdown 格式儲存,要先用函式解析成 HTML 格式,例如:{!! \Illuminate\Support\Str::markdown($post->content) !!}
。
-
文章本文:對應
content
欄位,因以 markdown 格式儲存,要先用函式解析成 HTML 格式。 -
文章分類:透過
category_id
欄位關聯categories
資料表的name
欄位。 -
文章標籤:透過中介資料表
post_tag
關聯tags
資料表,取得其中的 id 與 name 欄位,再透過foreach
函式將有使用的標籤一一列出。
將原本的範例資料改成 Blade template 輸出之後的內文顯示區塊程式碼如下:
<article class="entry entry-single">
<!-- 封面圖片 -->
<div class="entry-img">
<img src="{{ Voyager::image($post->cover_image) }}" alt="{{ $post->title }}" class="img-fluid">
</div>
<!-- 文章標題 -->
<h2 class="entry-title">
<a href="{{ url()->full() }}">{{ $post->title }}</a>
</h2>
<div class="entry-meta">
<ul>
<!-- 作者名:使用 Eloquent 語法帶出 users 資料表的 name 欄位值 -->
<li class="d-flex align-items-center"><i class="bi bi-person"></i>
<a href="#profile">{{ $post->user->name }}</a>
</li>
<!-- 建立時間:輸出 createed_at 資料欄位值 -->
<li class="d-flex align-items-center"><i class="bi bi-clock"></i>
<time datetime="{{ $post->created_at->toDateString() }}">{{ $post->created_at->toDateString() }}</time>
</li>
<!-- 文章回應計數:尚未實做回應功能,先保留 -->
<li class="d-flex align-items-center"><i class="bi bi-chat-dots"></i>
<a href="blog-single.html">12 則回應</a>
</li>
</ul>
</div>
<!-- 文章本文 -->
<div class="entry-content">
{!! \Illuminate\Support\Str::markdown($post->content) !!}
</div>
<!-- 作者簡介錨點 -->
<a name="profile">
<div class="entry-footer">
<i class="bi bi-folder"></i>
<ul class="cats">
<!-- 分類名:使用 Eloquent 語法帶出 categories 資料表的 name 欄位值 -->
<li><a href="/cagegory/{{ $post->category->id }}">{{ $post->category->name }}</a></li>
</ul>
<i class="bi bi-tags"></i>
<ul class="tags">
<!-- 以 foreach 迴圈輸出使用的標籤項目,標籤名透過 Eloquent 語法帶出 tags 資料表的 name 欄位值 -->
@foreach($post->tags as $tag)
<li><a href="/tag/{{ $tag->id }}">{{ $tag->name }}</a></li>
@endforeach
</ul>
</div>
</article><!-- End blog entry -->
以 網站網址/blog/1 再次瀏覽,看看是否如同預期輸出文章內容。
新增標籤樣式
在文章中使用 HTML handing(標題)標籤區分文章大小標題,除了協助閱讀者清楚文章段落,在搜尋引擎索引上也有幫助。Moderna 主題在標題標籤上的分配是:
- H1 標籤:網站名稱。
- H2 標籤:文章標題。
- H3 標籤:文章內標題。
- H4 標籤:沒有...
我的寫作習慣會在需要時設定文章二階標題,加上顯示圖片的 img 標籤樣式需要調整,因此修改 public/css/style.css
檔案,對內文區塊增加樣式:
.blog .entry .entry-content h4 {
font-size: 16px;
margin: 20px 0px 10px 0px;
font-weight: bold;
}
.blog .entry .entry-content img {
display:block;
margin:auto;
max-width: 100%;
padding: 5px;
}
網址改以別名(slug)輸出
在與 SEO(搜尋引擎最佳化)有關的網路文章中大多會建議頁面網址使用容易閱讀的格式,幫助瀏覽者從網址就能知道該頁面內容的大概。在規劃資料表內容時我使用 slug 欄位存放文章別名。
以別名取代 id,讓網址更容易閱讀
在瀏覽部落格文章時我使用的網址是這樣的:
https://(網站網址)/blog/1
單純從網址可以看出這個頁面會是網站部落格的第 1 篇或是 id 為 1 的文章,沒看到內容前其實不知道裡頭寫什麼。那麼如果網址換成以下格式:
https://(網站網址)/blog/prologue
除了知道是部落格文章之外,還可以推測文章內容會是某個作品的序言,顯得更人性化一些不是嗎?
別名出處從哪來?
網址改以別名輸出是很好,不過別名內容從哪來?
英文文章的情況會以文章標題作為別名來源,將空白改成「-」就完成別名格式,有套件能夠將這件事自動化輸出。
那麼中文文章呢?目前看到三種作法:
- 直接將標題文字原封不動作為別名格式,這樣作法的缺點是在瀏覽器網址列以外的情況,網址的中文字部分可能會轉為 unicode 字碼,就有機會看到超級長,而且看不懂的網址...
- 將中文標題透過套件運作轉成拼音文字後作為別名格式,在對岸比較常看到。
- 自己的作法:自行輸入簡單的英文句子作為別名。這個方法算是最笨的,不過不需要套件輔助,只要在 Model 增加敘述。
在 Model 增加路由敘述
以 Post Model 為例,在類別中增加 getRouteKeyName
方法,回傳 slug
這個定義為別名的欄位。
public function getRouteKeyName()
{
return 'slug';
}
Controller 的改動
原先以 id
為搜尋依據的敘述改成以 slug
搜尋,與先前的寫法對照如下:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Post;
class PostController extends Controller
{
// 以 slug 為依據顯示單一頁面
public function renderBlogPage($slug)
{
$post = Post::where('slug', $slug)->first();
return view('blog_page', compact('post'));
}
// 以 id 為依據顯示單一頁面
// public function renderBlogPage($id){
// $post = Post::findOrFail($id);
// return view('blog_page', compact('post'));
// }
}
更改 /routes/web.php 的路由敘述
和 Controller 相同將 id
改成 slug
,前後對照如下:
Route::get('/blog/{slug}', 'App\Http\Controllers\PostController@renderBlogPage')->name('post');
// Route::get('/blog/{id}', 'App\Http\Controllers\PostController@renderBlogPage')->name('post');
將網址列的 id 改成別名後再次存取,如果瀏覽器正常顯示頁面代表更動完成,分類(Category)與標籤(Tag)的做法也是一樣的。
側邊欄小工具的自動化
文章、分類及標籤都已有資料庫紀錄的現在,可以將原有分離出去,內容還是範例資料的小工具 Blade template 檔案一一改成程式運作結果。
在 PostController 將 Category 與 Tag 兩個 Model 引入,然後再新增變數以儲存「分類列表」、「近期文章」及「標籤雲」的顯示內容。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
class PostController extends Controller
{
// 以 slug 為依據顯示單一頁面
public function renderBlogPage($slug)
{
$post = Post::where('slug', $slug)->first();
$bindings = [
//分類列表:列出狀態為發佈,精選為「是」的分類項目
'categories' => Category::where('status', 'published')->where('featured', 'yes')->get(),
//近期文章:列出狀態為發佈,精選為「是」的項目,以建立時間降冪排序後列出前五項
'recent_articles' => Post::where('status', 'published')->where('featured', 'yes')->orderBy('created_at', 'desc')->limit(5)->get(),
//標籤雲:列出狀態為發佈,精選為「是」的標籤項目
'tags' => Tag::where('status', 'published')->where('featured', 'yes')->get(),
];
return view('blog_page', $bindings, compact('post'));
}
}
分類列表:resources/views/widgets/categories.blade.php
<h3 class="sidebar-title">文章分類</h3>
<div class="sidebar-item categories">
<ul>
@foreach($categories as $category)
<li><a href="/category/{{ $category->slug }}">{{ $category->name }}
<span>({{ $category->posts->count() }})</a></li>
@endforeach
</ul>
</div><!-- End sidebar categories-->
分類名稱的旁邊是分類內文章數量統計,在哥布林老師指點之前我並不知道可以在 Blade template 內使用 Eloquent 語法,真是受教了。
近期文章:resources/views/widgets/recent_posts.blade.php
<h3 class="sidebar-title">近期文章</h3>
<div class="sidebar-item recent-posts">
@foreach($recent_articles as $recent_article)
<div class="post-item clearfix">
<img src="{{ Voyager::image($recent_article->cover_image) }}" alt="{{ $recent_article->title }}">
<h4><a href="/blog/{{ $recent_article->slug }}">{{ $recent_article->title }}</a></h4>
<time datetime="{{ $recent_article->created_at->toDateString() }}">{{ $recent_article->created_at->toDateString() }}</time>
</div>
@endforeach
</div><!-- End sidebar recent posts-->
標籤雲:resources/views/widgets/tags.blade.php
<h3 class="sidebar-title">標籤雲</h3>
<div class="sidebar-item tags">
<ul>
@foreach ($tags as $tag)
<li><a href="/tag/{{ $tag->slug }}">{{ $tag->name }}</a></li>
@endforeach
</ul>
</div><!-- End sidebar tags-->
結語
透過 Voyager 的 BREAD 讓我將網站文章、分類及標籤等網站內容寫入資料庫,接著在 Controller 引用 Model 資源,把資料庫查詢結果傳送到 View(Blade template),最終在網站前台顯示。利用 Eloquent 語法我可以取用關聯資料表間的欄位內容在想要的位置呈現,跟之前做 ffxitoolbox 時僅對單一資料表存取的情況相比增加了更多擴展性,能夠做的事情也變得更多。
不過也因為資料表間的關聯與 Eloquent 的關聯語法的都是第一次使用,老實說學習過程不怎麼順利,遇到問題上網找解答時也常常有看沒有懂...再次感謝哥布林老師的適時指導。自學程式,卡在死胡同找不到出口的情況下,有沒有人協助可能就會演變成「繼續走下去」或「到此為止」兩種截然不同的結果。
「先有文章分類再寫文章」已經是內容建立流程上的習慣,只是文章要添加的標籤往往都在撰寫文章時才會想到。以現在的情況,要不是先行建立好然後在文章編輯畫面中取用,不然就是先儲存文章,到標籤集畫面新增,最後再回到文章編輯畫面添加...
希望日後技術精進後做出「標籤即時新增」功能,讓文章建立流程更加順利。