larablog 建構日誌:文章與 Controller

larablog 建構日誌:文章與 Controller

在 Voyager 建立文章分類、標籤以及文章項目

  如何在網站上建立文章?以使用者的立場我想到的是:

  1. 有一個登入網站的頁面,在此輸入登入帳號及密碼後登入網站。
  2. 如果這個帳號具備建立文章的權限,那麼畫面中會有一個名為「建立文章」,或類似意思的連結或圖示,讓我點選後建立文章。
  3. 以常見的網站架構規劃,不同類型的文章會有對應的文章分類。
  4. 文章標籤讓使用者在編輯文章時依喜好加入。
  5. 在「建立文章」的頁面建立與文章有關的內容:選擇文章分類,輸入標題、文章內容(透過WYSIWYG 編輯器,或是先前設定好的 markdown 編輯器),選擇適合的標籤...等,最後點選「儲存」儲存文章。
  6. 寫好的文章會在網站前台顯示。

  在〈Voyager 的 BREAD〉及〈在 Voyager 新增編輯器〉兩篇文章中我已經建立好與網站內容有關的欄位與選單項目,上面流程 1 - 5 項所需的運作機制已經完成,那麼就透過 Voyager 來建立網站內容吧。

  以下內容是在「已建立好資料表 BREAD」的情況下進行的,如果尚未建立 BREAD 請參考前述文章連結。

新增文章分類

  1. 登入 Voyager 後點選側邊欄的「分類集」。 點選 Voyager 側邊欄「分類集」項目
  2. 點選「添加」建立新分類。 點選「添加」新增項目
  3. 在編輯過程中輸入分類相關資料,然後點頁面最下方的「保存」按鈕儲存。 建立文章分類資料
  4. 完成後就會在分類集頁面看到新增的文章分類。 在分類集畫面檢視已建立的文章分類

新增文章標籤

  1. 點選側邊欄的「標籤集」。 點選 Voyager 側邊欄「標籤集」項目
  2. 點選「添加」建立新標籤。 點選「添加」新增項目
  3. 在編輯過程中輸入標籤相關資料,暫時不會使用的標籤在「狀態」部分要選擇「處理中」。最後點頁面最下方的「保存」按鈕儲存。 建立標籤資料
  4. 完成後就會在標籤集頁面看到新增的標籤項目。 在標籤集畫面檢視已建立的標籤項目

新增文章內容

  1. 點選側邊欄的「文章集」。 點選 Voyager 側邊欄「文章集」項目
  2. 點選「添加」建立新文章。 點選「添加」新增項目
  3. 在編輯過程中輸入/選擇文章資料,「精選」選擇「是」的文章才會在首頁顯示。最後點頁面最下方的「保存」按鈕儲存。 建立文章資料
  4. 完成後就會在文章集頁面看到新增的文章項目...第一篇文章就佔了畫面好大區塊,好像不大對。 文章集畫面中的文章項目

  點選側邊欄「工具 - BREAD」,點選資料表項目(本例是「posts」)右方的「編輯」。 編輯 posts 資料庫的 BREAD

  將不需要顯示項目的「瀏覽」取消勾選後移至畫面最下方點選「發佈」儲存變更,回到文章集畫面檢視結果。 精簡過後的文章集畫面

與文章有關的 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'));
    }
}

  我將與文章有關的PostModel 引入,利用 Eloquent 語法以 id 為依據尋找文章資料,之後再將查詢結果傳到 blog_page這個 Blade template 檔案顯示,MVC 架構三大元素都湊齊了。

  使用 dd 函式可以觀看輸出結果是否符合預期,在輸出至 View 之前先行判斷。 dd 函式觀看輸出是否符合預期

在 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

  除了知道是部落格文章之外,還可以推測文章內容會是某個作品的序言,顯得更人性化一些不是嗎?

別名出處從哪來?

  網址改以別名輸出是很好,不過別名內容從哪來?

  英文文章的情況會以文章標題作為別名來源,將空白改成「-」就完成別名格式,有套件能夠將這件事自動化輸出。

  那麼中文文章呢?目前看到三種作法:

  1. 直接將標題文字原封不動作為別名格式,這樣作法的缺點是在瀏覽器網址列以外的情況,網址的中文字部分可能會轉為 unicode 字碼,就有機會看到超級長,而且看不懂的網址...
  2. 將中文標題透過套件運作轉成拼音文字後作為別名格式,在對岸比較常看到。
  3. 自己的作法:自行輸入簡單的英文句子作為別名。這個方法算是最笨的,不過不需要套件輔助,只要在 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 的關聯語法的都是第一次使用,老實說學習過程不怎麼順利,遇到問題上網找解答時也常常有看沒有懂...再次感謝哥布林老師的適時指導。自學程式,卡在死胡同找不到出口的情況下,有沒有人協助可能就會演變成「繼續走下去」或「到此為止」兩種截然不同的結果。

  「先有文章分類再寫文章」已經是內容建立流程上的習慣,只是文章要添加的標籤往往都在撰寫文章時才會想到。以現在的情況,要不是先行建立好然後在文章編輯畫面中取用,不然就是先儲存文章,到標籤集畫面新增,最後再回到文章編輯畫面添加...

  希望日後技術精進後做出「標籤即時新增」功能,讓文章建立流程更加順利。

作者照片

A-Bo Lee

居住在臺灣的 Joomler,期望以程式設計、開放原碼推廣活動收入養活一家老小。
35 歲後改姓李,id 作為曾為郭姓的證明。
FFXI:Abokuo@Sylph鯖、よろしくです。