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
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
| Hook | Loại | Khi nào chạy | Dùng để làm gì |
|---|---|---|---|
after_setup_theme | Action | Rất sớm, trước init | Khai báo theme support, đăng ký menu |
init | Action | Sau khi WP load xong, trước khi xử lý request | Đăng ký CPT, taxonomy, shortcode |
wp_enqueue_scripts | Action | Trước khi render frontend | Load CSS/JS cho frontend |
admin_enqueue_scripts | Action | Trước khi render admin | Load CSS/JS cho trang admin |
wp_head | Action | Trong thẻ <head> | In ra meta tag, link css (KHÔNG dùng để enqueue!) |
wp_footer | Action | Trước </body> | In script, tracking code |
save_post | Action | Khi 1 post được lưu | Xử lý lưu custom field |
the_content | Filter | Khi hiển thị nội dung bài viết | Chèn quảng cáo, sửa nội dung tự động |
the_title | Filter | Khi hiển thị tiêu đề | Sửa/thêm vào tiêu đề |
body_class | Filter | Khi 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ìmsingle-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$postcho post tiếp theo — bắt buộc gọi đầu vòng lặp, nếu không các hàmthe_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ừ$posttoàn cục và echo ra luôn (khác vớiget_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ách | Khi nào dùng | Reset cần không |
|---|---|---|
have_posts() / the_post() mặc định | Loop chính của trang | Không cần (đã là query gốc) |
new WP_Query([...]) | Query phụ, độc lập, cần custom nhiều điều kiện | Cầ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ường | N/A |
pre_get_posts filter | Sử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() và $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
truetrongwp_enqueue_script→ load script ởwp_footerthay 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ử
- Cài theme vào
wp-content/themes/my-theme/, activate trong wp-admin. - Tạo vài bài viết test, xem trang chủ hiển thị đúng Loop.
- 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):
- Action và Filter khác nhau ở điểm mấu chốt nào? (Filter phải return giá trị, Action thì không)
- Tại sao không nên
echoscript trực tiếp vàowp_headmà phải dùngwp_enqueue_script? (WP không quản lý được dependency/version, dễ load trùng) wp_reset_postdata()dùng để làm gì, nếu quên thì hậu quả gì? (Trả$postglobal về đúng post gốc sau khi dùng WP_Query phụ; quên thì code sau đó lấy nhầm dữ liệu)- 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ùngpre_get_poststhay thế) - Kể tên đúng thứ tự ưu tiên Template Hierarchy cho trang single của CPT
product, slugao-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.
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
Hãy là người đầu tiên bình luận về chương này!