
Bạn đã bao giờ mơ ước đăng hàng trăm bài viết lên WordPress chỉ trong vài phút thay vì nhập tay từng bài? Tính năng chèn dữ liệu bài post số lượng lớn tự động chính là giải pháp giúp bạn tiết kiệm thời gian và công sức. Trong bài blog này, mình sẽ chia sẻ cách thực hiện điều đó một cách đơn giản và hiệu quả.
Hướng Dẫn Nhanh
- Tạo hàm khái quát để tái sử dụng
function ws_register_generic_import_handler($action, $config)
{
add_action("wp_ajax_{$action}", function () use ($config) {
ws_generic_import_handler($config);
});
}
function ws_generic_import_handler($config)
{
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Permission denied']);
wp_die();
}
$defaults = [
'path_dir' => plugin_dir_path(__FILE__) . '../data',
'post_type' => 'post',
'meta_fields' => [],
'default_meta_fields' => [],
'transient_key' => 'generic_import_current',
'content_callback' => 'ws_generic_insert_content',
'excerpt_key' => 'excerpt',
];
$config = wp_parse_args($config, $defaults);
$data_dir = rtrim($config['path_dir'], '/');
if (!is_dir($data_dir)) {
wp_send_json_error(['message' => 'Data directory not found: ' . $data_dir]);
wp_die();
}
$items = array_filter(glob($data_dir . '/*'), 'is_dir');
$total = count($items);
if ($total === 0) {
wp_send_json_error(['message' => 'No items found to import']);
wp_die();
}
$current = get_transient($config['transient_key']) ?: 0;
if ($current >= $total) {
delete_transient($config['transient_key']);
wp_send_json_success([
'progress' => 100,
'message' => 'All items imported',
'complete' => true,
]);
wp_die();
}
$item_dir = $items[$current];
$item_name = basename($item_dir);
$info_file = $item_dir . '/info.txt';
$meta_values = [];
if (file_exists($info_file)) {
$lines = file($info_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines !== false) {
$current_key = '';
$all_meta_fields = array_merge($config['default_meta_fields'], $config['meta_fields']);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line))
continue;
if (preg_match('/^([a-zA-Z0-9_]+):\s*\|\-$/', $line, $matches)) {
$prefix = $matches[1];
foreach ($all_meta_fields as $key => $meta_config) {
$mapped_prefix = is_array($meta_config) ? $meta_config['prefix'] : $meta_config;
if ($mapped_prefix === $prefix || $key === $prefix) {
$current_key = $key;
$meta_values[$key] = '';
break;
}
}
} elseif ($current_key && strpos($line, ':') === false) {
$meta_values[$current_key] .= ($meta_values[$current_key] ? "\n" : '') . trim($line);
} elseif (strpos($line, ':') !== false && !preg_match('/\|\-$/', $line)) {
[$prefix, $value] = array_map('trim', explode(':', $line, 2));
foreach ($all_meta_fields as $key => $meta_config) {
$mapped_prefix = is_array($meta_config) ? $meta_config['prefix'] : $meta_config;
if ($mapped_prefix === $prefix || $key === $prefix) {
$meta_values[$key] = $value;
$current_key = $key;
break;
}
}
}
}
error_log("Meta values from $info_file: " . print_r($meta_values, true));
} else {
error_log("Failed to read info.txt at: $info_file");
}
} else {
error_log("info.txt not found at: $info_file");
}
foreach ($config['default_meta_fields'] as $key => $meta_config) {
if (!isset($meta_values[$key]) && isset($meta_config['value'])) {
$meta_values[$key] = $meta_config['value'];
}
}
$all_images = glob($item_dir . '/*.{jpg,png,gif,webp}', GLOB_BRACE);
$thumbnail_id = !empty($all_images) ? ws_import_image_to_media($all_images[0]) : 0;
$excerpt_key = array_key_exists('excerpt', $config['meta_fields']) ? 'excerpt' : $config['excerpt_key'];
$content = call_user_func(
$config['content_callback'],
$all_images,
$item_name,
$all_images[0] ?? '',
$meta_values[$excerpt_key] ?? ''
);
$post_data = [
'post_title' => $item_name,
'post_content' => $content,
'post_excerpt' => $meta_values[$excerpt_key] ?? '',
'post_status' => 'publish',
'post_type' => $config['post_type'],
];
$post_id = wp_insert_post($post_data, true);
if (is_wp_error($post_id)) {
wp_send_json_error(['message' => 'Failed to insert post: ' . $post_id->get_error_message()]);
wp_die();
} else {
if ($thumbnail_id) {
set_post_thumbnail($post_id, $thumbnail_id);
update_post_meta($post_id, 'thumbnail_id', $thumbnail_id);
}
$all_meta_fields = array_merge($config['default_meta_fields'], $config['meta_fields']);
foreach ($all_meta_fields as $key => $meta_config) {
if (isset($meta_values[$key])) {
update_post_meta($post_id, $key, $meta_values[$key]);
}
}
}
$current++;
set_transient($config['transient_key'], $current, HOUR_IN_SECONDS);
$progress = round(($current / $total) * 100);
$message = "Imported: $item_name (Item $current of $total)";
wp_send_json_success([
'progress' => $progress,
'message' => $message,
'complete' => ($current >= $total),
]);
wp_die();
}
function ws_generic_insert_content($all_images, $item_name, $thumbnail_path, $excerpt = '')
{
$content = !empty($excerpt) ? '<p>' . esc_html($excerpt) . '</p>' : '';
if (!empty($all_images)) {
foreach ($all_images as $index => $image_path) {
if ($index === 0)
continue;
$image_id = ws_import_image_to_media($image_path);
if ($image_id) {
$image_url = wp_get_attachment_url($image_id);
$content .= '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($item_name) . ' detail" style="max-width: 100%; height: auto;" /><br />';
}
}
}
return $content;
}
function ws_import_image_to_media($file_path)
{
$webp_file = ws_image_to_webp($file_path, 100);
$file_to_upload = $webp_file !== false ? $webp_file : $file_path;
$wp_upload_dir = wp_upload_dir();
$filename = basename($file_to_upload);
if ($webp_file !== false) {
$filename = pathinfo($file_path, PATHINFO_FILENAME) . '.webp';
}
$new_file = $wp_upload_dir['path'] . '/' . $filename;
if (!file_exists($new_file)) {
if (!copy($file_to_upload, $new_file)) {
error_log("Failed to copy file: $file_to_upload to $new_file");
return 0;
}
}
$filetype = wp_check_filetype($filename, null);
$attachment = [
'guid' => $wp_upload_dir['url'] . '/' . $filename,
'post_mime_type' => $filetype['type'],
'post_title' => sanitize_file_name($filename),
'post_content' => '',
'post_status' => 'inherit',
];
$attach_id = wp_insert_attachment($attachment, $new_file);
if ($attach_id && !is_wp_error($attach_id)) {
require_once ABSPATH . 'wp-admin/includes/image.php';
$attach_data = [
'file' => $new_file,
'width' => 0,
'height' => 0,
'sizes' => [],
];
wp_update_attachment_metadata($attach_id, $attach_data);
if ($webp_file !== false && $webp_file !== $file_path && file_exists($webp_file)) {
unlink($webp_file);
}
return $attach_id;
}
if ($webp_file !== false && $webp_file !== $file_path && file_exists($webp_file)) {
unlink($webp_file);
}
return 0;
}
function ws_image_to_webp($file, $compression_quality = 100)
{
if (!file_exists($file))
return false;
$file_type = exif_imagetype($file);
$output_file = $file . '.webp';
if (file_exists($output_file))
return $output_file;
if (function_exists('imagewebp')) {
switch ($file_type) {
case IMAGETYPE_GIF:
$image = imagecreatefromgif($file);
break;
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($file);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($file);
imagepalettetotruecolor($image);
imagealphablending($image, true);
imagesavealpha($image, true);
break;
case IMAGETYPE_BMP:
$image = imagecreatefrombmp($file);
break;
case IMAGETYPE_XBM:
$image = imagecreatefromxbm($file);
break;
default:
return false;
}
$result = imagewebp($image, $output_file, $compression_quality);
if ($result === false)
return false;
imagedestroy($image);
return $output_file;
} elseif (class_exists('Imagick')) {
$image = new Imagick();
$image->readImage($file);
if ($file_type === IMAGETYPE_PNG) {
$image->setImageFormat('webp');
$image->setImageCompressionQuality($compression_quality);
$image->setOption('webp:lossless', 'true');
}
$image->writeImage($output_file);
return $output_file;
}
return false;
}
- Sau đây tôi sẽ tiến hành chèn dữ liệu vào post có type ws_device có đường dẫn data/device
ws_register_generic_import_handler('ws_action_import_data', [
'path_dir' => plugin_dir_path(__FILE__) . '../data/device',
'post_type' => 'ws_device',
'transient_key' => 'ws_data_import_current',
'meta_fields' => [
'device_price' => 'price',
'device_excerpt' => 'description',
],
'default_meta_fields' => [
'device_type' => [
'prefix' => 'device_type',
'value' => 'teambuilding',
],
],
'excerpt_key' => 'device_excerpt',
]);
- Tiếp theo sẽ tạo 1 trang trong admin để quản lí việc chèn dữ liệu
add_action('admin_menu', 'ws_import_menu');
function ws_import_menu()
{
add_menu_page(
'Import WS Devices',
'Import Data',
'manage_options',
'ws_action_import_data',
'ws_import_page',
'dashicons-upload',
20
);
}
function ws_import_page()
{
?>
<div class="wrap">
<h1>Import WS Data</h1>
<button id="start-import" class="button button-primary">Start Import</button>
<div id="import-progress" style="margin-top: 20px;">
<p>Progress: <span id="progress-text">0%</span></p>
<progress id="progress-bar" value="0" max="100"></progress>
<div id="import-log"></div>
</div>
</div>
<script>
jQuery(document).ready(function ($) {
$('#start-import').on('click', function () {
$(this).prop('disabled', true);
startImport();
});
function startImport() {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'ws_action_import_data',
start: 1
},
success: function (response) {
if (response.success) {
updateProgress(response.data);
if (response.data.complete) {
$('#progress-text').text('100%');
$('#progress-bar').val(100);
$('#import-log').append('<p>Import completed!</p>');
} else {
startImport();
}
} else {
$('#import-log').append('<p>Error: ' + response.data.message + '</p>');
}
},
error: function () {
$('#import-log').append('<p>AJAX error occurred.</p>');
}
});
}
function updateProgress(data) {
$('#progress-text').text(data.progress + '%');
$('#progress-bar').val(data.progress);
$('#import-log').append('<p>' + data.message + '</p>');
}
});
</script>
<?php
}
- Sau đó refesh lại page thì ta thấy trong admin sẽ có trang import

- Cấu trúc folder data import có dạng data/device/folder-name-device
- Trong folder-name-device/info.txt chứa thông tin device có dạng và folder-name-device/name-image.[jpg,png,gif,webp]
- Bên dưới là cấu trúc file info.txt
price: 250000
description: -Đơn vị tính: Bộ -Dài 80cm - Cao 70cm