Blade cơ bản (Laravel 13) — dành cho người mới

Nguyễn Dương Thế Vĩ · Created: 10:41 08/09/2026 · Updated: 02:08 13/09/2026 · 10 Views

Blade là bộ tạo template (view engine) của Laravel. Bạn viết file HTML trộn với vài cú pháp gọn (`{{ }}`, `@if`, `@foreach`...), Laravel biên dịch thành PHP thuần rồi cache lại — nên vừa dễ viết vừa chạy nhanh.

Blade cơ bản (Laravel 13) — dành cho người mới

Blade là bộ tạo template (view engine) của Laravel. Bạn viết file HTML trộn với vài cú pháp gọn ({{ }}, @if, @foreach...), Laravel biên dịch thành PHP thuần rồi cache lại — nên vừa dễ viết vừa chạy nhanh.

  • File view nằm ở resources/views/, đuôi .blade.php.
  • File biên dịch nằm ở storage/framework/views/ (bạn không sửa tay). Đổi view mà thấy chưa cập nhật? Chạy php artisan view:clear.
  • Trong dự án này mọi trang đều kế thừa layout chung resources/views/layouts/app.blade.php.

1. In dữ liệu ra màn hình

{{ }} — in an toàn (dùng 99% trường hợp)

{{ }} tự động escape HTML (chống XSS). Đây là cách in mặc định.

<h1>{{ $post->title }}</h1>
<span>{{ $post->user?->name }}</span>        {{-- ?-> : nếu user null thì không lỗi --}}
<p>{{ $post->description ?: 'Chưa có mô tả' }}</p>  {{-- ?: giá trị mặc định --}}
<time>{{ $post->created_at->format('H:i d/m/Y') }}</time>

Bên trong {{ }}PHP thuần, nên bạn dùng được toán tử, gọi hàm, ternary...

{!! !!} — in HTML thô (cẩn thận!)

{!! !!} không escape — trình duyệt sẽ hiểu chuỗi là HTML. Chỉ dùng khi nội dung đã được làm sạch. Ví dụ thật trong dự án (bài viết Markdown đã lọc thẻ nguy hiểm):

{{-- resources/views/pages/posts/show.blade.php --}}
<div class="markdown">
    {!! $post->renderedContent() !!}
</div>

⚠️ Không bao giờ đổ dữ liệu người dùng nhập thẳng vào {!! !!} nếu chưa sanitize — đó là lỗ hổng XSS.

Ghi chú (comment) — {{-- --}}

{{-- Comment này chỉ có trong source, KHÔNG xuất ra HTML gửi về trình duyệt. --}}

2. Điều kiện (if / else)

@if ($post->thumbnail)
    <img src="{{ $post->thumbnail }}" alt="">
@elseif ($post->description)
    <p>{{ $post->description }}</p>
@else
    <p>Không có gì để hiển thị.</p>
@endif

Các directive tiện lợi khác:

@unless (auth()->check())      {{-- = @if (! ...) --}}
    <a href="{{ route('login') }}">Đăng nhập</a>
@endunless

@isset($post)   ...  @endisset     {{-- biến tồn tại & không null --}}
@empty($posts)  ...  @endempty     {{-- biến "rỗng" (null, [], '', 0) --}}

Kiểm tra đăng nhập nhanh (rất hay dùng trong partials/auth-menu.blade.php):

@auth
    Xin chào, {{ auth()->user()->name }}
@endauth

@guest
    <a href="{{ route('login') }}">Đăng nhập</a>
@endguest

3. Vòng lặp

@forelse — lặp mảng, có sẵn nhánh "rỗng" (mẫu dùng cho danh sách bài viết)

{{-- resources/views/pages/home/index.blade.php --}}
@forelse ($posts as $post)
    <article>{{ $post->title }}</article>
@empty
    <div>{{ __('LBL_POST_EMPTY') }}</div>
@endforelse

@foreach và biến $loop

@foreach ($items as $item)
    <li>
        {{ $loop->iteration }}. {{ $item }}   {{-- đếm từ 1 --}}
        @if ($loop->first) (đầu tiên) @endif
        @if ($loop->last)  (cuối cùng) @endif
    </li>
@endforeach

$loop còn có: index (từ 0), count, even, odd, remaining... Ngoài ra có @for, @while, @foreach ... @endforeach như PHP thường.


4. Layout: kế thừa & chèn nội dung

Trong dự án, mọi trang viết theo mẫu này:

{{-- resources/views/pages/home/index.blade.php --}}
@extends('layouts.app')            {{-- kế thừa layout chung --}}
@section('page', 'home')           {{-- tên page (để tự nạp CSS/JS riêng nếu có) --}}

