Giáo trình ôn tập WordPress bài bản + Vibe Coding

Chương 1 – Kiến trúc lõi WordPress & Theme Development

Cỡ chữ: 16px
Font:
Nền:
← Chương Trước Chương Tiếp →

PHẦN A: HOOKS SYSTEM — “Xương sống” của WordPress

1. Tại sao WordPress cần Hooks?

Hãy tưởng tượng WordPress core là một cỗ máy đóng kín — bạn không được sửa trực tiếp file core (vì update sẽ mất hết). Hooks là những “cái móc” mà core chủ động treo sẵn ở nhiều điểm trong luồng chạy, để plugin/theme “móc” code của mình vào mà không đụng vào file gốc.

Có 2 loại hook:

2. Action Hooks — “Làm gì đó tại thời điểm này”

// Core WordPress (giả lập) có dòng này ở đâu đó trong wp-login.php:
do_action( 'wp_login', $user_login, $user );

// Bạn "móc" vào:
add_action( 'wp_login', 'my_send_welcome_email', 10, 2 );
function my_send_welcome_email( $user_login, $user ) {
    wp_mail( $user->user_email, 'Chào mừng', 'Bạn vừa đăng nhập!' );
}

Giải phẫu add_action():

add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 )
  • $hook_name: tên hook đúng như core khai báo (bắt buộc khớp chính xác).
  • $callback: hàm sẽ chạy.
  • $priority: số càng nhỏ chạy càng sớm (mặc định 10). Nếu 2 function cùng priority, chạy theo thứ tự đăng ký.
  • $accepted_args: số tham số bạn muốn nhận từ do_action(). Lỗi hay gặp nhất: quên khai báo số này → chỉ nhận được tham số đầu tiên, các tham số sau bị null.

Đặc điểm quan trọng: Action không trả về giá trị. Nó chỉ “làm việc gì đó” (gửi email, ghi log, insert DB…). Nếu bạn return trong 1 action callback, giá trị đó bị bỏ qua, vô nghĩa.

3. Filter Hooks — “Nhận vào, biến đổi, trả ra”

// Core có dòng:
$title = apply_filters( 'the_title', $post->post_title, $post->ID );

// Bạn móc vào để SỬA giá trị:
add_filter( 'the_title', 'my_add_emoji_to_title', 10, 2 );
function my_add_emoji_to_title( $title, $post_id ) {
    return '🔥 ' . $title;   // BẮT BUỘC phải return
}

Khác biệt cốt lõi với Action: Filter luôn nhận vào 1 giá trị và bắt buộc phải return giá trị đó (đã sửa hoặc giữ nguyên). Nếu bạn quên return, giá trị filter sẽ thành null và làm hỏng cả chuỗi — đây là lỗi rất phổ biến của người mới.

4. Câu hỏi kinh điển: “Cho ví dụ khi nào dùng Action, khi nào dùng Filter?”

Trả lời mẫu:

“Nếu tôi cần thực hiện một hành động phụ khi có sự kiện xảy ra (gửi email, ghi log, đồng bộ dữ liệu sang hệ thống khác) — dùng Action. Nếu tôi cần thay đổi một giá trị/nội dung trước khi nó được dùng tiếp hoặc hiển thị ra (sửa tiêu đề bài viết, thêm class vào body, đổi excerpt length) — dùng Filter.”

5. Các hook “phải thuộc lòng” vì cực kỳ hay dùng

HookLoạiKhi nào chạyDùng để làm gì
after_setup_themeActionRất sớm, trước initKhai báo theme support, đăng ký menu
initActionSau khi WP load xong, trước khi xử lý requestĐăng ký CPT, taxonomy, shortcode
wp_enqueue_scriptsActionTrước khi render frontendLoad CSS/JS cho frontend
admin_enqueue_scriptsActionTrước khi render adminLoad CSS/JS cho trang admin
wp_headActionTrong thẻ <head>In ra meta tag, link css (KHÔNG dùng để enqueue!)
wp_footerActionTrước </body>In script, tracking code
save_postActionKhi 1 post được lưuXử lý lưu custom field
the_contentFilterKhi hiển thị nội dung bài viếtChèn quảng cáo, sửa nội dung tự động
the_titleFilterKhi hiển thị tiêu đềSửa/thêm vào tiêu đề
body_classFilterKhi in class cho thẻ <body>Thêm class tuỳ điều kiện

