lt;pgt;Sau khi hoàn thành module lt;stronggt;Categorieslt;/stronggt;, chúng ta sẽ xây dựng chức năng quan trọng nhất của Blog CMS: lt;stronggt;quản lý bài viết (Posts)lt;/stronggt;.lt;/pgt;lt;pgt;Khác với Category chỉ có vài trường dữ liệu, Post sẽ liên kết với nhiều bảng khác nhau như lt;stronggt;Categorylt;/stronggt;, lt;stronggt;Userlt;/stronggt;, đồng thời hỗ trợ lt;stronggt;upload hình ảnhlt;/stronggt;, kiểm tra dữ liệu đầu vào và hiển thị danh sách bài viết chuyên nghiệp.lt;/pgt;lt;pgt;Đây cũng là module có số lượng kiến thức nhiều nhất trong toàn bộ dự án.lt;/pgt;lt;hr /gt;lt;h1gt;Mục tiêu bài họclt;/h1gt;lt;pgt;Sau bài này bạn sẽ thực hiện được:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;CRUD bài viếtlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Upload ảnh đại diệnlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Chọn Categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Gán tác giả (User)lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Validation dữ liệult;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hiển thị Relationshiplt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tự động tạo sluglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Phân trang dữ liệult;/pgt;lt;div class=quot;separatorquot; style=quot;clear: both; text-align: center;quot;gt;lt;a href=quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-N_39djC6QL25iMVnO00eB4LwOn_hTWeu1Nlppifi8Z1Js5KhAkmfY-4gwDF9OBHEE2poQL2mNThb17EIEjmCWFz1F1FD2Og7Pk0bEdELmATyZ60WXi5CWe_P-2JVl1IuAcndIL2I0eUyG87Pwyb0nouJ6UmheKu1prL5ULhDJ2QCQdiL_6V_VjeXs4w/s1536/laravel-rung.pngquot; style=quot;margin-left: 1em; margin-right: 1em;quot;gt;lt;img border=quot;0quot; data-original-height=quot;1024quot; data-original-width=quot;1536quot; height=quot;213quot; src=quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-N_39djC6QL25iMVnO00eB4LwOn_hTWeu1Nlppifi8Z1Js5KhAkmfY-4gwDF9OBHEE2poQL2mNThb17EIEjmCWFz1F1FD2Og7Pk0bEdELmATyZ60WXi5CWe_P-2JVl1IuAcndIL2I0eUyG87Pwyb0nouJ6UmheKu1prL5ULhDJ2QCQdiL_6V_VjeXs4w/s320/laravel-rung.pngquot; width=quot;320quot; /gt;lt;/agt;lt;spangt;lt;a name=#39;more#39;gt;lt;/agt;lt;/spangt;lt;/divgt;lt;pgt;lt;/pgt;lt;/ligt;lt;/ulgt;lt;hr /gt;lt;h1gt;Kết quả sau bài họclt;/h1gt;lt;pgt;Danh sách bài viết sẽ hiển thị như sau:lt;/pgt;lt;tablegt;lt;theadgt;lt;trgt;lt;thgt;Ảnhlt;/thgt;lt;thgt;Tiêu đềlt;/thgt;lt;thgt;Categorylt;/thgt;lt;thgt;Tác giảlt;/thgt;lt;thgt;Ngày đănglt;/thgt;lt;thgt;Thao táclt;/thgt;lt;/trgt;lt;/theadgt;lt;tbodygt;lt;trgt;lt;tdgt;✔lt;/tdgt;lt;tdgt;Laravel 12 mới có gì?lt;/tdgt;lt;tdgt;Laravellt;/tdgt;lt;tdgt;Adminlt;/tdgt;lt;tdgt;07/08/2026lt;/tdgt;lt;tdgt;Edit / Deletelt;/tdgt;lt;/trgt;lt;/tbodygt;lt;/tablegt;lt;hr /gt;lt;h1gt;1. Cấu trúc Databaselt;/h1gt;lt;pgt;Bảng lt;stronggt;postslt;/stronggt;lt;/pgt;lt;tablegt;lt;theadgt;lt;trgt;lt;thgt;Fieldlt;/thgt;lt;thgt;Kiểult;/thgt;lt;/trgt;lt;/theadgt;lt;tbodygt;lt;trgt;lt;tdgt;idlt;/tdgt;lt;tdgt;bigintlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;user_idlt;/tdgt;lt;tdgt;foreignIdlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;category_idlt;/tdgt;lt;tdgt;foreignIdlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;titlelt;/tdgt;lt;tdgt;stringlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;sluglt;/tdgt;lt;tdgt;stringlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;imagelt;/tdgt;lt;tdgt;stringlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;excerptlt;/tdgt;lt;tdgt;textlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;contentlt;/tdgt;lt;tdgt;longTextlt;/tdgt;lt;/trgt;lt;trgt;lt;tdgt;created_atlt;/tdgt;lt;tdgt;timestamplt;/tdgt;lt;/trgt;lt;/tbodygt;lt;/tablegt;lt;pgt;Quan hệlt;/pgt;lt;pregt;lt;codegt;User
│
├──────lt; Posts gt;────── Category
lt;/codegt;lt;/pregt;
lt;pgt;Một User có nhiều Post.lt;/pgt;lt;pgt;Một Category có nhiều Post.lt;/pgt;lt;pgt;Một Post chỉ thuộc một User và một Category.lt;/pgt;lt;pgt;Chú ý:lt;/pgt;
lt;h3gt;
lt;spangt;1. Sửa migration lt;codegt;create_posts_tablelt;/codegt;lt;/spangt;
lt;/h3gt;
lt;pgt;Đưa luôn cấu trúc hoàn chỉnh vào migration tạo lt;codegt;postslt;/codegt;:lt;/pgt;
lt;pregt;lt;codegt;Schema::create(#39;posts#39;, function (Blueprint $table) {
$table-gt;id();
$table-gt;foreignId(#39;user_id#39;)
-gt;constrained()
-gt;cascadeOnDelete();
$table-gt;foreignId(#39;category_id#39;)
-gt;nullable()
-gt;constrained()
-gt;nullOnDelete();
$table-gt;string(#39;title#39;);
$table-gt;string(#39;slug#39;)
-gt;unique();
$table-gt;string(#39;image#39;)
-gt;nullable();
$table-gt;text(#39;excerpt#39;)
-gt;nullable();
$table-gt;longText(#39;content#39;);
$table-gt;enum(#39;status#39;, [
#39;draft#39;,
#39;published#39;,
#39;hidden#39;,
#39;scheduled#39;
])-gt;default(#39;draft#39;);
$table-gt;timestamp(#39;published_at#39;)-gt;nullable();
$table-gt;timestamps();
});lt;/codegt;lt;/pregt;
lt;pgt;Như vậy bạn lt;stronggt;không cần 3 migration riênglt;/stronggt; để:lt;/pgt;
lt;ulgt;
lt;ligt;
thêm lt;codegt;category_idlt;/codegt;
lt;/ligt;
lt;ligt;
thêm lt;codegt;imagelt;/codegt;
lt;/ligt;
lt;ligt;
thêm lt;codegt;sluglt;/codegt;
lt;/ligt;
lt;ligt;
thêm lt;codegt;excerptlt;/codegt;
lt;/ligt;
lt;/ulgt;
lt;pgt;nữa, nếu đây vẫn là database đang phát triển và chưa cần bảo toàn dữ liệu production.lt;/pgt;
lt;hr /gt;
lt;h3gt;2. Xóa các migration bổ sung cũlt;/h3gt;
lt;pgt;Nếu bạn đã có các migration kiểu:lt;/pgt;
lt;pregt;lt;codegt;add_category_id_to_posts_table
add_image_to_posts_table
add_slug_and_excerpt_to_posts_tablelt;/codegt;lt;/pregt;
lt;pgt;thì có thể xóa chúng lt;stronggt;sau khi đã đưa cấu trúc cuối cùng vào migration lt;codegt;create_posts_tablelt;/codegt;lt;/stronggt;.lt;/pgt;
lt;pgt;Mục tiêu là migration tạo bảng lt;codegt;postslt;/codegt; ngay từ đầu đã hoàn chỉnh.lt;/pgt;
lt;h3gt;3. Sửa Post modellt;/h3gt;
lt;codegt;app/Models/Post.phplt;/codegt;
lt;pregt;lt;codegt;lt;?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = [
#39;user_id#39;,
#39;category_id#39;,
#39;title#39;,
#39;slug#39;,
#39;image#39;,
#39;excerpt#39;,
#39;content#39;,
#39;status#39;,
#39;published_at#39;,
];
protected $casts = [
#39;published_at#39; =gt; #39;datetime#39;,
];
}lt;/codegt;lt;/pregt;
lt;h3gt;4. Seeder sửa Factorylt;/h3gt;
lt;pregt;lt;codegt;lt;?php
namespace Database\Factories;
use App\Models\Model;
use App\Models\User;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factorylt;Modelgt;
*/
class PostFactory extends Factory
{
/**
* Define the model#39;s default state.
*
* @return arraylt;string, mixedgt;
*/
public function definition(): array
{
$status = fake()-gt;randomElement([
#39;draft#39;,
#39;published#39;,
#39;hidden#39;,
#39;scheduled#39;,
]);
return [
// Định nghĩa dữ liệu mẫu
#39;title#39; =gt; fake()-gt;sentence(),
#39;slug#39; =gt; fake()-gt;unique()-gt;slug(),
#39;image#39; =gt; fake()-gt;imageUrl(1200, 800),
#39;excerpt#39; =gt; fake()-gt;paragraph(),
#39;content#39; =gt; fake()-gt;paragraphs(5, true),
#39;user_id#39; =gt; fn () =gt;
User::query()-gt;inRandomOrder()-gt;value(#39;id#39;),
#39;category_id#39; =gt; fn () =gt;
Category::query()-gt;inRandomOrder()-gt;value(#39;id#39;),
#39;status#39; =gt; $status,
#39;published_at#39; =gt; match ($status) {
#39;published#39; =gt;
fake()-gt;dateTimeBetween(#39;-1 year#39;, #39;now#39;),
#39;scheduled#39; =gt;
fake()-gt;dateTimeBetween(#39;now#39;, #39;+1 month#39;),
default =gt; null,
},
];
}
}
lt;/codegt;lt;/pregt;
lt;divgt;
lt;pgt;lt;ugt;Chú ýlt;/ugt;: phải tạo bảng chứa khóa ngoại trước nếu không sẽ báo lỗi.lt;/pgt;
lt;pgt;thứ tự migration nên là:lt;/pgt;
lt;pregt;lt;codegt;1. create_countries_table
2. create_users_table
3. create_categories_table
4. create_posts_tablelt;/codegt;lt;/pregt;
lt;pgt;Sau đó chạy:lt;/pgt;
lt;pregt;lt;codegt;php artisan migrate:fresh --seedlt;/codegt;lt;/pregt;
lt;hr /gt;
lt;h1gt;2. Relationshiplt;/h1gt;
lt;h2gt;User.phplt;/h2gt;
lt;/divgt;
lt;pregt;lt;code class=quot;language-phpquot;gt;public function posts()
{
return $this-gt;hasMany(Post::class);
}
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h2gt;Category.phplt;/h2gt;
lt;pregt;lt;code class=quot;language-phpquot;gt;public function posts()
{
return $this-gt;hasMany(Post::class);
}
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h2gt;Post.phplt;/h2gt;
lt;pregt;lt;code class=quot;language-phpquot;gt;public function category()
{
return $this-gt;belongsTo(Category::class);
}
public function user()
{
return $this-gt;belongsTo(User::class);
}
lt;/codegt;lt;/pregt;lt;pgt;Đây là ba relationship được sử dụng xuyên suốt dự án.lt;/pgt;lt;hr /gt;lt;h1gt;3. Resource Controllerlt;/h1gt;lt;pgt;Tạo controllerlt;/pgt;
lt;pregt;lt;code class=quot;language-bashquot;gt;php artisan make:controller PostController --resource
lt;/codegt;lt;/pregt;lt;pgt;Đăng ký routelt;/pgt;
lt;pregt;lt;code class=quot;language-phpquot;gt;Route::resource(#39;posts#39;, PostController::class);
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ tạo sẵn:lt;/pgt;lt;pregt;lt;codegt;index
create
store
show
edit
update
destroy
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;4. Hiển thị danh sách bài viếtlt;/h1gt;lt;pgt;Controllerlt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;public function index()
{
//dd(Auth::user());
//lấy sẵn user để sau này chặn ngay từ view chỉ có admin được xem all posts
$user = Auth::user();
$posts = Post::with([#39;category#39;,#39;user#39;])
-gt;latest()
//-gt;paginate(10); chỗ này dùng dataTables quản lý rồi nên k cần
-gt;get();
return view(#39;posts.index#39;, compact(#39;posts#39;));
}
lt;/codegt;lt;/pregt;lt;pgt;Điểm quan trọnglt;/pgt;
lt;pregt;lt;codegt;with()
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ eager loading.lt;/pgt;lt;pgt;Không xảy ra lỗi lt;stronggt;N+1 Querylt;/stronggt;.lt;/pgt;lt;pgt;lt;ugt;Chú ýlt;/ugt;: nhớ kiểm tra route list trước coi đủ 7 món hay không.lt;/pgt;
lt;pregt;lt;codegt;php artisan optimize:clearlt;/codegt;lt;/pregt;
lt;pgt;Kiểm tra:lt;/pgt;
lt;pregt;lt;codegt;php artisan route:list --name=postslt;/codegt;lt;/pregt;
lt;hr /gt;
lt;divgt;lt;span role=quot;textquot;gt;trong lt;codegt;x-app-layoutlt;/codegt; đã có sẵn thư viên datatables để xử lý gọn phần bảnglt;/spangt;lt;/divgt;
lt;divgt;lt;span role=quot;textquot;gt;do file lt;codegt;views/layouts/app.blade.phplt;/codegt; đã nạp đủ thư việnlt;/spangt;
lt;pregt;lt;codegt;lt;!DOCTYPE htmlgt;
lt;html lang=quot;{{ str_replace(#39;_#39;, #39;-#39;, app()-gt;getLocale()) }}quot;gt;
lt;headgt;
lt;meta charset=quot;utf-8quot;gt;
lt;meta name=quot;viewportquot; content=quot;width=device-width, initial-scale=1quot;gt;
lt;meta name=quot;csrf-tokenquot; content=quot;{{ csrf_token() }}quot;gt;
lt;titlegt;{{ config(#39;app.name#39;, #39;Laravel#39;) }}lt;/titlegt;
lt;!-- Fonts --gt;
lt;link rel=quot;preconnectquot; href=quot;https://fonts.bunny.netquot;gt;
lt;link href=quot;https://fonts.bunny.net/css?family=figtree:400,500,600display=swapquot; rel=quot;stylesheetquot;gt;
lt;link rel=quot;stylesheetquot;
href=quot;https://cdn.datatables.net/2.3.4/css/dataTables.dataTables.min.cssquot;gt;
lt;!-- Scripts --gt;
@vite([#39;resources/css/app.css#39;, #39;resources/js/app.js#39;])
lt;/headgt;
lt;body class=quot;font-sans antialiasedquot;gt;
lt;div class=quot;min-h-screen bg-gray-100quot;gt;
@include(#39;layouts.navigation#39;)
lt;!-- Page Heading --gt;
@isset($header)
lt;header class=quot;bg-white shadowquot;gt;
lt;div class=quot;max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8quot;gt;
{{ $header }}
lt;/divgt;
lt;/headergt;
@endisset
lt;!-- Page Content --gt;
lt;maingt;
{{ $slot }}
lt;/maingt;
lt;/divgt;
lt;/bodygt;
lt;/htmlgt;
lt;/codegt;lt;/pregt;
lt;pgt;
File lt;codegt;resources/js/app.jslt;/codegt; đã import đủ và xử lý JavaScript.
lt;/pgt;
lt;pregt;lt;codegt;import $ from #39;jquery#39;;
window.$ = $;
window.jQuery = $;
import DataTable from #39;datatables.net#39;;
import Alpine from #39;alpinejs#39;;
window.Alpine = Alpine;
Alpine.start();
//cáu hình cho table posts index blade
const table = document.querySelector(#39;#postsTable#39;);
if (table) {
new DataTable(table, {
language: {
search: quot;🔍 Tìm kiếm:quot;,
lengthMenu: quot;Hiển thị _MENU_ dòngquot;,
info: quot;Hiển thị _START_ đến _END_ trong tổng số _TOTAL_ bài viếtquot;,
paginate: {
first: quot;Đầuquot;,
last: quot;Cuốiquot;,
next: quot;Sauquot;,
previous: quot;Trướcquot;
},
zeroRecords: quot;Không tìm thấy dữ liệuquot;,
emptyTable: quot;Chưa có dữ liệuquot;
}
});
}lt;/codegt;lt;/pregt;
lt;h3gt;File bladelt;codegt;posts.indexlt;/codegt;lt;/h3gt;
lt;pregt;lt;codegt;lt;x-app-layoutgt;
lt;div class=quot;py-6quot;gt;
lt;div class=quot;mx-auto max-w-7xl px-4 sm:px-6 lg:px-8quot;gt;
lt;!-- Header --gt;
lt;div class=quot;mb-6 flex items-center justify-betweenquot;gt;
lt;divgt;
lt;h2 class=quot;text-2xl font-bold text-gray-800quot;gt;
Quản lý bài viết
lt;/h2gt;
lt;p class=quot;mt-1 text-sm text-gray-500quot;gt;
Danh sách tất cả bài viết trong hệ thống
lt;/pgt;
lt;/divgt;
lt;a href=quot;{{ route(#39;posts.create#39;) }}quot;
class=quot;inline-flex items-center rounded-lg bg-indigo-600 px-4 py-2.5
text-sm font-semibold text-white shadow-sm
hover:bg-indigo-700quot;gt;
+ Thêm bài viết
lt;/agt;
lt;/divgt;
lt;!-- Card --gt;
lt;div class=quot;overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-200quot;gt;
lt;!-- Card Header --gt;
lt;div class=quot;border-b border-gray-200 px-6 py-4quot;gt;
lt;div class=quot;flex items-center justify-betweenquot;gt;
lt;divgt;
lt;h3 class=quot;text-lg font-semibold text-gray-800quot;gt;
Danh sách bài viết
lt;/h3gt;
lt;p class=quot;text-sm text-gray-500quot;gt;
Tổng cộng {{ $posts-gt;total() }} bài viết
lt;/pgt;
lt;/divgt;
lt;/divgt;
lt;/divgt;
lt;spangt; lt;/spangt;lt;spangt; lt;/spangt;lt;spangt; lt;/spangt;lt;spangt; lt;/spangt;{{-- Alert --}}
lt;x-alert /gt;
lt;spangt; lt;/spangt;lt;spangt; lt;/spangt;lt;spangt; lt;/spangt;lt;spangt; lt;/spangt;
lt;!-- Table --gt;
lt;div class=quot;overflow-x-autoquot;gt;
lt;table class=quot;min-w-full divide-y divide-gray-200quot;gt;
lt;thead class=quot;bg-gray-50quot;gt;
lt;trgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Tiêu đề
lt;/thgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Ngày
lt;/thgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Tác giả
lt;/thgt;
lt;th class=quot;px-6 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Danh mục
lt;/thgt;
lt;th class=quot;px-6 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500quot;gt;
Thao tác
lt;/thgt;
lt;/trgt;
lt;/theadgt;
lt;tbody class=quot;divide-y divide-gray-200 bg-whitequot;gt;
@forelse($posts as $post)
lt;tr class=quot;hover:bg-gray-50quot;gt;
{{-- TITLE --}}
lt;td class=quot;px-6 py-4quot;gt;
lt;a href=quot;{{ route(#39;posts.show#39;, $post) }}quot;
class=quot;font-semibold text-gray-800 hover:text-indigo-600quot;gt;
{{ $post-gt;title }}
lt;/agt;
lt;/tdgt;
{{-- DATE --}}
lt;td class=quot;whitespace-nowrap px-6 py-4 text-sm text-gray-500quot;gt;
{{ $post-gt;created_at-gt;format(#39;d/m/Y#39;) }}
lt;/tdgt;
{{-- AUTHOR --}}
lt;td class=quot;whitespace-nowrap px-6 py-4quot;gt;
@if($post-gt;user)
lt;span class=quot;text-sm font-medium text-gray-700quot;gt;
{{ $post-gt;user-gt;name }}
lt;/spangt;
@else
lt;span class=quot;text-sm text-gray-400quot;gt;
—
lt;/spangt;
@endif
lt;/tdgt;
{{-- CATEGORY --}}
lt;td class=quot;whitespace-nowrap px-6 py-4quot;gt;
@if($post-gt;category)
lt;span class=quot;inline-flex rounded-full
bg-blue-100 px-3 py-1
text-xs font-medium text-blue-700quot;gt;
{{ $post-gt;category-gt;name }}
lt;/spangt;
@else
lt;span class=quot;text-sm text-gray-400quot;gt;
—
lt;/spangt;
@endif
lt;/tdgt;
{{-- ACTIONS --}}
lt;td class=quot;whitespace-nowrap px-6 py-4quot;gt;
lt;div class=quot;flex justify-end gap-2quot;gt;
{{-- VIEW --}}
lt;a href=quot;{{ route(#39;posts.show#39;, $post) }}quot;
class=quot;rounded-lg bg-gray-100 px-3 py-2
text-sm font-medium text-gray-700
hover:bg-gray-200quot;gt;
Xem
lt;/agt;
{{-- EDIT --}}
lt;a href=quot;{{ route(#39;posts.edit#39;, $post) }}quot;
class=quot;rounded-lg bg-blue-100 px-3 py-2
text-sm font-medium text-blue-700
hover:bg-blue-200quot;gt;
Sửa
lt;/agt;
{{-- DELETE --}}
lt;form action=quot;{{ route(#39;posts.destroy#39;, $post) }}quot;
method=quot;POSTquot;
onsubmit=quot;return confirm(#39;Bạn có chắc muốn xóa bài viết này?#39;);quot;gt;
@csrf
@method(#39;DELETE#39;)
lt;button type=quot;submitquot;
class=quot;rounded-lg bg-red-100 px-3 py-2
text-sm font-medium text-red-700
hover:bg-red-200quot;gt;
Xóa
lt;/buttongt;
lt;/formgt;
lt;/divgt;
lt;/tdgt;
lt;/trgt;
@empty
lt;trgt;
lt;td colspan=quot;5quot;
class=quot;px-6 py-12 text-center text-sm text-gray-400quot;gt;
Chưa có bài viết nào.
lt;/tdgt;
lt;/trgt;
@endforelse
lt;/tbodygt;
lt;/tablegt;
lt;/divgt;
lt;!-- Pagination --gt;
@if($posts-gt;hasPages())
lt;div class=quot;border-t border-gray-200 bg-white px-6 py-4quot;gt;
{{ $posts-gt;links() }}
lt;/divgt;
@endif
lt;/divgt;
lt;/divgt;
lt;/divgt;
lt;/x-app-layoutgt;lt;/codegt;lt;/pregt;lt;/divgt;
lt;hr /gt;lt;h1gt;5. Hiển thị Relationshiplt;/h1gt;lt;pgt;Trong Bladelt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;{{ $post-gt;category-gt;name }}
lt;/codegt;lt;/pregt;lt;pregt;lt;code class=quot;language-bladequot;gt;{{ $post-gt;user-gt;name }}
lt;/codegt;lt;/pregt;lt;pgt;Không cần viết Query JOIN.lt;/pgt;lt;pgt;Đó chính là sức mạnh của Eloquent ORM.lt;/pgt;lt;hr /gt;lt;h1gt;6. Form tạo bài viếtlt;/h1gt;lt;pgt;Form sẽ có:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Tiêu đềlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Sluglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Categorylt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Hình ảnhlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Mô tả ngắnlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nội dunglt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Categorylt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;lt;select name=quot;category_idquot;gt;
@foreach($categories as $category)
lt;option value=quot;{{ $category-gt;id }}quot;gt;
{{ $category-gt;name }}
lt;/optiongt;
@endforeach
lt;/selectgt;
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ lấy toàn bộ Category để người dùng lựa chọn.lt;/pgt;lt;pgt;Bước 1: sửa hàm store trong PostControllerlt;/pgt;lt;div class=quot;group relative clear-both my-4 w-full overflow-visible z-1quot; data-writing-block-fullscreen-fallback-target=quot;inlinequot;gt;lt;div class=quot;group/writing-block-surface relative isolate w-full min-w-0 overflow-clip rounded-[var(--oai-wb-radius)] [box-shadow:var(--oai-wb-shadow)] [color:var(--oai-wb-text-primary)] [contain:inline-size] [font-family:var(--oai-wb-font-sans)] [--wb-text-primary:var(--text-primary)] [--wb-text-secondary:var(--text-secondary)] [--wb-text-tertiary:var(--text-tertiary)] [--wb-surface-primary:var(--bg-primary)] [--wb-surface-secondary:var(--bg-tertiary)] [--wb-border:rgb(0_0_0/7%)] [--wb-border-hover:rgb(0_0_0/7%)] [--wb-divider:var(--border-light)] [--wb-focus:var(--border-strong)] [--wb-control-border:var(--interactive-border-focus)] [--wb-entity-accent:var(--theme-entity-accent)] [--wb-accent:var(--interactive-label-accent-default)] [--wb-on-accent:var(--bg-primary)] [--wb-interactive-background:var(--interactive-bg-accent-default)] [--wb-selection:var(--theme-user-selection-bg,var(--selection))] [--wb-radius-sm:var(--radius-sm)] [--wb-shadow-lg:var(--shadow-lg)] [--wb-font-sans:var(--font-sans)] [--wb-font-mono:var(--font-mono)] [--wb-shimmer-dark-contrast:var(--gray-550)] [--wb-shimmer-dark-secondary:var(--text-tertiary)] [--wb-radius:24px] [--wb-shadow:0_4px_80px_rgb(0_0_0/2%)] [contain:none]quot; data-oai-writing-block-surface=quot;quot; data-testid=quot;writing-block-containerquot; data-writing-block=quot;truequot; id=quot;writing-block-6ede7a61-9bb4-469f-b7ce-1081dbac2c72quot;gt;lt;div aria-hidden=quot;truequot; class=quot;pointer-events-none absolute inset-0 rounded-[inherit] z-[-1] [background:var(--oai-wb-surface-primary)] bg-token-bg-primary! dark:bg-[#2a2a2a]!quot; data-writing-block-fullscreen-shell-surface=quot;truequot;gt;lt;/divgt;lt;div class=quot;relative z-[1]quot;gt;lt;div class=quot;z-1 relative md:sticky md:top-(--sticky-padding-top)quot; data-testid=quot;writing-block-header-sticky-containerquot;gt;lt;div class=quot;relative isolate flex w-full items-center justify-between gap-3 font-sans py-2.5 pe-3quot; data-testid=quot;writing-block-header-surfacequot;gt;lt;div aria-hidden=quot;truequot; class=quot;bg-token-bg-primary pointer-events-none absolute inset-0 -z-1 dark:bg-[#2a2a2a]quot; data-writing-block-fullscreen-header-surface=quot;truequot;gt;lt;/divgt;lt;div class=quot;relative z-1 flex w-full items-center justify-between gap-3quot; data-writing-block-fullscreen-header-chrome=quot;truequot; data-writing-block-fullscreen-header-layout=quot;inlinequot;gt;lt;div class=quot;flex min-w-0 grow gap-3 items-centerquot;gt;lt;div class=quot;flex min-w-0 grow items-center gap-1 self-start px-2quot; data-testid=quot;writing-block-header-magic-edit-layoutquot;gt;lt;div class=quot;z-2 relative flex min-w-0 items-center self-start shrink-0quot; data-header-magic-edit-entrypoint-version=quot;v1quot; data-testid=quot;writing-block-header-magic-edit-entrypointquot;gt;lt;div class=quot;pointer-events-none absolute inset-y-0 z-3 flex items-centerquot; data-testid=quot;writing-block-header-magic-edit-leading-icon-slotquot;gt;lt;span class=quot;flex size-9 shrink-0 items-center justify-center rounded-[43px]quot;gt;lt;svg aria-hidden=quot;truequot; class=quot;icon size-5 shrink-0 grow-0 [color:var(--oai-wb-text-primary)]quot; height=quot;20quot; width=quot;20quot; xmlns=quot;http://www.w3.org/2000/svgquot;gt;lt;use fill=quot;currentColorquot; href=quot;/cdn/assets/sprites-core-78807d7e.svg#6d87e1quot;gt;lt;/usegt;lt;/svggt;lt;/spangt;lt;/divgt;lt;div aria-hidden=quot;falsequot; class=quot;relative z-2 flex shrink-0 items-center gap-0.5 overflow-visible transition-opacity duration-200 ease-out opacity-100quot; data-testid=quot;writing-block-header-magic-edit-controlsquot;gt;lt;/divgt;lt;div class=quot;absolute top-1/2 z-1 flex h-9 min-w-0 shrink-0 -translate-y-1/2 items-center overflow-hidden rounded-full border [border-color:var(--oai-wb-divider)] [background-color:var(--oai-wb-surface-primary)] transition-[width,background-color,border-color,box-shadow,opacity] duration-200 ease-out dark:bg-[#2a2a2a] pointer-events-none shadow-nonequot; data-state=quot;collapsedquot; data-testid=quot;writing-block-header-magic-edit-composerquot;gt;lt;/divgt;lt;/divgt;lt;div aria-hidden=quot;falsequot; class=quot;min-w-0 flex-1quot; data-testid=quot;writing-block-header-magic-edit-trailing-contentquot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;flex shrink-0 items-centerquot;gt;lt;div class=quot;flex items-center gap-1quot;gt;lt;span data-state=quot;closedquot;gt;lt;/spangt;lt;span data-state=quot;closedquot;gt;lt;/spangt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div class=quot;mt4SwW_editor [--wb-text-primary:var(--text-primary)] [--wb-text-secondary:var(--text-secondary)] [--wb-text-tertiary:var(--text-tertiary)] [--wb-surface-primary:var(--bg-primary)] [--wb-surface-secondary:var(--bg-tertiary)] [--wb-border:rgb(0_0_0/7%)] [--wb-border-hover:rgb(0_0_0/7%)] [--wb-divider:var(--border-light)] [--wb-focus:var(--border-strong)] [--wb-control-border:var(--interactive-border-focus)] [--wb-entity-accent:var(--theme-entity-accent)] [--wb-accent:var(--interactive-label-accent-default)] [--wb-on-accent:var(--bg-primary)] [--wb-interactive-background:var(--interactive-bg-accent-default)] [--wb-selection:var(--theme-user-selection-bg,var(--selection))] [--wb-radius-sm:var(--radius-sm)] [--wb-shadow-lg:var(--shadow-lg)] [--wb-font-sans:var(--font-sans)] [--wb-font-mono:var(--font-mono)] [--wb-shimmer-dark-contrast:var(--gray-550)] [--wb-shimmer-dark-secondary:var(--text-tertiary)] [--wb-radius:24px] [--wb-shadow:0_4px_80px_rgb(0_0_0/2%)] [contain:none] writing-block-editor markdown-new-styling relative flow-rootquot;gt;lt;div aria-disabled=quot;falsequot; class=quot;outline-none data-[writing-block-fullscreen-editor]:min-h-[75vh] ProseMirror markdown prose dark:prose-invert w-full min-h-6 break-words focus:outline-nonequot; data-writing-block-fullscreen-editor-layout=quot;inlinequot; data-writing-block-fullscreen-editor-region=quot;truequot; dir=quot;autoquot; translate=quot;noquot;gt;lt;pre dir=quot;ltrquot;gt;lt;code dir=quot;ltrquot;gt;public function store(Request $request)
{
$request-gt;validate(
[
#39;title#39; =gt; #39;required|max:255#39;,
#39;slug#39; =gt; [
#39;required#39;,
#39;max:255#39;,
#39;unique:posts,slug#39;,
],
#39;excerpt#39; =gt; #39;nullable#39;,
#39;content#39; =gt; #39;required|string#39;,
#39;category_id#39; =gt; [
#39;nullable#39;,
#39;exists:categories,id#39;,
],
#39;tags#39; =gt; #39;nullable|array#39;,
#39;tags.*#39; =gt; #39;exists:tags,id#39;,
#39;image#39; =gt; [
#39;nullable#39;,
#39;image#39;,
#39;mimes:jpg,jpeg,png,webp#39;,
#39;max:2048#39;,
],
#39;status#39; =gt; [
#39;required#39;,
#39;in:draft,published,hidden,scheduled#39;,
],
#39;published_at#39; =gt; #39;nullable|date#39;,
],
[
#39;title.required#39; =gt; #39;Vui lòng nhập tiêu đề.#39;,
#39;title.max#39; =gt; #39;Tiêu đề tối đa 255 ký tự.#39;,
#39;slug.required#39; =gt; #39;Vui lòng nhập slug.#39;,
#39;slug.unique#39; =gt; #39;Slug này đã tồn tại.#39;,
#39;slug.max#39; =gt; #39;Slug tối đa 255 ký tự.#39;,
#39;content.required#39; =gt; #39;Vui lòng nhập nội dung.#39;,
#39;content.min#39; =gt; #39;Nội dung tối thiểu 20 ký tự.#39;,
#39;category_id.exists#39; =gt; #39;Danh mục không tồn tại.#39;,
#39;tags.array#39; =gt; #39;Tags không hợp lệ.#39;,
#39;tags.*.exists#39; =gt; #39;Tag không tồn tại.#39;,
#39;image.image#39; =gt; #39;File tải lên phải là hình ảnh.#39;,
#39;image.mimes#39; =gt; #39;Ảnh phải có định dạng jpg, jpeg, png hoặc webp.#39;,
#39;image.max#39; =gt; #39;Ảnh tối đa 2MB.#39;,
#39;status.required#39; =gt; #39;Vui lòng chọn trạng thái.#39;,
#39;status.in#39; =gt; #39;Trạng thái không hợp lệ.#39;,
#39;published_at.date#39; =gt; #39;Ngày xuất bản không hợp lệ.#39;,
]
);
/*
|--------------------------------------------------------------------------
| Lưu ảnh
|--------------------------------------------------------------------------
*/
$image = null;
if ($request-gt;hasFile(#39;image#39;)) {
$image = $request
-gt;file(#39;image#39;)
-gt;store(
#39;posts#39;,
#39;public#39;
);
}
/*
|--------------------------------------------------------------------------
| Published At
|--------------------------------------------------------------------------
*/
$publishedAt = $request-gt;published_at;
// Nếu chọn Published nhưng không nhập ngày
// thì lấy thời điểm hiện tại
if (
$request-gt;status === #39;published#39;
empty($publishedAt)
) {
$publishedAt = now();
}
/*
|--------------------------------------------------------------------------
| SANITIZE CONTENT
|--------------------------------------------------------------------------
*/
$content = sanitizeHtml($request-gt;content);
/*
|--------------------------------------------------------------------------
| KIỂM TRA NỘI DUNG THỰC
|--------------------------------------------------------------------------
*/
//dd($content);
if (mb_strlen(trim(strip_tags($content))) lt; 20) {
return back()
-gt;withInput()
-gt;withErrors([
#39;content#39; =gt; #39;Nội dung phải có ít nhất 20 ký tự.#39;,
]);
}
/*
|--------------------------------------------------------------------------
| Tạo Post
|--------------------------------------------------------------------------
*/
$post = Post::create([
#39;user_id#39; =gt; auth()-gt;id(),
#39;category_id#39; =gt; $request-gt;category_id,
#39;title#39; =gt; $request-gt;title,
#39;slug#39; =gt; $request-gt;slug,
#39;image#39; =gt; $image,
#39;excerpt#39; =gt; $request-gt;excerpt,
// Lưu HTML đã sanitize
#39;content#39; =gt; $content,
#39;status#39; =gt; $request-gt;status,
#39;published_at#39; =gt; $publishedAt,
]);
/*
|--------------------------------------------------------------------------
| Lưu Tags
|--------------------------------------------------------------------------
*/
$post-gt;tags()-gt;sync(
$request-gt;tags ?? []
);
/*
|--------------------------------------------------------------------------
| Redirect
|--------------------------------------------------------------------------
*/
return redirect()
-gt;route(#39;posts.show#39;, $post)
-gt;with(
#39;success#39;,
#39;Bài viết đã được tạo thành công.#39;
);
}
//END store functionlt;br /gt;lt;/codegt;lt;/pregt;lt;/divgt;lt;/divgt;lt;/divgt;lt;div aria-hidden=quot;truequot; class=quot;pointer-events-none absolute inset-0 rounded-[inherit] z-[2] [box-shadow:inset_0_0_0_1px_var(--oai-wb-border)] group-hover/writing-block-surface:[box-shadow:inset_0_0_0_1px_var(--oai-wb-border-hover)]quot; data-writing-block-fullscreen-shell-border=quot;truequot;gt;lt;/divgt;lt;/divgt;lt;/divgt;lt;p data-end=quot;4290quot; data-start=quot;4249quot;gt;lt;bgt;Create Blade Formlt;/bgt;:lt;/pgt;
lt;pregt;lt;codegt;lt;x-app-layoutgt;
lt;div class=quot;max-w-3xl mx-auto py-8quot;gt;
lt;x-ui.card
title=quot;Thêm Bài Viếtquot;
description=quot;Nhập thông tin bài viếtquot;gt;
lt;form
action=quot;{{ route(#39;posts.store#39;) }}quot;
method=quot;POSTquot;
enctype=quot;multipart/form-dataquot;gt;
@csrf
{{-- TITLE --}}
lt;x-ui.input
label=quot;Tiêu đềquot;
name=quot;titlequot;
/gt;
{{-- SLUG --}}
lt;x-ui.input-slug
label=quot;Slugquot;
name=quot;slugquot;
source=quot;titlequot;
/gt;
{{-- EXCERPT --}}
lt;x-ui.textarea
label=quot;Mô tả ngắnquot;
name=quot;excerptquot;
rows=quot;3quot;
/gt;
{{-- CONTENT --}}
lt;x-ui.editor
label=quot;Nội dungquot;
name=quot;contentquot;
height=quot;500pxquot;
/gt;
{{-- CATEGORY --}}
lt;x-ui.select
label=quot;Danh mụcquot;
name=quot;category_idquot;
:options=quot;$categoriesquot;
/gt;
{{-- TAGS --}}
lt;x-ui.select
label=quot;Tagsquot;
name=quot;tagsquot;
:options=quot;$tagsquot;
multiple
/gt;
{{-- IMAGE --}}
lt;x-ui.file-input
label=quot;Ảnh đại diệnquot;
name=quot;imagequot;
/gt;
{{-- STATUS --}}
lt;x-ui.select
label=quot;Trạng tháiquot;
name=quot;statusquot;
:options=quot;[
#39;draft#39; =gt; #39;Nháp#39;,
#39;published#39; =gt; #39;Xuất bản#39;,
#39;hidden#39; =gt; #39;Ẩn#39;,
#39;scheduled#39; =gt; #39;Lên lịch#39;,
]quot;
/gt;
{{-- PUBLISHED AT --}}
lt;x-ui.input
label=quot;Ngày xuất bảnquot;
name=quot;published_atquot;
type=quot;datetime-localquot;
/gt;
lt;div class=quot;mt-6quot;gt;
lt;x-ui.buttongt;
Lưu bài viết
lt;/x-ui.buttongt;
lt;/divgt;
lt;/formgt;
lt;/x-ui.cardgt;
lt;!--card chứa form--gt;
lt;/divgt;
lt;/x-app-layoutgt;
lt;/codegt;lt;/pregt;
lt;pgt;abclt;/pgt;lt;hr /gt;lt;h1gt;7. Validation(---)lt;/h1gt;lt;pregt;lt;code class=quot;language-phpquot;gt;$request-gt;validate([
#39;title#39;=gt;#39;required|max:255#39;,
#39;slug#39;=gt;#39;required|unique:posts#39;,
#39;category_id#39;=gt;#39;required|exists:categories,id#39;,
#39;image#39;=gt;#39;nullable|image|max:2048#39;,
#39;content#39;=gt;#39;required#39;
]);
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ kiểm tra:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;tiêu đề bắt buộclt;/pgt;lt;/ligt;lt;ligt;lt;pgt;slug không được trùnglt;/pgt;lt;/ligt;lt;ligt;lt;pgt;category phải tồn tạilt;/pgt;lt;/ligt;lt;ligt;lt;pgt;chỉ nhận ảnhlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;dung lượng tối đalt;/pgt;lt;/ligt;lt;ligt;lt;pgt;nội dung không được rỗnglt;/pgt;lt;/ligt;lt;/ulgt;lt;hr /gt;lt;h1gt;8. Upload hình ảnhlt;/h1gt;lt;pgt;Formlt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;enctype=quot;multipart/form-dataquot;
lt;/codegt;lt;/pregt;lt;pgt;Controllerlt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;$image = null;
if($request-gt;hasFile(#39;image#39;)){
$image = $request-gt;file(#39;image#39;)
-gt;store(#39;posts#39;,#39;public#39;);
}
lt;/codegt;lt;/pregt;lt;pgt;Kết quảlt;/pgt;lt;pregt;lt;codegt;storage/app/public/posts/
abc123.jpg
xyz456.png
lt;/codegt;lt;/pregt;lt;pgt;Laravel sẽ tự sinh tên file ngẫu nhiên để tránh trùng.lt;/pgt;lt;hr /gt;lt;h1gt;9. Lưu dữ liệult;/h1gt;lt;pregt;lt;code class=quot;language-phpquot;gt;Post::create([
#39;user_id#39;=gt;auth()-gt;id(),
#39;category_id#39;=gt;$request-gt;category_id,
#39;title#39;=gt;$request-gt;title,
#39;slug#39;=gt;$request-gt;slug,
#39;image#39;=gt;$image,
#39;excerpt#39;=gt;$request-gt;excerpt,
#39;content#39;=gt;$request-gt;content
]);
lt;/codegt;lt;/pregt;lt;pgt;Tác giả sẽ được lấy từlt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;auth()-gt;id()
lt;/codegt;lt;/pregt;lt;pgt;Người dùng không thể giả mạo tác giả bài viết.lt;/pgt;lt;hr /gt;lt;h1gt;10. Hiển thị ảnhlt;/h1gt;lt;pgt;Bladelt;/pgt;lt;pregt;lt;code class=quot;language-bladequot;gt;lt;img src=quot;{{ asset(#39;storage/#39;.$post-gt;image) }}quot;gt;
lt;/codegt;lt;/pregt;lt;pgt;Đừng quên tạo symbolic linklt;/pgt;lt;pregt;lt;code class=quot;language-bashquot;gt;php artisan storage:link
lt;/codegt;lt;/pregt;lt;pgt;Sau khi chạy lệnh này, Laravel sẽ tạolt;/pgt;lt;pregt;lt;codegt;public/storage
lt;/codegt;lt;/pregt;lt;pgt;trỏ tớilt;/pgt;lt;pregt;lt;codegt;storage/app/public
lt;/codegt;lt;/pregt;lt;hr /gt;lt;h1gt;11. Cập nhật bài viếtlt;/h1gt;lt;pgt;Nếu người dùng chọn ảnh mớilt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;Storage::disk(#39;public#39;)-gt;delete($post-gt;image);
lt;/codegt;lt;/pregt;lt;pgt;sau đó upload lại.lt;/pgt;lt;pgt;Nếu không chọn ảnhlt;/pgt;lt;pgt;Laravel vẫn giữ ảnh cũ.lt;/pgt;lt;pgt;Đây là cách xử lý được sử dụng trong hầu hết các CMS hiện nay.lt;/pgt;lt;hr /gt;lt;h1gt;12. Xóa bài viếtlt;/h1gt;lt;pgt;Sửa hàm destroy trong PostControllerlt;/pgt;
lt;pregt;lt;codegt;public function destroy(string $id)
{
//
$post = Post::findOrFail($id);
$post-gt;delete();
return redirect()
-gt;route(#39;posts.index#39;)
-gt;with(#39;success#39;, #39;Xóa bài viết thành công.#39;);
}
lt;/codegt;lt;/pregt;
lt;pgt;Khi xóalt;/pgt;lt;pregt;lt;code class=quot;language-phpquot;gt;if (!empty($post-gt;image)) {
Storage::disk(#39;public#39;)-gt;delete($post-gt;image);
}
$post-gt;delete();
lt;/codegt;lt;/pregt;lt;pgt;Không nên để file ảnh bị quot;mồ côiquot; trong thư mục Storage.lt;/pgt;lt;hr /gt;lt;h1gt;13. Giao diện quản trịlt;/h1gt;lt;pgt;Danh sách nên hiển thịlt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Thumbnaillt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tiêu đềlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Danh mụclt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Tác giảlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Ngày tạolt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Trạng tháilt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nút Editlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Nút Deletelt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Có thể kết hợp:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Tailwind CSSlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;DataTables.netlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Badge màult;/pgt;lt;/ligt;lt;ligt;lt;pgt;Icon Heroiconslt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Xác nhận trước khi xóalt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Đây cũng là giao diện mà chúng ta sẽ sử dụng xuyên suốt các module còn lại.lt;/pgt;lt;pgt;Nếu sau khi chỉnh sửa giao diện mà k thay đổi gì phải chạy npm run buildlt;/pgt;lt;hr /gt;lt;h1gt;14. Kết quả đạt đượclt;/h1gt;lt;pgt;Sau bài học này, Blog CMS đã có module quản lý bài viết hoàn chỉnh.lt;/pgt;lt;pgt;✔ Thêm bài viếtlt;/pgt;lt;pgt;✔ Sửa bài viếtlt;/pgt;lt;pgt;✔ Xóa bài viếtlt;/pgt;lt;pgt;✔ Upload ảnhlt;/pgt;lt;pgt;✔ Relationship Userlt;/pgt;lt;pgt;✔ Relationship Categorylt;/pgt;lt;pgt;✔ Validationlt;/pgt;lt;pgt;✔ Hiển thị ảnhlt;/pgt;lt;pgt;✔ Phân tranglt;/pgt;lt;pgt;✔ Quản lý tác giảlt;/pgt;lt;pgt;Đây là một module CRUD thực tế mà bạn sẽ gặp trong hầu hết các hệ thống quản trị nội dung sử dụng Laravel.lt;/pgt;lt;hr /gt;lt;h1gt;Tổng kếtlt;/h1gt;lt;pgt;Trong bài học này chúng ta đã kết hợp rất nhiều kiến thức đã học trước đó:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;lt;stronggt;Resource Controllerlt;/stronggt; để xây dựng CRUD nhanh chóng.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Eloquent Relationshiplt;/stronggt; để liên kết Post với Category và User.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Validationlt;/stronggt; để đảm bảo dữ liệu hợp lệ.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Storagelt;/stronggt; để quản lý hình ảnh.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Authenticationlt;/stronggt; để tự động gán tác giả cho bài viết.lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;lt;stronggt;Paginationlt;/stronggt; giúp danh sách bài viết dễ theo dõi khi số lượng tăng lên.lt;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Đến thời điểm này, dự án Blog CMS đã gần hoàn chỉnh và có cấu trúc tương tự nhiều hệ thống quản trị nội dung được sử dụng trong thực tế.lt;/pgt;lt;hr /gt;lt;h1gt;Bài tiếp theolt;/h1gt;lt;pgt;Ở lt;stronggt;Bài 28 — CRUD Userslt;/stronggt;, chúng ta sẽ xây dựng module quản lý người dùng dành cho quản trị viên:lt;/pgt;lt;ulgt;lt;ligt;lt;pgt;Danh sách Userlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Chỉnh sửa thông tinlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Thay đổi Role (Admin/User)lt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Khóa hoặc mở khóa tài khoảnlt;/pgt;lt;/ligt;lt;ligt;lt;pgt;Đặt lại mật khẩult;/pgt;lt;/ligt;lt;/ulgt;lt;pgt;Sau bài này, Blog CMS sẽ có đầy đủ ba module quản trị cốt lõi: lt;stronggt;Categorieslt;/stronggt;, lt;stronggt;Postslt;/stronggt; và lt;stronggt;Userslt;/stronggt;.lt;/pgt;