Tích hợp Bài Viết WordPress Thành Sản Phẩm Bán với WooCommerce

Việc sử dụng bài viết như một sản phẩm để thêm vào giỏ hàng và bán hàng có thể không phổ biến trên các trang thương mại điện tử, nhưng lại là một giải pháp đáng cân nhắc, đặc biệt khi xem xét về mặt SEO. Các bài viết thường có khả năng SEO tốt hơn so với trang sản phẩm truyền thống, điều này mang lại nhiều lợi ích tiềm năng cho chiến lược tiếp thị nội dung.
Tại sao lại như vậy? Google có hệ sinh thái quảng cáo riêng và nguồn doanh thu lớn từ đó, vì vậy SEO sản phẩm trực tiếp có thể ảnh hưởng đến doanh thu quảng cáo của họ. Các thuật toán tìm kiếm có thể được điều chỉnh để cân đối nhằm tối ưu hóa doanh thu từ quảng cáo SEM. Với góc nhìn cá nhân, mình cho rằng điều này phần nào giải thích lý do tại sao nhiều người làm tiếp thị liên kết, đặc biệt là cho các nền tảng như Amazon hay AliExpress, thường chọn cách tiếp cận SEO thông qua các bài viết review và hướng dẫn.

Để 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é!

Recently Updated
Included
  • Lifetime support
  • Future updates
  • Free installation
  • Paid customization
Add code to cart$8.99$13.99
Last Update
Published

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ại post 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()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');