Lỗi kinh điển cần tránh: dùng wp_head để echo '<script src="...">' thay vì dùng wp_enqueue_script(). Cách làm tay này khiến WP không quản lý được dependency, dễ bị load trùng, không cache-bust đúng version.

6. Thứ tự chạy tổng quát (đủ để hiểu, không cần thuộc số)

wp-load.php
  → load plugins (file chính của mỗi plugin được include)
  → do_action('plugins_loaded')
  → load theme's functions.php
  → do_action('after_setup_theme')
  → do_action('init')          ← hầu hết đăng ký CPT/taxonomy/shortcode ở đây
  → do_action('wp_loaded')
  → [WP xử lý query, xác định trang nào]
  → do_action('template_redirect')
  → [chọn template file theo Template Hierarchy]
  → do_action('wp_enqueue_scripts')
  → do_action('wp_head')
  → [render nội dung — Loop chạy ở đây]
  → do_action('wp_footer')

Ý nghĩa thực tế: nếu bạn gọi register_post_type() ở top-level file (ngoài mọi hook) hoặc gọi quá sớm (trước init), CPT sẽ không hoạt động đúng — đây là lỗi rất hay gặp ở người tự học.


PHẦN B: TEMPLATE HIERARCHY

1. Cơ chế hoạt động

Khi có request tới, WordPress xác định “đây là loại trang gì” (single post? archive? 404?) rồi tìm file template theo thứ tự ưu tiên giảm dần, dùng file đầu tiên tìm thấy trong theme.

2. Bảng thứ tự cho các loại trang hay gặp nhất

Trang single post:

single-{post_type}-{slug}.php
single-{post_type}.php
single.php
singular.php
index.php

Trang archive của CPT:

archive-{post_type}.php
archive.php
index.php

Trang page (không phải post):

page-{slug}.php
page-{id}.php
page.php
singular.php
index.php

Trang category:

category-{slug}.php
category-{id}.php
category.php
archive.php
index.php

Trang 404:

404.php
index.php

3. Ví dụ thực tế để nhớ lâu

Bạn có CPT product (slug: product), 1 sản phẩm slug ao-thun:

  • Xem chi tiết sản phẩm → WP tìm single-product-ao-thun.php → không có → tìm single-product.php → có → dùng file này.
  • Xem danh sách tất cả sản phẩm → WP tìm archive-product.php.

Mẹo sử dụng: Nguyên tắc: WP luôn tìm từ file cụ thể nhất đến chung nhất, cuối cùng fallback về index.php. Khi cần chính xác tra lại Developer Handbook. — Đây là tư duy đúng thay vì học vẹt.

4. Hàm hỗ trợ đọc code người khác

// In ra file template đang được dùng — cực hữu ích khi debug
add_action( 'wp_footer', function() {
    global $template;
    echo '<!-- Template: ' . $template . ' -->';
});

PHẦN C: THE LOOP — WP_Query

1. Loop mặc định (dùng query có sẵn của WordPress cho trang hiện tại)

<?php if ( have_posts() ) : ?>
    <?php while ( have_posts() ) : the_post(); ?>
        <h2><?php the_title(); ?></h2>
        <div><?php the_excerpt(); ?></div>
    <?php endwhile; ?>
<?php else : ?>
    <p>Không có bài viết nào.</p>
<?php endif; ?>

Giải thích từng hàm:

  • have_posts(): kiểm tra còn post nào chưa xử lý trong query hiện tại không.
  • the_post(): thiết lập biến toàn cục $post cho post tiếp theo — bắt buộc gọi đầu vòng lặp, nếu không các hàm the_title(), the_content()… sẽ không biết đang nói về post nào.
  • the_title(), the_excerpt(): các “template tag” tự động lấy dữ liệu từ $post toàn cục và echo ra luôn (khác với get_the_title() — trả về string, không echo).

