Joomla Bài học 29: Tùy biến trang báo lỗi (error.php), trang bảo trì (offline.php) và giao diện in ấn (component.php)
Trong quá trình vận hành một website doanh nghiệp, trải nghiệm người dùng (UX) không chỉ quyết định bởi trang chủ hay bài viết chi tiết, mà còn được đo lường qua những khoảnh khắc hệ thống gặp sự cố: khi khách truy cập vào một đường dẫn không tồn tại (Lỗi 404), khi máy chủ quá tải (Lỗi 500), khi website tạm khóa để bảo trì (Offline), hay khi họ cần in một tài liệu tài chính quan trọng mà không muốn dính các thanh Menu hay Sidebar rườm rà.
Trong Joomla Bài học 29, chúng ta sẽ tự tay lập trình 3 tệp tin hệ thống cực kỳ quan trọng nằm trong thư mục /templates/joomlalab/: error.php, offline.php và component.php.
1. Lập trình trang báo lỗi chuyên nghiệp (error.php)
Khi xảy ra lỗi HTTP (phổ biến nhất là 404 Not Found hoặc 500 Internal Server Error), Joomla sẽ ném ra ngoại lệ và tìm kiếm file error.php bên trong thư mục Template hiện tại.
Điểm đặc biệt của error.php là nó hoạt động độc lập với hệ thống Component chính, do đó bạn có thể tự do kết hợp các biến thông tin lỗi để thiết kế giao diện sáng tạo, thân thiện.
Các biến cốt lõi có sẵn trong error.php:
-
$this->error: Đối tượng Exception chứa mã lỗi và thông điệp.-
$this->error->getCode(): Lấy mã lỗi HTTP (Ví dụ:404,403,500). -
$this->error->getMessage(): Lấy nội dung thông báo lỗi gốc từ hệ thống.
-
-
$this->debug: Biến kiểu boolean (true/false) cho biết hệ thống có đang bật chế độ Debug hay không (nếu bật, hiển thị Stack Trace chi tiết cho lập trình viên xem).
Mã nguồn hoàn chỉnh cho src/templates/joomlalab/error.php:
PHP
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
/** @var \Joomla\CMS\Document\ErrorDocument $this */
$app = Factory::getApplication();
$error = $this->error;
$code = $error->getCode();
$message = $error->getMessage();
// Lấy đường dẫn gốc của website để làm nút bấm về trang chủ
$baseUri = Uri::root();
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lỗi <?php echo $code; ?> - <?php echo htmlspecialchars($this->title, ENT_QUOTES, 'UTF-8'); ?></title>
<!-- Nạp nhanh Bootstrap 5 từ CDN hoặc thông qua Web Asset Manager -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
body {
background-color: #f8fafc;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.error-code {
font-size: 7rem;
font-weight: 800;
color: #2563eb;
line-height: 1;
}
</style>
</head>
<body>
<div class="container error-container">
<div class="row justify-content-center text-center w-100">
<div class="col-12 col-md-8 col-lg-6">
<div class="card border-0 shadow-sm rounded-4 p-5 bg-white">
<div class="error-code mb-3"><?php echo $code ? $code : '500'; ?></div>
<h2 class="fw-bold text-dark mb-2">
<?php if ($code == 404) : ?>
Ôi không! Không tìm thấy trang bạn yêu cầu.
<?php elseif ($code == 403) : ?>
Truy cập bị từ chối (403 Forbidden).
<?php else : ?>
Đã xảy ra lỗi hệ thống máy chủ.
<?php endif; ?>
</h2>
<p class="text-secondary mb-4">
<?php if ($code == 404) : ?>
Đường dẫn có thể đã bị thay đổi hoặc trang web không còn tồn tại trên hệ thống của chúng tôi.
<?php else : ?>
<?php echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); ?>
<?php endif; ?>
</p>
<div class="d-flex justify-content-center gap-3">
<a href="<?php echo $baseUri; ?>" class="btn btn-primary rounded-pill px-4 py-2 fw-semibold">
<i class="fas fa-home me-2"></i> Trở về Trang chủ
</a>
<a href="javascript:history.back()" class="btn btn-outline-secondary rounded-pill px-4 py-2 fw-semibold">
<i class="fas fa-arrow-left me-2"></i> Quay lại trang trước
</a>
</div>
<?php if ($this->debug) : ?>
<div class="text-start mt-5 pt-3 border-top small text-muted">
<p class="fw-bold text-danger mb-1"><i class="fas fa-bug me-1"></i> Chi tiết Debug (Chỉ hiển thị cho Dev):</p>
<pre class="bg-light p-3 rounded-3 overflow-auto" style="max-height: 200px;"><?php echo htmlspecialchars($error->getTraceAsString(), ENT_QUOTES, 'UTF-8'); ?></pre>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</body>
</html>
2. Lập trình trang bảo trì hệ thống (offline.php)
Khi bạn bật chế độ Site Offline = Yes trong Global Configuration (hoặc khi thực hiện nâng cấp database lớn), toàn bộ giao diện Frontend sẽ bị khóa lại và hiển thị nội dung từ file offline.php.
Điểm đặc biệt của trang bảo trì là nó vẫn cho phép Quản trị viên (Admin) đăng nhập trực tiếp ngay trên khung giao diện này để kiểm tra website mà không cần phải vào đường dẫn /administrator.
Mã nguồn hoàn chỉnh cho src/templates/joomlalab/offline.php:
PHP
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
/** @var \Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$sitename = $app->get('sitename');
$offlineMessage = $app->get('offline_message');
$logo = $app->get('offline_image');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($sitename, ENT_QUOTES, 'UTF-8'); ?> - Đang bảo trì hệ thống</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
body {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
min-height: 100vh;
display: flex;
align-items: center;
color: #fff;
font-family: system-ui, -apple-system, sans-serif;
}
</style>
</head>
<body>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-md-7 col-lg-5">
<div class="card border-0 shadow-lg rounded-4 p-4 p-md-5 bg-white text-dark">
<div class="text-center mb-4">
<?php if (!empty($logo)) : ?>
<img src="<?php echo Uri::root() . htmlspecialchars($logo, ENT_QUOTES, 'UTF-8'); ?>" alt="Logo" height="60" class="mb-3">
<?php else : ?>
<div class="inline-block bg-primary text-white rounded-circle p-3 mb-3" style="width: 70px; height: 70px; display: inline-flex; align-items: center; justify-content: center; font-size: 2rem;">
<i class="fas fa-tools"></i>
</div>
<?php endif; ?>
<h3 class="fw-bold text-dark"><?php echo htmlspecialchars($sitename, ENT_QUOTES, 'UTF-8'); ?></h3>
<p class="text-muted small">Hệ thống đang được nâng cấp định kỳ để phục vụ tốt hơn.</p>
</div>
<!-- THÔNG BÁO BẢO TRÌ TỪ GLOBAL CONFIGURATION -->
<div class="alert alert-warning border-0 rounded-3 small mb-4">
<i class="fas fa-info-circle me-1"></i>
<?php if (!empty($offlineMessage)) : ?>
<?php echo $offlineMessage; ?>
<?php else : ?>
Trang web hiện đang tạm đóng để bảo trì. Vui lòng quay lại sau ít phút!
<?php endif; ?>
</div>
<!-- FORM ĐĂNG NHẬP DÀNH CHO ADMIN NGAY TẠI TRANG BẢO TRÌ -->
<form action="<?php echo Uri::root(true); ?>/index.php?option=com_users&task=user.login" method="post" class="form-validate">
<div class="mb-3">
<label for="username" class="form-label small fw-semibold text-secondary">Tên đăng nhập Quản trị</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="fas fa-user text-muted"></i></span>
<input type="text" name="username" id="username" class="form-control bg-light border-start-0" required placeholder="Username">
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label small fw-semibold text-secondary">Mật khẩu</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="fas fa-lock text-muted"></i></span>
<input type="password" name="password" id="password" class="form-control bg-light border-start-0" required placeholder="Password">
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" name="remember" id="remember" class="form-check-input" value="yes">
<label class="form-check-label small text-secondary" for="remember">Nhớ tài khoản trên thiết bị này</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary rounded-pill py-2 fw-semibold shadow-sm">
<i class="fas fa-sign-in-alt me-2"></i> Đăng nhập Quản trị
</button>
</div>
<!-- THẺ CSRF TOKEN BẢO MẬT BẮT BUỘC -->
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
3. Lập trình giao diện In ấn tối giản (component.php)
Khi người dùng bấm vào nút Print hoặc Email trên một bài viết, hoặc khi bạn mở trang trong một cửa sổ Popup Modal, nếu hệ thống hiển thị nguyên vẹn cả thanh Menu, Sidebar, Banner và Footer thì khi in ra giấy sẽ cực kỳ lãng phí giấy và mực.
File component.php sinh ra để giải quyết bài toán này. Nó đóng vai trò là một Master Layout thu gọn, chỉ chứa đúng nội dung trung tâm của Component (<jdoc:include type="component" />) mà không có bất kỳ khối điều hướng nào xung quanh.
Cách gọi giao diện này qua URL:
Bất kỳ đường dẫn nào trên Joomla, khi bạn cộng thêm tham số ?tmpl=component vào cuối (Ví dụ: http://localhost:8080/tin-tuc/bai-viet?tmpl=component), Joomla sẽ tự động bỏ qua file index.php thông thường và chuyển sang render bằng file component.php!
Mã nguồn hoàn chỉnh cho src/templates/joomlalab/component.php:
PHP
<?php
\defined('_JEXEC') or die;
use Joomla\CMS\Factory;
/** @var \Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$wa = $this->getWebAssetManager();
// Nạp bộ asset cơ bản của template nhưng loại bỏ các menu điều hướng không cần thiết
$wa->useStyle('template.joomlalab.base');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
<style>
body {
background-color: #fff !important;
color: #000 !important;
padding: 20px;
}
/* Tự động ẩn các nút bấm in ấn khi đang ở chế độ in thật qua máy in */
@media print {
.no-print {
display: none !important;
}
}
</style>
</head>
<body class="contentpane component">
<!-- Nút bấm in nhanh hiển thị ở góc trên (sẽ tự ẩn khi gọi lệnh in của máy in) -->
<div class="container mb-4 no-print text-end">
<button onclick="window.print();" class="btn btn-sm btn-outline-dark rounded-pill px-3">
<i class="fas fa-print me-1"></i> In trang này
</button>
</div>
<main class="container">
<!-- Hàng đợi thông báo hệ thống -->
<jdoc:include type="message" />
<!-- NỘI DUNG CHÍNH CỦA COMPONENT -->
<jdoc:include type="component" />
</main>
</body>
</html>
4. Tổng kết bài học
-
Hiểu rõ tầm quan trọng của trải nghiệm UX trong các tình huống ngoại lệ (
404,500,Offline). -
Thành thạo việc lập trình file
error.phpvới các đối tượng$this->error->getCode()và$this->debug. -
Xây dựng file
offline.phptích hợp form đăng nhập trực tiếp dành cho quản trị viên. -
Làm chủ file
component.phpvà tham số?tmpl=componentchuyên dùng cho các trang in ấn hoặc popup tối giản.
Trong Bài học 30 — bài học cuối cùng khép lại Phần 3 (Lập trình Giao diện Template), chúng ta sẽ hoàn thiện quy trình với: Đóng gói (Packaging) file cài đặt Template chuẩn và kiểm thử tương thích đa thiết bị.
All Rights Reserved