Để tích hợp các bài viết WordPress thành các sản phẩm bán được qua WooCommerce, chia sẻ này sẽ hướng dẫn cách thêm bài viết vào giỏ hàng. Người dùng có thể thêm bài viết vào giỏ hàng và tiến hành thanh toán như với các sản phẩm WooCommerce khác.
Các bước tích hợp
1. Tạo sản phẩm ảo cho Bài Viết
Trước hết, cần tạo một sản phẩm “placeholder” để dùng làm mẫu cho các bài viết bán hàng:
- Sử dụng hàm
polydev_create_placeholder_product()
để kiểm tra xem sản phẩm “placeholder” đã tồn tại hay chưa. Nếu chưa có, hàm này sẽ tạo mới một sản phẩm với thuộc tính ẩn khỏi danh mục WooCommerce. - Thông tin sản phẩm ảo này sẽ là khung lưu trữ các thông thông tin và giá trị, điều này cần thiết để WooCommerce nhận diện các thành phần liên quan tới sản phẩm trong quá trình thêm vào giỏ hàng. Theo mình đây là cách đơn giản, hiệu quả để hạn chế hook vào core của WooCommerce quá nhiều, đặc biệt khó kiểm soát khi Woo update về sau. Trường hợp bạn có hướng nào tích hợp thì chia sẻ thêm nhé!
2. Hàm thêm bài viết vào giỏ hàng
Sử dụng polydev_add_code_to_cart_with_placeholder_product()
để:
- Lấy giá trị
sale_price
từ thông tin meta của bài viết. Thông tin này dùng ACF hay tự code metabox thì tùy vào nhu cầu. Đây là giá bán khi thêm vào giỏ hàng. - Cơ chế hàm lưu trữ một số biến custom để mang giá trị vào xử lý trong các hàm hook khác như: custom_type, custom_price,post_id. Nếu cần tích hợp cho các custom post type khác thì có thể tùy biến ở đây.
Ví dụ mình tích hợp thêm shortcode, block. Sau đó chèn vào bất cứ nội dung bài viết nào, như phần hiển thị Add code to cart sau. Bạn có thể thử Add code to cart để xem quá trình bài viết thêm vào giỏ hàng có giống sản phẩm không nhé!
- Lifetime support
- Future updates
- Free installation
- Paid customization
3. Hiển thị thông tin bài viết trong giỏ hàng và đơn hàng
Để thông bài viết hiển thị đúng trong giỏ hàng và đơn hàng, chúng ta cần xử lý thêm dựa trên các biến số đã lưu. Các hàm này sẽ thay thế thông tin của sản phẩm ảo mặc định.
- Tên bài viết và giá bán: Dùng
polydev_update_cart_item_data_with_post_info()
để cập nhật tên và giá bán trong giỏ hàng. - Ảnh đại diện bài viết: Dùng
polydev_custom_cart_item_thumbnail()
để hiển thị ảnh đại diện của bài viết thay vì ảnh sản phẩm mặc định. Trường hợp dùng ảnh mặc định thì không cần hook hàm này. - Liên kết bài viết: Thay thế liên kết sản phẩm ảo với liên kết bài viết bằng
polydev_custom_cart_item_permalink()
.
4. Hiển thị thông tin đơn hàng trong admin
Trong đơn hàng, WooCommerce sẽ lưu trữ và hiển thị thông tin bài viết:
- Lưu thông tin meta bài viết: Dùng
polydev_save_post_data_to_order_item()
để lưu ID bài viết và loạipost
cho từng sản phẩm trong đơn hàng. - Hiển thị danh mục bài viết: Tùy chỉnh thông tin đơn hàng để hiển thị danh mục bài viết trong WooCommerce Admin bằng
polydev_modify_order_item_meta_data()
.
5. Thêm tùy chỉnh hướng dẫn BACS cho đơn hàng
Dùng custom_bacs_instructions_with_order_id()
để hiển thị hướng dẫn chuyển khoản khi đặt hàng hoàn tất, lưu ý khách hàng ghi chú mã đơn hàng (Order ID) trong nội dung thanh toán.
Phần này mặc định WooCommerce đã hỗ trợ. Tuy nhiên, nếu bạn muốn bổ sung thông tin liên quan tới đơn hàng như OrderID thì có thể tùy biến thêm tại đây. Để mở rộng tính năng, hoàn thiện hơn phần này, bạn có thể viết thêm vào phần cài đặt khi viết plugin để tiện quản lý.
6. Thêm nút DOWNLOAD cho nội dung Premium
Nếu bài viết chứa nội dung premium, dùng hàm này để xử lý. Ví dụ như bạn thấy trên blog này một số mã code mình chia sẻ, có thể có code nâng cao và đầy đủ hơn thì khách hàng có thể đặt mua trước khi có thể tải về. Hàm này ví dụ về sẽ nén nội dung premium để người dùng tải về. Bạn có thể tích hợp thêm upload file zip, doc, media,… hay đại loại những gì bạn muốn bán lên.
- Dùng
polydev_add_download_code_button_to_order()
để thêm nút tải xuống cho bài viết khi đơn hàng đã hoàn tất. - Nội dung premium từ bài viết sẽ được tải xuống dưới dạng file khi người dùng nhấn vào nút “Download code”.
Ở phần này để nên tích hợp thêm các tính năng nén có mật khẩu hoặc kết nối API Google Drive, OneDrive hay thông tin truy cập vào các thư mục premium của bạn.
7. Một số tùy chỉnh khácTùy Chỉnh Thông Báo và Thông Tin Giỏ Hàng
Cuối cùng, đảm bảo tính thống nhất về thông tin các bài viết ở các tính năng như xóa bài viết, thêm bài viết đã có trong giỏ hàng thì bạn cần hook các hàm sau. Nếu không xử lý phần này thì các thông tin WooCommerce lấy từ sản phẩm ảo sẽ khiến trải nghiệm người dùng không được tốt.
- Thông báo lỗi: Thay đổi thông báo khi người dùng thêm bài viết trùng vào giỏ hàng bằng
polydev_custom_modify_cart_error_message()
. - Thông báo xóa sản phẩm: Xóa thông báo mặc định của WooCommerce khi sản phẩm “placeholder” bị xóa và thay bằng tên bài viết thực qua
polydev_remove_cart_notices()
vàpolydev_custom_cart_item_removed()
.
Ưu nhược điểm
Việc tích hợp này giúp biến các bài viết WordPress thành sản phẩm bán trực tiếp thông qua WooCommerce mà không phải tạo sản phẩm riêng. Người dùng có thể dễ dàng thêm bài viết vào giỏ hàng, thanh toán và tải nội dung premium. Ngoài ra, bạn cũng hoàn toàn có thể sử dụng song song tính năng sản phẩm mặc định để bán trên site. Việc hook đã xử lý độc lập nên giỏ hàng Woo vẫn chấp nhận khách hàng mua sản phẩm, bài viết hay bất kỳ custom post type nào bạn tích hợp thêm.
Có một nhược điểm ở cách này là việc xem báo cáo, thống kê đơn hàng sẽ không thể hiện theo post mà chỉ ghi nhận duy nhất doanh số của sản phẩm ảo. Tuy nhiên, nếu muốn bạn hoàn toàn có thể mở rộng để hoàn thiện bằng cách hook thêm vào các thành phần này trên WooCommerce nhé!
define('POLYDEV_PLACEHOLDER_PRODUCT_TITLE', 'PolyDev Product Template'); function polydev_create_placeholder_product() { $query = new WP_Query([ 'title' => POLYDEV_PLACEHOLDER_PRODUCT_TITLE, 'post_type' => 'product', 'post_status' => 'any', 'posts_per_page'=> 1 ]); if ($query->have_posts()) { $existing_product = $query->post; wp_reset_postdata(); return $existing_product->ID; } $product = new WC_Product(); $product->set_name(POLYDEV_PLACEHOLDER_PRODUCT_TITLE); $product->set_status('private'); $product->set_catalog_visibility('hidden'); $product->set_price(0); $product->set_regular_price(0); $product->set_sold_individually(true); $product->save(); return $product->get_id(); } function polydev_add_code_to_cart_with_placeholder_product() { if (isset($_GET['add_code_to_cart']) && isset($_GET['post_id'])) { $post_id = intval($_GET['post_id']); $preview_settings = get_post_meta($post_id, '_polydev_preview_settings', true); $sale_price = isset($preview_settings['sale_price']) ? floatval($preview_settings['sale_price']) : 0; if ($sale_price > 0) { $placeholder_product_id = polydev_create_placeholder_product(); WC()->cart->add_to_cart( $placeholder_product_id, 1, '', '', array( 'custom_type' => 'post', 'post_id' => $post_id, 'custom_price' => $sale_price ) ); wp_redirect(wc_get_cart_url()); exit(); } } } add_action('template_redirect', 'polydev_add_code_to_cart_with_placeholder_product'); function polydev_handle_order_again_custom_items($cart_item_data, $item, $order) { $post_id = $item->get_meta('post_id'); if ($post_id !== null) { $preview_settings = get_post_meta($post_id, '_polydev_preview_settings', true); $sale_price = isset($preview_settings['sale_price']) ? floatval($preview_settings['sale_price']) : 0; if ($sale_price > 0 && $post_id) { WC()->cart->empty_cart(); $placeholder_product_id = polydev_create_placeholder_product(); WC()->cart->add_to_cart( $placeholder_product_id, 1, '', '', array( 'custom_type' => 'post', 'post_id' => $post_id, 'custom_price' => $sale_price ) ); wp_redirect(wc_get_cart_url()); exit(); } } } add_filter('woocommerce_order_again_cart_item_data', 'polydev_handle_order_again_custom_items', 10, 3); function custom_bacs_instructions_with_order_id($order_id) { $order = wc_get_order($order_id); if ($order && $order->get_payment_method() === 'bacs') { echo '<h2>' . __('Payment Instructions', 'polydev') . '</h2>'; echo '<p>' . sprintf( __('Please make the payment to our bank account and use Order ID <strong>#%s</strong> as the payment reference. Once we receive your payment, the download link for the product will be updated in the order details.', 'polydev'), $order_id ) . '</p>'; } } add_action('woocommerce_thankyou_bacs', 'custom_bacs_instructions_with_order_id', 10, 1); function polydev_add_cart_item_data($cart_item_data, $product_id) { if (isset($_GET['add_code_to_cart']) && isset($_GET['post_id'])) { $post_id = intval($_GET['post_id']); $cart_item_data['custom_type'] = 'post'; $cart_item_data['post_id'] = $post_id; $cart_item_data['unique_key'] = md5($product_id . '_' . $post_id); } return $cart_item_data; } add_filter('woocommerce_add_cart_item_data', 'polydev_add_cart_item_data', 10, 2); function polydev_save_post_data_to_order_item($item_id, $values, $cart_item_key) { if (isset($values['custom_type']) && $values['custom_type'] === 'post') { wc_add_order_item_meta($item_id, 'custom_type', 'post'); wc_add_order_item_meta($item_id, 'post_id', $values['post_id']); } } add_action('woocommerce_add_order_item_meta', 'polydev_save_post_data_to_order_item', 10, 3); function polydev_update_cart_item_data_with_post_info($cart_object) { foreach ($cart_object->get_cart() as $cart_item) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post') { $post_id = $cart_item['post_id']; $custom_price = floatval($cart_item['custom_price']); $cart_item['data']->set_name(get_the_title($post_id)); $cart_item['data']->set_price($custom_price); } } } add_action('woocommerce_before_calculate_totals', 'polydev_update_cart_item_data_with_post_info', 10, 1); function polydev_custom_cart_item_name_display($name, $cart_item, $cart_item_key) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post') { $post_id = $cart_item['post_id']; $name = get_the_title($post_id); } return $name; } add_filter('woocommerce_cart_item_name', 'polydev_custom_cart_item_name_display', 10, 3); function polydev_custom_cart_item_price_display($price, $cart_item, $cart_item_key) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post') { $price = wc_price($cart_item['custom_price']); } return $price; } add_filter('woocommerce_cart_item_price', 'polydev_custom_cart_item_price_display', 10, 3); function polydev_custom_cart_item_permalink($url, $cart_item, $cart_item_key) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post') { $post_id = $cart_item['post_id']; $url = get_permalink($post_id); } return $url; } add_filter('woocommerce_cart_item_permalink', 'polydev_custom_cart_item_permalink', 10, 3); function polydev_custom_order_item_permalink($url, $item, $order) { if (isset($item['custom_type']) && $item['custom_type'] === 'post') { $post_id = $item['post_id']; $url = get_permalink($post_id); } return $url; } add_filter('woocommerce_order_item_permalink', 'polydev_custom_order_item_permalink', 10, 3); function polydev_custom_admin_order_item_permalink($url, $item, $order) { if (isset($item['custom_type']) && $item['custom_type'] === 'post') { $post_id = $item['post_id']; $url = get_permalink($post_id); } return $url; } add_filter('woocommerce_admin_order_item_permalink', 'polydev_custom_admin_order_item_permalink', 10, 3); function polydev_modify_order_item_meta_data($formatted_meta, $item) { $custom_type = wc_get_order_item_meta($item->get_id(), 'custom_type', true); $post_id = wc_get_order_item_meta($item->get_id(), 'post_id', true); if ($custom_type === 'post' && $post_id) { $polydev_exists = false; foreach ($formatted_meta as $key => $meta) { if ($meta->key === 'polydev') { $polydev_exists = true; } if ($meta->key === 'custom_type') { unset($formatted_meta[$key]); } if ($meta->key === 'post_id') { $categories = get_the_terms($post_id, 'category'); if ($categories && !is_wp_error($categories)) { $category_links = array_map(function ($cat) { return '<a href="' . esc_url(get_term_link($cat)) . '" target="_blank">' . esc_html($cat->name) . '</a>'; }, $categories); $category_list = implode(', ', $category_links); $formatted_meta[$key]->display_key = 'Category'; $formatted_meta[$key]->display_value = $category_list; } else { $formatted_meta[$key]->display_key = 'Category'; $formatted_meta[$key]->display_value = 'No category assigned'; } } } } return $formatted_meta; } add_filter('woocommerce_order_item_get_formatted_meta_data', 'polydev_modify_order_item_meta_data', 10, 2); function polydev_custom_admin_order_item_link($item_id, $item, $order) { $post_id = wc_get_order_item_meta($item_id, 'post_id', true); $custom_type = wc_get_order_item_meta($item_id, 'custom_type', true); if ($custom_type === 'post' && $post_id) { echo '<div class="polydev-custom-order-item">'; $post_title = get_the_title($post_id); $post_url = get_permalink($post_id); $thumbnail = get_the_post_thumbnail($post_id, 'thumbnail'); if ($thumbnail) { echo '<div class="polydev-woocommerce-order-item-thumbnail">' . $thumbnail . '</div>'; } echo '<a href="' . esc_url($post_url) . '" target="_blank">' . esc_html($post_title) . '</a>'; echo '<a href="' . esc_url(admin_url('post.php?post=' . $post_id . '&action=edit')) . '" target="_blank"><i class="dashicons dashicons-edit"></i></a>'; echo '<style>.wc-order-item-name { display: none; }.thumb { display: none!important; }</style>'; } } add_action('woocommerce_before_order_itemmeta', 'polydev_custom_admin_order_item_link', 10, 3); function polydev_wrap_order_item_end($item_id, $item, $order) { echo '</div>'; } add_action('woocommerce_after_order_itemmeta', 'polydev_wrap_order_item_end', 10, 3); function polydev_custom_modify_cart_error_message($error) { if (strpos($error, POLYDEV_PLACEHOLDER_PRODUCT_TITLE) !== false && isset($_GET['post_id'])) { $current_post_id = intval($_GET['post_id']); foreach (WC()->cart->get_cart() as $cart_item) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post' && $cart_item['post_id'] == $current_post_id) { $post_title = get_the_title($current_post_id); $error = sprintf(__('You cannot add another "%s" to your cart.', 'polydev'), $post_title); break; } } } return $error; } add_filter('woocommerce_add_error', 'polydev_custom_modify_cart_error_message'); function polydev_remove_cart_notices() { if (is_cart()) { ?> <script type="text/javascript"> function removePolyDevNotices() { const messages = document.querySelectorAll('.woocommerce-message'); messages.forEach(message => { if (message.innerText.includes('<?php echo POLYDEV_PLACEHOLDER_PRODUCT_TITLE?>')) { message.remove(); } }); } document.addEventListener('DOMContentLoaded', function() { removePolyDevNotices(); jQuery(document.body).on('updated_wc_div', function() { removePolyDevNotices(); }); }); </script> <?php } } add_action('wp_footer', 'polydev_remove_cart_notices'); function polydev_custom_cart_item_removed($cart_item_key, $cart) { if (isset($cart->removed_cart_contents[$cart_item_key]['custom_type']) && $cart->removed_cart_contents[$cart_item_key]['custom_type'] === 'post') { $post_id = $cart->removed_cart_contents[$cart_item_key]['post_id']; $post_title = get_the_title($post_id); wc_add_notice(sprintf(__('"%s" has been removed from your cart.', 'polydev'), $post_title), 'success'); } } add_action('woocommerce_cart_item_removed', 'polydev_custom_cart_item_removed', 10, 2); function polydev_custom_cart_item_thumbnail($thumbnail, $cart_item, $cart_item_key) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post') { $post_id = $cart_item['post_id']; $post_thumbnail = get_the_post_thumbnail($post_id, 'thumbnail'); if ($post_thumbnail) { $thumbnail = $post_thumbnail; } } return $thumbnail; } add_filter('woocommerce_cart_item_thumbnail', 'polydev_custom_cart_item_thumbnail', 10, 3); function polydev_custom_cart_item_name($name, $cart_item, $cart_item_key) { if (isset($cart_item['custom_type']) && $cart_item['custom_type'] === 'post') { $post_id = $cart_item['post_id']; $post_title = get_the_title($post_id); $post_url = get_permalink($post_id); $name = '<a href="' . esc_url($post_url) . '" target="_blank">' . esc_html($post_title) . '</a>'; } return $name; } add_filter('woocommerce_cart_item_name', 'polydev_custom_cart_item_name', 10, 3); function polydev_add_download_code_button_to_order($item_id, $item, $order) { if ($order->has_status('completed')) { $post_id = $item->get_meta('post_id'); if (!empty($post_id)) { $code_blocks = get_post_meta($post_id, '_polydev_code_blocks', true); $has_premium_content = false; if (!empty($code_blocks) && is_array($code_blocks)) { foreach ($code_blocks as $block) { if (isset($block['is_premium']) && $block['is_premium'] === 1) { $has_premium_content = true; break; } } } if ($has_premium_content) { $download_url = add_query_arg(array( 'polydev_code' => '1', 'post_id' => $post_id, 'order_id' => $order->get_id() ), home_url()); echo '<p><a href="' . esc_url($download_url) . '" class="button">Download code</a></p>'; } } } } add_action('woocommerce_order_item_meta_end', 'polydev_add_download_code_button_to_order', 10, 3); function handle_polydev_code_download() { if (!isset($_GET['polydev_code']) || !isset($_GET['post_id']) || !isset($_GET['order_id'])) { return; } $order_id = intval($_GET['order_id']); $post_id = intval($_GET['post_id']); $order = wc_get_order($order_id); if (!$order || $order->get_user_id() !== get_current_user_id() || !$order->has_status('completed')) { wp_safe_redirect(wc_get_account_endpoint_url('orders')); exit; } $code_blocks = get_post_meta($post_id, '_polydev_code_blocks', true); $premium_code_content = array(); $language_extensions = array( 'htmlmixed' => 'html', 'js' => 'js', 'css' => 'css', 'python' => 'py', 'csharp' => 'cs' ); if (!empty($code_blocks) && is_array($code_blocks)) { foreach ($code_blocks as $block) { if (isset($block['is_premium']) && $block['is_premium'] === 1) { $extension = isset($language_extensions[$block['language']]) ? $language_extensions[$block['language']] : 'txt'; $premium_code_content[] = array( 'content' => $block['content'], 'extension' => $extension ); } } } if (!empty($premium_code_content)) { $user_name = Helper::get_current_username() ?? ''; $subfix_file_name = '_thank_you' . ($user_name ? '_' . $user_name : ''); if (count($premium_code_content) === 1) { $content = $premium_code_content[0]['content']; $extension = $premium_code_content[0]['extension']; $post_title = get_the_title($post_id); $file_name_slug = sanitize_title($post_title); header('Content-Type: text/plain'); header('Content-Disposition: attachment; filename="' . $file_name_slug . $subfix_file_name . '.' . $extension . '"'); echo $content; exit; } else { $zip = new ZipArchive(); $zip_file = tempnam(sys_get_temp_dir(), 'polydev_code_') . '.zip'; if ($zip->open($zip_file, ZipArchive::CREATE) === TRUE) { foreach ($premium_code_content as $index => $block) { $content = $block['content']; $extension = $block['extension']; $zip->addFromString("code_block_" . ($index + 1) . "." . $extension, $content); } $zip->close(); $post_title = get_the_title($post_id); $file_name_slug = sanitize_title($post_title); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . $file_name_slug . $subfix_file_name . '.zip"'); readfile($zip_file); unlink($zip_file); exit; } } } wp_safe_redirect(wc_get_account_endpoint_url('orders')); exit; } add_action('template_redirect', 'handle_polydev_code_download');