Bài viết này chia sẻ đến ae cách tạo plugin WordPress cực kỳ đơn giản chỉ với vài thao tác. Quan trọng là ae không cần biết lập trình nhưng vẫn làm được, nhờ vào sự hỗ trợ của AI. Đây không phải hướng dẫn cách làm đúng hay chuẩn mực, chỉ là một ví dụ thực hành.
Mục tiêu của chúng ta là tạo được plugin nhằm thực thi được một tính năng đơn giản, nó chạy được và trình bày theo ý muốn. Ae cũng không cần quan tâm đến vấn đề hiệu suất, bảo mật vì vọc cho biết thôi, chuyện tối ưu mấy cái đó tính sau. Tư duy này sẽ giúp cho ae có một khởi đầu thuận lợi hơn thay vì nghĩ quá nhiều.

Để thực hiện thì ae cần một web nháp, web bỏ,… nói chung là web không quan trọng để test. Ở đây mình sẽ thực hành trên hosting cPanel, ae dựa vào đó làm theo nhé ! Đề bài là: Tạo một plugin để dọn dẹp revisions của website, với tính năng là hiển thị thông tin về dung lượng, các mục có revisions và nút xoá, hosting đang chạy php 8.4
Bước 01: Trong trình quản lý file của cPanel, bạn vào thư mục plugin của website đang dùng, tạo một thư mục có tên là hb-revisions >> trong thư mục đó tạo một file là hb-revisions.php >> dán đoạn code này vào file đó:
<?php
/**
* Plugin Name: HB Revisions
* Plugin URI: #
* Description: Đếm tổng số Post Revisions trong cơ sở dữ liệu WordPress.
* Version: 1.3.0
* Author: Hocban.vn
* License: GPL-2.0+
* Text Domain: hb-revisions
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'admin_menu', 'hbrev_register_menu' );
function hbrev_register_menu(): void {
add_menu_page(
'HB Revisions', 'HB Revisions', 'manage_options',
'hb-revisions', 'hbrev_render_page', 'dashicons-backup', 80
);
}
function hbrev_type_label( string $post_type ): array {
$system_types = [
'custom_css' => 'custom_css',
'customize_changeset' => 'customize_changeset',
'wp_global_styles' => 'global styles',
'wp_navigation' => 'navigation',
'wp_template' => 'template',
'wp_template_part' => 'template part',
'nav_menu_item' => 'nav menu',
'oembed_cache' => 'oembed',
'user_request' => 'user request',
'wp_block' => 'reusable block',
];
if ( isset( $system_types[ $post_type ] ) ) {
return [ 'label' => $system_types[ $post_type ], 'system' => true ];
}
$map = [
'post' => 'post',
'page' => 'page',
];
return [
'label' => $map[ $post_type ] ?? $post_type,
'system' => false,
];
}
function hbrev_count(): int {
global $wpdb;
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'"
);
}
function hbrev_size_kb(): float {
global $wpdb;
$bytes = (float) $wpdb->get_var(
"SELECT SUM(
LENGTH(post_content) + LENGTH(post_title) +
LENGTH(post_excerpt) + LENGTH(post_name)
)
FROM {$wpdb->posts}
WHERE post_type = 'revision'"
);
return $bytes / 1024;
}
function hbrev_format_size( float $kb ): string {
if ( $kb >= 1024 ) {
return number_format( $kb / 1024, 2 ) . ' MB';
}
return number_format( $kb, 1 ) . ' KB';
}
function hbrev_get_posts( int $limit = 50, int $offset = 0 ): array {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT
p.ID,
p.post_title,
p.post_type,
p.post_modified,
COUNT(r.ID) AS revision_count
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->posts} r
ON r.post_parent = p.ID AND r.post_type = 'revision'
WHERE p.post_type != 'revision'
GROUP BY p.ID
ORDER BY revision_count DESC
LIMIT %d OFFSET %d",
$limit,
$offset
),
ARRAY_A
);
}
function hbrev_total_posts_with_rev(): int {
global $wpdb;
return (int) $wpdb->get_var(
"SELECT COUNT(DISTINCT post_parent)
FROM {$wpdb->posts}
WHERE post_type = 'revision' AND post_parent > 0"
);
}
add_action( 'admin_post_hbrev_cleanup', 'hbrev_do_cleanup' );
function hbrev_do_cleanup(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Không có quyền.' );
}
check_admin_referer( 'hbrev_cleanup_nonce' );
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'" );
$wpdb->query(
"DELETE pm FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE p.ID IS NULL"
);
wp_redirect( admin_url( 'admin.php?page=hb-revisions&hbrev_done=1' ) );
exit;
}
function hbrev_styles(): void {
echo '
<style>
.hbrev-wrap {
max-width: 900px;
margin: 32px 20px 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.hbrev-summary {
display: flex;
gap: 16px;
margin-bottom: 28px;
flex-wrap: wrap;
}
.hbrev-stat {
flex: 1;
min-width: 160px;
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0,0,0,.07);
padding: 22px 24px;
text-align: center;
}
.hbrev-stat .val {
font-size: 42px;
font-weight: 700;
color: #2271b1;
line-height: 1;
display: block;
}
.hbrev-stat .lbl {
font-size: 12px;
color: #72777c;
margin-top: 6px;
display: block;
}
.hbrev-cleanup-card {
flex: 1;
min-width: 160px;
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0,0,0,.07);
padding: 22px 24px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
}
.hbrev-cleanup-card .lbl { font-size: 12px; color: #72777c; }
.hbrev-btn-delete {
display: inline-block;
background: #d63638;
color: #fff !important;
font-size: 13px;
font-weight: 600;
border: none;
border-radius: 6px;
padding: 9px 20px;
cursor: pointer;
text-decoration: none;
transition: background .15s;
}
.hbrev-btn-delete:hover { background: #b32d2e; color: #fff !important; }
.hbrev-notice {
background: #edfaef;
border-left: 4px solid #00a32a;
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 20px;
font-size: 13px;
color: #1d6a2e;
}
/* Bảng */
.hbrev-table-wrap {
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 8px rgba(0,0,0,.07);
overflow: hidden;
}
.hbrev-table-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f1;
}
.hbrev-table-head h2 { font-size: 14px; font-weight: 600; color: #1d2327; margin: 0; }
.hbrev-table-head span { font-size: 12px; color: #72777c; }
table.hbrev-table { width: 100%; border-collapse: collapse; font-size: 13px; }
table.hbrev-table thead th {
background: #f6f7f7;
color: #50575e;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .4px;
padding: 10px 16px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
table.hbrev-table thead th.col-num { width: 48px; text-align: center; }
table.hbrev-table thead th.col-rev { width: 110px; text-align: center; }
table.hbrev-table thead th.col-type { width: 120px; }
table.hbrev-table thead th.col-date { width: 140px; }
table.hbrev-table tbody tr { border-bottom: 1px solid #f0f0f1; transition: background .1s; }
table.hbrev-table tbody tr:last-child { border-bottom: none; }
table.hbrev-table tbody tr:hover { background: #f8f9fa; }
table.hbrev-table td { padding: 11px 16px; color: #1d2327; vertical-align: middle; }
table.hbrev-table td.col-num { text-align: center; color: #a0a5aa; font-size: 12px; }
table.hbrev-table td.col-rev { text-align: center; }
.hbrev-badge {
display: inline-block;
color: #fff;
font-size: 12px;
font-weight: 700;
border-radius: 20px;
padding: 2px 10px;
min-width: 32px;
text-align: center;
}
.hbrev-badge.high { background: #d63638; }
.hbrev-badge.med { background: #dba617; }
.hbrev-badge.low { background: #2271b1; }
.hbrev-title a { color: #2271b1; text-decoration: none; font-weight: 500; }
.hbrev-title a:hover { text-decoration: underline; }
.hbrev-title .post-id { font-size: 11px; color: #a0a5aa; margin-left: 6px; }
.hbrev-type {
display: inline-block;
font-size: 11px;
border-radius: 4px;
padding: 2px 7px;
}
.hbrev-type.is-content {
background: #e8f0fb;
color: #2271b1;
}
.hbrev-type.is-system {
background: #f0f0f1;
color: #72777c;
}
.hbrev-date { color: #72777c; font-size: 12px; }
.hbrev-pagination {
padding: 14px 20px;
border-top: 1px solid #f0f0f1;
text-align: right;
font-size: 12px;
color: #72777c;
}
.hbrev-pagination a { color: #2271b1; text-decoration: none; margin-left: 10px; }
.hbrev-pagination a:hover { text-decoration: underline; }
.hbrev-footer { margin-top: 16px; font-size: 12px; color: #a0a5aa; text-align: center; }
</style>
';
}
function hbrev_render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Bạn không có quyền truy cập trang này.', 'hb-revisions' ) );
}
$per_page = 50;
$current_page = isset( $_GET['hbrev_page'] ) ? max( 1, (int) $_GET['hbrev_page'] ) : 1;
$offset = ( $current_page - 1 ) * $per_page;
$total_rev = hbrev_count();
$total_posts_rev = hbrev_total_posts_with_rev();
$size_str = hbrev_format_size( hbrev_size_kb() );
$rows = hbrev_get_posts( $per_page, $offset );
$total_pages = $total_posts_rev > 0 ? (int) ceil( $total_posts_rev / $per_page ) : 1;
$done = isset( $_GET['hbrev_done'] );
hbrev_styles();
?>
<div class="wrap hbrev-wrap">
<?php if ( $done ) : ?>
<div class="hbrev-notice">
Đã dọn dẹp xong! Tất cả revisions đã được xóa khỏi database.
</div>
<?php endif; ?>
<div class="hbrev-summary">
<div class="hbrev-stat">
<span class="val"><?php echo number_format( $total_rev ); ?></span>
<span class="lbl">Tổng số Revisions</span>
</div>
<div class="hbrev-stat">
<span class="val" style="font-size:32px;"><?php echo esc_html( $size_str ); ?></span>
<span class="lbl">Tổng dung lượng Revisions</span>
</div>
<div class="hbrev-cleanup-card">
<span class="lbl">Xóa toàn bộ revisions khỏi database</span>
<?php if ( $total_rev > 0 ) : ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
onsubmit="return confirm('Bạn có chắc muốn xóa toàn bộ <?php echo number_format( $total_rev ); ?> revisions không? Hành động này không thể hoàn tác.');">
<input type="hidden" name="action" value="hbrev_cleanup">
<?php wp_nonce_field( 'hbrev_cleanup_nonce' ); ?>
<button type="submit" class="hbrev-btn-delete">🗑 Dọn dẹp Revisions</button>
</form>
<?php else : ?>
<span style="font-size:13px;color:#00a32a;font-weight:600;"> Database đang sạch</span>
<?php endif; ?>
</div>
</div>
<div class="hbrev-table-wrap">
<div class="hbrev-table-head">
<h2>📋 Danh sách mục — sắp xếp theo số Revision</h2>
<span>Trang <?php echo $current_page; ?> / <?php echo $total_pages; ?></span>
</div>
<?php if ( empty( $rows ) ) : ?>
<p style="padding:24px 20px;color:#72777c;margin:0;">Không có bài viết nào có revision.</p>
<?php else : ?>
<table class="hbrev-table">
<thead>
<tr>
<th class="col-num">#</th>
<th>Tiêu đề</th>
<th class="col-type">Loại</th>
<th class="col-rev">Revisions</th>
<th class="col-date">Sửa lần cuối</th>
</tr>
</thead>
<tbody>
<?php foreach ( $rows as $i => $row ) :
$stt = $offset + $i + 1;
$count = (int) $row['revision_count'];
$badge = $count >= 20 ? 'high' : ( $count >= 10 ? 'med' : 'low' );
$title = $row['post_title'] ?: '(Không có tiêu đề)';
$date = mysql2date( 'd/m/Y H:i', $row['post_modified'] );
$type_info = hbrev_type_label( $row['post_type'] );
$pill_class = $type_info['system'] ? 'is-system' : 'is-content';
$pill_text = $type_info['system']
? '(hệ thống) ' . $type_info['label']
: '(' . $type_info['label'] . ')';
$edit_url = $type_info['system'] ? null : get_edit_post_link( $row['ID'] );
?>
<tr>
<td class="col-num"><?php echo $stt; ?></td>
<td class="hbrev-title">
<?php if ( $edit_url ) : ?>
<a href="<?php echo esc_url( $edit_url ); ?>" target="_blank">
<?php echo esc_html( $title ); ?>
</a>
<?php else : ?>
<?php echo esc_html( $title ); ?>
<?php endif; ?>
<span class="post-id">ID: <?php echo (int) $row['ID']; ?></span>
</td>
<td>
<span class="hbrev-type <?php echo $pill_class; ?>">
<?php echo esc_html( $pill_text ); ?>
</span>
</td>
<td class="col-rev">
<span class="hbrev-badge <?php echo $badge; ?>"><?php echo $count; ?></span>
</td>
<td class="hbrev-date"><?php echo esc_html( $date ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ( $total_pages > 1 ) : ?>
<div class="hbrev-pagination">
<?php
$base = admin_url( 'admin.php?page=hb-revisions' );
if ( $current_page > 1 ) {
printf( '<a href="%s">← Trước</a>', esc_url( $base . '&hbrev_page=' . ( $current_page - 1 ) ) );
}
echo ' Trang ' . $current_page . ' / ' . $total_pages . ' ';
if ( $current_page < $total_pages ) {
printf( '<a href="%s">Tiếp →</a>', esc_url( $base . '&hbrev_page=' . ( $current_page + 1 ) ) );
}
?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<p class="hbrev-footer">HB Revisions v1.3.0 · Dữ liệu thời gian thực từ database</p>
</div>
<?php
}Bước 02: Bạn vào trong trang quản trị của web, kích hoạt plugin vừa tạo lên, plugin tên là HB Revisions ấy. Lúc này trong thanh công cụ (gần mục cài đặt website) sẽ có một mục tính năng tên là HB Revisions, bạn nhấp vào đó và xem thành quả.

Bước 03: Copy code đưa cho Claude, bảo nó thêm/ chỉnh sửa tính năng theo ý bạn rồi copy dán đè lại file php của plugin trên hosting. Còn về code mẫu ở trên chỉ là mình làm nháp thôi, gọi là thực hành tạo plugin, chạy được và đếm đúng số revisions là được chứ cũng không phải bản hoàn chỉnh gì đâu.
Tương tự như mình làm ở trên, ae có thể nhờ AI tạo cho nhiều plugin đơn giản khác để nghịch hoặc để thực thi một tính năng nào đó mà ae muốn. Mình ưa dùng Claude nhất cho mấy cái vọc này, nêu ý tưởng là nó rất hiểu ý và làm oke, ae thấy hứng thú thì quẩy thử xem sao nhen ^^
Bác Tịnh đặt tên thư mục của plugin thì nên đặt bằng chữ thường và phân tách các từ bằng dấu gạch nối (-) nhé. Ví dụ trong bài viết là “hb-revisions”. Nếu bác đặt là “HB Revisions” thì khi truy cập trên thanh địa chỉ của trình duyệt sẽ hiện “wp-content/plugins/HB%20Revisions/” trông vừa khó chịu, vừa thiếu chuyên nghiệp. Thậm chí trong một số trường hợp còn có thể gây lỗi 404. 😂
Ây dà, thiệt là thiếu sót quá mà, haha ! Cảm ơn bạn Hiếu đã chia sẻ kinh nghiệm chỗ đó. Để mình cập nhật lại cách đặt tên chuyên mục :v