0

Joomla Bài học 24: Xây dựng file index.php, sử dụng các thẻ <jdoc:include> và khai báo Module Positions

Ở Bài học 23, chúng ta đã dựng xong bộ khung thư mục và khai báo danh sách vị trí hiển thị trong file templateDetails.xml cho giao diện độc lập joomlalab. Trong Joomla Bài học 24, chúng ta sẽ lập trình hoàn chỉnh file index.php — nơi đóng vai trò là bộ khung xương sống (Master Layout) ghép nối dữ liệu từ Component, các khối Module, hệ thống thông báo lỗi và tài nguyên CSS/JS thành một trang HTML hoàn chỉnh trả về cho trình duyệt.

1. Bản chất cơ chế biên dịch thẻ <jdoc:include /> trong Joomla 5

Nhiều người mới học thường lầm tưởng <jdoc:include> là một thẻ HTML đặc biệt của trình duyệt. Thực chất, đây là các Placeholder Tags (Thẻ giữ chỗ nội bộ) của lớp Joomla\CMS\Document\HtmlDocument:

  • Thời điểm thực thi: Joomla áp dụng cơ chế Deferred Rendering (Kết xuất trễ). Khi người dùng gửi HTTP Request, Joomla thực thi Component chính trước tiên (để Component có thể tự thay đổi thẻ <title>, thêm CSS/JS riêng hoặc bắn ra thông báo lỗi), sau đó mới đọc file index.php của Template.

  • Quá trình Regex Parsing: Bộ Document Parser của Joomla dùng biểu thức chính quy quét qua nội dung file index.php, tìm toàn bộ các thẻ <jdoc:include type="..." /> và gọi các Document Renderer tương ứng (MetasRenderer, StylesRenderer, ScriptsRenderer, ComponentRenderer, ModulesRenderer, MessageRenderer) để thay thế các thẻ này bằng chuỗi HTML thực tế trước khi gửi phản hồi về Client.

Cú pháp thẻ <jdoc:include> Vị trí đặt Nhiệm vụ kỹ thuật & Lưu ý quan trọng
<jdoc:include type="metas" /> Trong <head> Render các thẻ <meta charset>, <title>, <meta name="description">, <link rel="canonical"> và Schema JSON-LD.
<jdoc:include type="styles" /> Trong <head> Render toàn bộ các thẻ <link rel="stylesheet"> và inline CSS đã đăng ký qua Web Asset Manager.
<jdoc:include type="scripts" /> Trong <head> Render toàn bộ các thẻ <script> (hỗ trợ thuộc tính defer và type="module" của Joomla 5).
<jdoc:include type="head" /> Trong <head> (Legacy) Thẻ gộp cũ của Joomla 3. Trên Joomla 5, khuyên dùng bộ 3 thẻ tách rời (metas, styles, scripts) ở trên để tối ưu hóa thứ tự tải tài nguyên trên trình duyệt.
<jdoc:include type="message" /> Đầu <main> Render hàng đợi thông báo hệ thống ($app->enqueueMessage()), ví dụ: "Đăng nhập thành công", "Sai mật khẩu", "Đã lưu bài viết". Chỉ xuất hiện 1 lần trên trang.
<jdoc:include type="component" /> Giữa <main> Render kết quả đầu ra của Component chính tương ứng với Menu Item hiện tại. Chỉ xuất hiện duy nhất 1 lần trong index.php.
<jdoc:include type="modules" name="..." style="..." /> Khắp <body> Render toàn bộ các Modules được gán vào vị trí name="..." với khung HTML bọc ngoài (Module Chrome) được chỉ định trong style="...".

2. Kỹ thuật kiểm tra sự tồn tại của Module với $this->countModules()

Khi xây dựng bố cục đa cột (ví dụ: Cột trái sidebar-left - Nội dung chính main - Cột phải sidebar-right), nếu một trang không có bất kỳ Module nào được bật ở vị trí sidebar-left mà bạn vẫn in thẻ <aside class="col-lg-3"> ra ngoài HTML, trang web sẽ bị một khoảng trắng rỗng chiếm mất 25% màn hình!

Để khắc phục, bạn luôn bọc khu vực chứa vị trí module trong hàm kiểm tra $this->countModules():

PHP

// 1. Kiểm tra một vị trí đơn lẻ
if ($this->countModules('sidebar-left')) {
    // Chỉ khi có ít nhất 1 module đang bật ở sidebar-left mới in thẻ <aside>
}

// 2. Kiểm tra biểu thức logic kết hợp nhiều vị trí (AND / OR)
if ($this->countModules('footer-1 or footer-2 or footer-3')) {
    // Nếu có ít nhất 1 trong 3 vị trí chân trang có module thì mới in thẻ <footer> bọc ngoài
}

Tính toán độ rộng cột Bootstrap 5 tự động bằng PHP