2. Custom Query — WP_Query (query phụ, VD: sidebar “bài viết liên quan”)

$related_query = new WP_Query([
    'post_type'      => 'post',
    'posts_per_page' => 5,
    'category__in'   => [ get_query_var('cat') ],
    'post__not_in'   => [ get_the_ID() ],   // loại bỏ bài đang xem
    'orderby'        => 'date',
    'order'          => 'DESC',
]);

if ( $related_query->have_posts() ) :
    while ( $related_query->have_posts() ) : $related_query->the_post();
        the_title();
    endwhile;
endif;

wp_reset_postdata();   // ⚠️ BẮT BUỘC — trả $post toàn cục về đúng post gốc

Vì sao wp_reset_postdata() cực kỳ quan trọng:
Khi bạn dùng $related_query->the_post(), nó ghi đè biến $post toàn cục. Nếu sau đoạn code trên bạn còn code khác dùng the_title() (không qua $related_query->) mà quên reset, nó sẽ lấy nhầm dữ liệu của bài viết cuối cùng trong related_query thay vì bài viết gốc. Đây là lỗi rất phổ biến gây ra bug “tiêu đề trang bị sai” khó hiểu.

3. Phân biệt 3 cách query hay bị nhầm

CáchKhi nào dùngReset cần không
have_posts() / the_post() mặc địnhLoop chính của trangKhông cần (đã là query gốc)
new WP_Query([...])Query phụ, độc lập, cần custom nhiều điều kiệnCần wp_reset_postdata()
query_posts()KHÔNG khuyến khích — sửa query chính, phá vỡ pagination, gây side-effect khó lườngN/A
pre_get_posts filterSửa query CHÍNH trước khi nó chạy (VD: đổi số bài/trang cho archive)Không cần WP_Query mới

Ví dụ pre_get_posts (cách chuẩn để sửa query chính, thay vì query_posts()):

add_action( 'pre_get_posts', function( $query ) {
    if ( ! is_admin() && $query->is_main_query() && is_post_type_archive('product') ) {
        $query->set( 'posts_per_page', 12 );
    }
});

Lưu ý bắt buộc: check ! is_admin()$query->is_main_query() để tránh sửa nhầm query trong wp-admin hoặc query phụ khác.


PHẦN D: XÂY 1 THEME TỪ ĐẦU — THỰC HÀNH TAY

Bước 1: Cấu trúc thư mục tối thiểu

my-theme/
├── style.css          ← bắt buộc, chứa header khai báo theme
├── index.php          ← bắt buộc, fallback cuối cùng của Template Hierarchy
├── functions.php       ← nơi khai báo mọi thứ (hooks, support...)
├── header.php
├── footer.php
├── single.php
├── archive.php
└── screenshot.png      ← ảnh preview, không bắt buộc nhưng nên có

Bước 2: style.css — header bắt buộc

/*
Theme Name: My Custom Theme
Theme URI: https://example.com
Author: Your Name
Description: Theme học tập
Version: 1.0
*/

WordPress đọc comment này để nhận diện đây là 1 theme hợp lệ — không phải CSS thường.

Bước 3: functions.php — thiết lập nền tảng

<?php
// Luôn kiểm tra ABSPATH để chặn truy cập file trực tiếp — thói quen bảo mật cơ bản
if ( ! defined( 'ABSPATH' ) ) exit;

add_action( 'after_setup_theme', function() {
    // Cho phép theme dùng ảnh đại diện (featured image)
    add_theme_support( 'post-thumbnails' );

    // Cho phép WP tự quản lý thẻ <title>
    add_theme_support( 'title-tag' );

    // Đăng ký vị trí menu
    register_nav_menus([
        'primary' => __( 'Menu chính', 'my-theme' ),
    ]);

    // Cho phép dùng HTML5 markup (search form, comment form...)
    add_theme_support( 'html5', ['search-form', 'comment-form', 'gallery'] );
});