@section('title', __('LBL_TITLE_HOME'))            {{-- đổ vào @yield('title') của layout --}}
@section('meta_description', __('LBL_SEO_HOME'))

@section('content')                {{-- khối nội dung chính --}}
    <h1>{{ __('LBL_POSTS_TITLE') }}</h1>
    ...
@endsection

Bên trong layout (layouts/app.blade.php) là các "chỗ trống" chờ nội dung:

<title>@yield('title', config('app.name', 'The Vi Blog'))</title>  {{-- có giá trị mặc định --}}
<main>
    @yield('content')   {{-- nội dung trang con đổ vào đây --}}
</main>
Directive Ý nghĩa
@extends('layouts.app') Trang con kế thừa layout nào
@section('x') ... @endsection Định nghĩa nội dung cho vùng x
@section('title', 'văn bản') Dạng ngắn: gán 1 dòng
@yield('x', 'mặc định') (Trong layout) đặt nội dung của section x vào đây
@include('partials.nav') Chèn 1 view con vào đúng chỗ (dùng chung header/footer)
@includeWhen($dk, 'view') Chỉ chèn khi điều kiện đúng

5. @push / @stack / @once — thêm CSS/JS đúng chỗ

Layout khai báo "ngăn xếp" bằng @stack, trang/component "đẩy" nội dung vào bằng @push:

{{-- Trong layout <head>: --}} @stack('styles')
{{-- Cuối <body>:        --}} @stack('scripts')
{{-- Một component nạp thư viện CDN của riêng nó — chỉ khi trang thực sự dùng: --}}
@once                              {{-- @once: chỉ chạy 1 lần dù component xuất hiện nhiều lần --}}
    @push('styles')
        <link rel="stylesheet" href="https://.../glightbox.min.css">
    @endpush
    @push('scripts')
        <script src="https://.../glightbox.min.js"></script>
    @endpush
@endonce

Đây chính là cách <x-gallery>partials/tooltip tự nạp thư viện — bạn chỉ cần dùng component, không phải nhớ thêm <script> vào layout.


6. Component <x-...> — mảnh giao diện tái sử dụng

Dự án dùng anonymous component: mỗi file trong resources/views/components/ tự động thành một thẻ <x-tên-file>. Thư mục con → dùng dấu chấm (components/form/input.blade.php<x-form.input>).

Cách gọi

<x-icon name="pencil" />                          {{-- prop tĩnh: name="pencil" --}}
<x-icon name="gear" class="text-xl" />            {{-- class thêm sẽ được gộp vào (xem $attributes) --}}
<x-form.input name="title" :label="__('LBL_POST_TITLE')" required />
<x-gallery :src="$post->thumbnail" :alt="$post->title" />
  • name="..." → truyền chuỗi tĩnh.
  • :label="..." → có dấu : nghĩa là vế phải là biểu thức PHP (biến, hàm...).
  • required (không có giá trị) → prop boolean = true.

Cách khai báo bên trong component

{{-- resources/views/components/icon.blade.php --}}
@props(['name'])   {{-- khai báo prop; có thể đặt mặc định: ['name' => 'house'] --}}

<i {{ $attributes->merge(['class' => 'bi bi-' . $name]) }} aria-hidden="true"></i>
  • @props([...]) — liệt kê các prop component nhận.
  • $attributesmọi thuộc tính khác người gọi truyền vào (class, id, data-*...).
  • $attributes->merge(['class' => '...']) — gộp class mặc định của component với class người gọi thêm (nhờ vậy <x-icon class="text-xl"> vẫn giữ được bi bi-...).

Slot — nội dung nằm giữa thẻ

<x-tooltip content="Gợi ý">   {{-- $slot = phần bên trong --}}
    <button>Di chuột vào tôi</button>
</x-tooltip>

Bên trong component in slot bằng {{ $slot }}.


7. Form, CSRF và hiển thị lỗi validation

{{-- resources/views/components/posts/form.blade.php (rút gọn) --}}
<form method="post" action="{{ $action }}" enctype="multipart/form-data">
    @csrf                       {{-- BẮT BUỘC với mọi form POST — token chống CSRF --}}
    @if ($method === 'PUT')
        @method('PUT')          {{-- giả lập PUT/PATCH/DELETE (HTML chỉ có GET/POST) --}}
    @endif

    <x-form.input name="title" :label="__('LBL_POST_TITLE')" :value="$post?->title" required />
    <button type="submit">{{ __('LBL_POST_SAVE') }}</button>