Trong file index.php, ta có thể tính toán class cột của vùng nội dung chính (<main>) linh hoạt dựa trên sự xuất hiện của 2 thanh Sidebar:

  • Không có Sidebar nào →\rightarrow <main class="col-12">

  • Có 1 Sidebar (Trái hoặc Phải) →\rightarrow <main class="col-lg-9">

  • Có cả 2 Sidebar (Trái và Phải) →\rightarrow <main class="col-lg-6">

3. Đọc thông tin Request và Cấu hình để gắn Dynamic Body Classes

Ở phần đầu (Logic Header) của file index.php, một lập trình viên Joomla chuyên nghiệp sẽ luôn trích xuất các biến ngữ cảnh từ Application và Input để gắn vào thuộc tính class="..." của thẻ <body>.

Việc này giúp bạn dễ dàng viết CSS nhắm trúng đích một trang cụ thể (ví dụ: .option-com_content.view-article hay .itemid-101):

  • $option = $input->getCmd('option', ''); (Ví dụ: com_content, com_users)

  • $view = $input->getCmd('view', ''); (Ví dụ: article, category, login)

  • $layout = $input->getCmd('layout', ''); (Ví dụ: blog, default)

  • $itemId = $input->getInt('Itemid', 0); (ID của Menu Item hiện tại)

  • $params = $this->params; (Đọc các tham số cấu hình động đã khai báo trong templateDetails.xml ở Bài 23).

4. Mã nguồn hoàn chỉnh của file templates/joomlalab/index.php

Hãy mở file src/templates/joomlalab/index.php và cập nhật toàn bộ mã nguồn chuẩn Joomla 5 dưới đây:

PHP

<?php
/**
 * @package     Joomla.Site
 * @subpackage  Templates.joomlalab
 */

\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();
$input = $app->getInput();
$wa    = $this->getWebAssetManager();

// 1. Lấy các tham số định tuyến hiện tại để gắn vào class của thẻ <body>
$option = $input->getCmd('option', '');
$view   = $input->getCmd('view', '');
$layout = $input->getCmd('layout', 'default');
$task   = $input->getCmd('task', '');
$itemId = $input->getInt('Itemid', 0);

// 2. Đọc các tham số cấu hình động từ templateDetails.xml (Bài 23)
$logoFile     = $this->params->get('logoFile', '');
$siteTitle    = $this->params->get('siteTitle', $app->get('sitename'));
$primaryColor = $this->params->get('primaryColor', '#2563eb');
$stickyHeader = (bool) $this->params->get('stickyHeader', 1);

// 3. Thiết lập thẻ Meta Viewport chuẩn Responsive và nạp CSS/JS cơ bản
$this->setMetaData('viewport', 'width=device-width, initial-scale=1.0');
HTMLHelper::_('bootstrap.framework'); // Nạp Bootstrap 5 Core

// Tiêm biến màu chủ đạo động vào :root CSS
$wa->addInlineStyle(':root { --joomlalab-primary: ' . htmlspecialchars($primaryColor, ENT_QUOTES, 'UTF-8') . '; }');

// 4. Tính toán số cột động cho vùng <main> dựa trên Sidebar Trái & Phải
$hasLeftSidebar  = $this->countModules('sidebar-left', true);
$hasRightSidebar = $this->countModules('sidebar-right', true);

if ($hasLeftSidebar && $hasRightSidebar) {
    $mainColClass = 'col-12 col-lg-6';
} elseif ($hasLeftSidebar || $hasRightSidebar) {
    $mainColClass = 'col-12 col-lg-9';
} else {
    $mainColClass = 'col-12';
}