add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_style(
        'my-theme-style',
        get_stylesheet_uri(),
        [],
        wp_get_theme()->get('Version')   // dùng version theme làm cache-buster
    );

    wp_enqueue_script(
        'my-theme-script',
        get_template_directory_uri() . '/js/main.js',
        ['jquery'],           // dependency — WP tự đảm bảo jquery load trước
        '1.0',
        true                  // load ở footer, không chặn render
    );
});

Giải thích tại sao mỗi dòng lại như vậy:

  • Dùng wp_get_theme()->get('Version') thay vì hard-code '1.0' → khi bump version theme, cache tự động bust theo, khỏi sửa nhiều chỗ.
  • Tham số cuối true trong wp_enqueue_script → load script ở wp_footer thay vì wp_head, giúp trang render nhanh hơn (không chặn parse HTML).
  • Truyền ['jquery'] làm dependency → WP tự sắp xếp thứ tự load đúng, tránh lỗi “jQuery is not defined”.

Bước 4: index.php — Loop cơ bản

<?php get_header(); ?>

<main>
    <?php if ( have_posts() ) : ?>
        <?php while ( have_posts() ) : the_post(); ?>
            <article <?php post_class(); ?>>
                <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
                <?php if ( has_post_thumbnail() ) the_post_thumbnail('medium'); ?>
                <div><?php the_excerpt(); ?></div>
            </article>
        <?php endwhile; ?>

        <?php the_posts_pagination(); ?>
    <?php else : ?>
        <p>Không tìm thấy nội dung.</p>
    <?php endif; ?>
</main>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

get_header(), get_sidebar(), get_footer() tự động include header.php, sidebar.php, footer.php — đây cũng là 1 dạng “template part” convention của WP, không cần include thủ công.

Bước 5: Test thử

  1. Cài theme vào wp-content/themes/my-theme/, activate trong wp-admin.
  2. Tạo vài bài viết test, xem trang chủ hiển thị đúng Loop.
  3. Vào DevTools → Network, kiểm tra file CSS/JS load đúng qua wp_enqueue (sẽ thấy version ?ver=1.0 ở cuối URL — dấu hiệu enqueue đúng cách).

PHẦN E: TỰ KIỂM TRA CUỐI NGÀY

Trả lời to thành tiếng (không nhìn tài liệu):

  1. Action và Filter khác nhau ở điểm mấu chốt nào? (Filter phải return giá trị, Action thì không)
  2. Tại sao không nên echo script trực tiếp vào wp_head mà phải dùng wp_enqueue_script? (WP không quản lý được dependency/version, dễ load trùng)
  3. wp_reset_postdata() dùng để làm gì, nếu quên thì hậu quả gì? (Trả $post global về đúng post gốc sau khi dùng WP_Query phụ; quên thì code sau đó lấy nhầm dữ liệu)
  4. Tại sao không nên dùng query_posts()? (Sửa trực tiếp query chính, phá pagination, có side-effect toàn cục khó kiểm soát — nên dùng pre_get_posts thay thế)
  5. Kể tên đúng thứ tự ưu tiên Template Hierarchy cho trang single của CPT product, slug ao-thun. (single-product-ao-thun.php → single-product.php → single.php → singular.php → index.php)

Nếu trả lời trôi cả 5 câu không cần xem lại — bạn đã sẵn sàng cho Ngày 2. Nếu còn khựng ở câu nào, quay lại đọc phần tương ứng trước khi qua ngày mới.


📝 2,785 từ 📅 13/07/2026 👁 4 lượt đọc
← Chương Trước Chương Tiếp →
Giáo trình ôn tập WordPress bài bản + Vibe Coding

Giáo trình ôn tập WordPress bài bản + Vibe Coding

Tác giả: Giang Thanh Nhon

Trạng thái: Đang tiến hành

Số chương: 7

Đánh giá: ⭐ 10/10

💬 Bình luận (0)

Hãy là người đầu tiên bình luận về chương này!