</form>

Các "trợ thủ" cho form:

  • old('title', $mặcĐịnh) — giữ lại giá trị user vừa nhập khi form bị lỗi & load lại (component x-form.input dùng sẵn: value="{{ old($name, $value) }}").
  • $errors — túi lỗi validation. Lấy lỗi 1 trường: $errors->first('title').
  • @error('title') ... @enderror — chạy khi trường đó có lỗi:
<input name="title" value="{{ old('title') }}">
@error('title')
    <p class="text-red-600">{{ $message }}</p>   {{-- $message có sẵn trong @error --}}
@enderror
  • Directive gán thuộc tính có điều kiện (rất gọn):
<input @required($post === null)>          {{-- thêm "required" nếu đúng --}}
<input type="checkbox" @checked($post->is_active)>
<option @selected($city === 'HN')>Hà Nội</option>
<button @disabled($dangGui)>Gửi</button>

8. Phân quyền trong view: @can

Ẩn/hiện nút theo quyền (dựa trên Policy). Ví dụ thật ở trang chủ: chỉ tác giả/admin mới thấy nút Sửa/Xoá.

@can('create', \App\Models\Post::class)
    <a href="{{ route('posts.create') }}">{{ __('LBL_POSTS_ADD') }}</a>
@endcan

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">{{ __('LBL_POST_EDIT') }}</a>
@endcan

@cannot('delete', $post)
    {{-- người không có quyền xoá --}}
@endcannot

9. Các hàm hay dùng trong {{ }}

Hàm Công dụng Ví dụ
__('LBL_...') Dịch chữ theo ngôn ngữ hiện tại (Anh/Việt) {{ __('LBL_POSTS_TITLE') }}
route('tên', $tham_số) Sinh URL từ tên route (đừng viết URL cứng) {{ route('posts.show', $post) }}
url('/duong-dan') URL tuyệt đối từ đường dẫn {{ url('/images/logo.png') }}
asset('duong-dan') Link tới file tĩnh trong public/ {{ asset('favicon.png') }}
config('app.name') Đọc cấu hình Laravel {{ config('app.name') }}
setting('features.x') Đọc config/settings.php (cấu hình app) @if (setting('features.show_language_switcher'))
old('field', $default) Giá trị cũ sau khi validate lỗi value="{{ old('email') }}"
auth()->user() User đang đăng nhập (hoặc null) {{ auth()->user()?->name }}

10. Chèn PHP thuần: @php

Thỉnh thoảng cần tính toán nhỏ ngay trong view:

@php
    $id = $attributes->get('id', $name);
@endphp

Hạn chế nhồi logic vào view. Logic nghiệp vụ để ở Service/Controller (xem KIEN_TRUC.md); view chỉ nên lo hiển thị.


11. Ghép lại — một trang hoàn chỉnh (rút gọn từ dự án)

@extends('layouts.app')
@section('page', 'home')
@section('title', __('LBL_TITLE_HOME'))

@section('content')
    <div class="mb-6 flex items-center justify-between">
        <h1>{{ __('LBL_POSTS_TITLE') }}</h1>
        @can('create', \App\Models\Post::class)
            <a href="{{ route('posts.create') }}">
                <x-icon name="plus-lg" /> {{ __('LBL_POSTS_ADD') }}
            </a>
        @endcan
    </div>

    @forelse ($posts as $post)
        <article>
            @if ($post->thumbnail)
                <x-gallery :src="$post->thumbnail" :alt="$post->title" />
            @endif
            <a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
            <span>{{ $post->user?->name }} · {{ $post->created_at->format('d/m/Y') }}</span>
        </article>
    @empty
        <p>{{ __('LBL_POST_EMPTY') }}</p>
    @endforelse

    {{ $posts->links() }}   {{-- thanh phân trang tự động của Laravel --}}
@endsection

12. Mẹo cho người mới

  • Đổi view không thấy cập nhật? php artisan view:clear (xoá cache view đã biên dịch).
  • Ưu tiên component có sẵn (<x-form.*>, <x-display.*>, <x-icon>, <x-gallery>) thay vì viết HTML thô — vừa nhất quán vừa đỡ lặp.
  • Chữ hiển thị luôn qua __('LBL_*') và thêm vào cả lang/en.json + lang/vi.json.
  • URL luôn qua route(), không viết /posts/1 cứng — route đổi là view vẫn đúng.
  • {{ }} là mặc định; chỉ dùng {!! !!} khi nội dung chắc chắn an toàn.
  • Xem "mặt thật" một component: mở file của nó trong resources/views/components/.