$bodyClasses = sprintf(
    'site %s view-%s layout-%s task-%s itemid-%d',
    $option,
    $view,
    $layout,
    $task ?: 'none',
    $itemId
);
?>
<!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" />
</head>
<body class="<?php echo htmlspecialchars(trim($bodyClasses), ENT_QUOTES, 'UTF-8'); ?>">

    <!-- [1] TOPBAR POSITION -->
    <?php if ($this->countModules('topbar', true)) : ?>
        <div class="bg-dark text-white py-1 topbar-zone">
            <div class="container d-flex justify-content-between align-items-center">
                <jdoc:include type="modules" name="topbar" style="none" />
            </div>
        </div>
    <?php endif; ?>

    <!-- [2] HEADER & NAVIGATION -->
    <header class="bg-white border-bottom shadow-sm <?php echo $stickyHeader ? 'sticky-top' : ''; ?>">
        <div class="container py-3 d-flex flex-wrap align-items-center justify-content-between">
            <a href="<?php echo Uri::root(); ?>" class="navbar-brand fw-bold fs-4 text-decoration-none" style="color: var(--joomlalab-primary);">
                <?php if (!empty($logoFile)) : ?>
                    <img src="<?php echo Uri::root() . htmlspecialchars($logoFile, ENT_QUOTES, 'UTF-8'); ?>" alt="<?php echo htmlspecialchars($siteTitle, ENT_QUOTES, 'UTF-8'); ?>" height="40">
                <?php else : ?>
                    <?php echo htmlspecialchars($siteTitle, ENT_QUOTES, 'UTF-8'); ?>
                <?php endif; ?>
            </a>

            <?php if ($this->countModules('menu', true)) : ?>
                <nav class="main-navigation" aria-label="Main Menu">
                    <jdoc:include type="modules" name="menu" style="none" />
                </nav>
            <?php endif; ?>

            <?php if ($this->countModules('search', true)) : ?>
                <div class="header-search ms-auto ms-lg-3">
                    <jdoc:include type="modules" name="search" style="none" />
                </div>
            <?php endif; ?>
        </div>
    </header>

    <!-- [3] HERO BANNER POSITION -->
    <?php if ($this->countModules('hero-banner', true)) : ?>
        <section class="hero-banner-zone bg-light border-bottom">
            <div class="container py-4">
                <jdoc:include type="modules" name="hero-banner" style="xhtml" />
            </div>
        </section>
    <?php endif; ?>

    <!-- [4] BREADCRUMBS POSITION -->
    <?php if ($this->countModules('breadcrumbs', true)) : ?>
        <div class="container mt-3">
            <jdoc:include type="modules" name="breadcrumbs" style="none" />
        </div>
    <?php endif; ?>

    <!-- [5] MAIN LAYOUT: SIDEBAR LEFT + COMPONENT + SIDEBAR RIGHT -->
    <div class="container my-4">
        <div class="row g-4">
            <?php if ($hasLeftSidebar) : ?>
                <aside class="col-12 col-lg-3 sidebar-left">
                    <jdoc:include type="modules" name="sidebar-left" style="card" />
                </aside>
            <?php endif; ?>

            <main id="content" class="<?php echo $mainColClass; ?>">
                <!-- Khu vực hiển thị thông báo lỗi/thành công của hệ thống -->
                <jdoc:include type="message" />

                <?php if ($this->countModules('main-top', true)) : ?>
                    <div class="main-top-zone mb-4">
                        <jdoc:include type="modules" name="main-top" style="xhtml" />
                    </div>
                <?php endif; ?>

                <!-- NỘI DUNG CHÍNH CỦA COMPONENT -->
                <jdoc:include type="component" />

                <?php if ($this->countModules('main-bottom', true)) : ?>
                    <div class="main-bottom-zone mt-4">
                        <jdoc:include type="modules" name="main-bottom" style="xhtml" />
                    </div>
                <?php endif; ?>
            </main>

            <?php if ($hasRightSidebar) : ?>
                <aside class="col-12 col-lg-3 sidebar-right">
                    <jdoc:include type="modules" name="sidebar-right" style="card" />
                </aside>
            <?php endif; ?>
        </div>
    </div>

    <!-- [6] FOOTER & DEBUG POSITIONS -->
    <footer class="bg-dark text-light py-4 mt-5">
        <div class="container">
            <?php if ($this->countModules('footer', true)) : ?>
                <div class="row mb-3">
                    <jdoc:include type="modules" name="footer" style="xhtml" />
                </div>
            <?php endif; ?>
            <div class="text-center text-secondary small border-top border-secondary pt-3">
                &copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars($siteTitle, ENT_QUOTES, 'UTF-8'); ?>. All rights reserved.
            </div>
        </div>
    </footer>

    <jdoc:include type="modules" name="debug" style="none" />
</body>
</html>

Điểm tinh tế của tham số thứ 2 trong $this->countModules('sidebar-left', true):

Trên Joomla 5, khi bạn truyền thêm true ($contentEmpty = true) vào tham số thứ hai của hàm countModules(), Joomla không chỉ kiểm tra xem có Module nào đang được gán ở vị trí đó hay không, mà còn thực sự render thử Module đó xem kết quả HTML trả về có bị rỗng ("") hay không. Ví dụ: Bạn bật một Module "Bài viết liên quan" ở cột phải, nhưng bài viết hiện tại không có bài liên quan nào nên Module trả về chuỗi rỗng. Nhờ tham số true, cột sidebar-right sẽ tự động ẩn đi hoàn toàn thay vì hiện ra một cái khung trống!

5. Tổng kết bài học

  • Hiểu rõ cơ chế Deferred Rendering và vai trò của bộ 3 thẻ <head> hiện đại (metas, styles, scripts) cùng bộ 3 thẻ <body> (message, component, modules).

  • Thành thạo kỹ thuật dùng $this->countModules('position', true) để ẩn các khung HTML rỗng và tự động tính toán độ rộng cột Bootstrap 5 cho vùng <main>.

  • Biết cách trích xuất các tham số định tuyến (option, view, layout, Itemid) để tạo Dynamic Body Classes và đọc cấu hình từ $this->params->get().

Ở đoạn code trên, chúng ta mới gọi tạm HTMLHelper::_('bootstrap.framework'). Vậy làm thế nào để quản lý chuyên nghiệp các file template.css, template.js, thư viện bên thứ ba (như SwiperJS, FontAwesome) và kiểm soát thứ tự nạp của chúng? Trong Bài học 25, chúng ta sẽ làm chủ công cụ: Quản lý CSS và JavaScript hiện đại với Web Asset Manager (joomla.asset.json).


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí