تاکسونومی سفارشی، یکی از آن ابزارهایی است که وقتی درست استفاده شود، ساختار اطلاعاتی سایت را از هم‌ریختگی نجات می‌دهد و وقتی اشتباه به‌کار برود، به منبع سردرگمی تبدیل می‌شود. در سال‌ها کار با وردپرس، دیده‌ام پروژه‌هایی که همهٔ دسته‌بندی‌ها را در «دسته‌ها»ی پیش‌فرض جمع می‌کنند، در ماه سوم به پنل مدیریتی آشفته و queryهای سنگین می‌رسند. این مقاله، کار حرفه‌ای با تاکسونومی سفارشی را مرور می‌کند: از ثبت و برچسب‌های فارسی تا term meta، کوئری سفارشی، نمایش در قالب و ساختار کلاس‌محور. برای درک پیش‌نیازها، افزونهٔ وردپرس چیست، هوک‌های وردپرس، ساخت CPT، و ساخت تاکسونومی سفارشی را پیش از ادامه ببینید.

تاکسونومی سفارشی: بازنگری کوتاه

تاکسونومی، مکانیزم وردپرس برای دسته‌بندی محتواست. وردپرس چهار تاکسونومی پیش‌فرض دارد: دسته (category) برای نوشته‌ها با ساختار سلسله‌مراتبی، برچسب (post_tag) برای نوشته‌ها با ساختار ساده، post_format برای قالب‌های نوشته، و link_category برای پیوندها. تاکسونومی سفارشی، دسته‌بندی جدیدی است که می‌سازید و به هر نوع محتوایی (نوشته، برگه، CPT) وصل می‌کنید. مثال‌های کاربردی: «برند» برای محصولات، «ژانر» برای کتاب‌ها، «منطقه» برای نمونه‌کارها، «نویسندهٔ مهمان» برای مقالات، «سطح» برای دوره‌های آموزشی. تفاوت تاکسونومی سفارشی با فیلد سفارشی را در ساخت فیلدهای سفارشی و تفاوت با دستهٔ پیش‌فرض را در توابع دسته‌بندی دیده‌ام.

تاکسونومی، اسکلت اطلاعاتی سایت شماست. اگر دسته‌های پیش‌فرض کافی نباشند، نبود تاکسونومی سفارشی، هزینه‌اش را در سه ماه بعد می‌پردازید.

ثبت تاکسونومی با پارامترهای حرفه‌ای

اسکلت حرفه‌ای ثبت تاکسونومی سفارشی:

function my_plugin_register_brand_taxonomy() {
    $labels = array(
        'name'              => 'برندها',
        'singular_name'     => 'برند',
        'search_items'      => 'جستجوی برندها',
        'all_items'         => 'همهٔ برندها',
        'parent_item'       => 'برند والد',
        'parent_item_colon' => 'برند والد:',
        'edit_item'         => 'ویرایش برند',
        'update_item'       => 'به‌روزرسانی برند',
        'add_new_item'      => 'افزودن برند جدید',
        'new_item_name'     => 'نام برند جدید',
        'menu_name'         => 'برندها',
    );

    $args = array(
        'labels'            => $labels,
        'hierarchical'      => true,
        'public'            => true,
        'show_ui'           => true,
        'show_admin_column' => true,
        'show_in_rest'      => true,
        'query_var'         => true,
        'rewrite'           => array(
            'slug'         => 'brand',
            'with_front'   => false,
            'hierarchical' => true,
        ),
        'capabilities'      => array(
            'manage_terms' => 'manage_categories',
            'edit_terms'   => 'manage_categories',
            'delete_terms' => 'manage_categories',
            'assign_terms' => 'edit_posts',
        ),
    );

    register_taxonomy( 'brand', array( 'product' ), $args );
}
add_action( 'init', 'my_plugin_register_brand_taxonomy', 0 );

register_taxonomy_for_object_type( 'brand', 'accessory' );

نکات کلیدی: یک — اولویت 0 در init: اگر تاکسونومی به CPT سفارشی وصل است، باید CPT قبل از تاکسونومی ثبت شده باشد. دو — پارامتر دوم آرایه‌ای: می‌توانید تاکسونومی را به چند نوع محتوا وصل کنید. سه — register_taxonomy_for_object_type: اگر CPT دیگری بعداً اضافه شد، با این تابع می‌توانید تاکسونومی را به آن وصل کنید. راهنمای هوک init در هوک‌های وردپرس.

برچسب‌های فارسی و ترجمه‌پذیری

برچسب‌ها، تجربهٔ مدیریت را برای کاربر غیرفنی می‌سازند. برای پروژه‌های فارسی، همهٔ برچسب‌ها باید فارسی و قابل ترجمه باشند:

$labels = array(
    'name'              => _x( 'برندها', 'Taxonomy general name', 'my-plugin' ),
    'singular_name'     => _x( 'برند', 'Taxonomy singular name', 'my-plugin' ),
    'search_items'      => __( 'جستجوی برندها', 'my-plugin' ),
    'all_items'         => __( 'همهٔ برندها', 'my-plugin' ),
    'edit_item'         => __( 'ویرایش برند', 'my-plugin' ),
    'update_item'       => __( 'به‌روزرسانی برند', 'my-plugin' ),
    'add_new_item'      => __( 'افزودن برند جدید', 'my-plugin' ),
    'new_item_name'     => __( 'نام برند جدید', 'my-plugin' ),
    'menu_name'         => __( 'برندها', 'my-plugin' ),
    'not_found'         => __( 'برندی یافت نشد', 'my-plugin' ),
    'no_terms'          => __( 'برندی وجود ندارد', 'my-plugin' ),
);

نکته: در تاکسونومی‌های سلسله‌مراتبی، برچسب parent_item را هم تعریف کنید. راهنمای ترجمه‌پذیری در آماده‌سازی قالب برای فارسی. تهیهٔ فایل .pot با WP-CLI:

wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin

انتخاب hierarchical درست

پارامتر hierarchical تفاوت اساسی در UX و ساختار داده می‌سازد: hierarchical = true: تاکسونومی مثل «دسته» عمل می‌کند — دارای ساختار والد/فرزند، رابط چک‌باکسی، و امکان تودرتویی. مناسب برای: «دسته‌بندی محصول»، «منطقه جغرافیایی»، «گروه خدمات». hierarchical = false: تاکسونومی مثل «برچسب» عمل می‌کند — flat، با رابط ورودی متنی. مناسب برای: «برند»، «رنگ»، «تگ خاص»، «نویسندهٔ مهمان». نکته: انتخاب اشتباه این پارامتر، هزینهٔ مهاجرت بالایی در ادامهٔ پروژه دارد. اگر تاکسونومی را false بگذارید و بعداً بخواهید زیربرندها را سازمان دهید، نیاز به بازنویسی داده است. توصیه: اگر شک دارید، true انتخاب کنید — چون true می‌تواند flat رفتار کند، ولی false نمی‌تواند سلسله‌مراتبی شود. الگوی دقیق در ساخت تاکسونومی سفارشی.

rewrite و ساختار URL تاکسونومی

ساختار URL، روی سئو و UX اثر مستقیم دارد. الگوی حرفه‌ای:

'rewrite' => array(
    'slug'         => 'brand',
    'with_front'   => false,
    'hierarchical' => true,
),

سه نکته: یک — slug کوتاه و معنادار. دو — with_front = false. اگر نامک نوشته‌ها پیشوند دارد، این تنظیم پیشوند را از URL تاکسونومی حذف می‌کند. سه — flush_rewrite_rules. پس از تغییر rewrite، باید قواعد بازسازی شوند. بهترین روش: بازدید از «تنظیمات ← پیوندهای یکتا» و ذخیره. برنامه‌نویسی‌شده:

function my_plugin_activate() {
    my_plugin_register_brand_taxonomy();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_plugin_activate' );

راهنمای کامل در ساختار URL و سئو.

capability و دسترسی

پارامتر capabilities در ثبت تاکسونومی، کنترل دسترسی به مدیریت ترم‌ها را ممکن می‌کند. برای پروژه‌های حساس، از capability اختصاصی استفاده کنید:

'capabilities' => array(
    'manage_terms' => 'manage_brands',
    'edit_terms'   => 'manage_brands',
    'delete_terms' => 'manage_brands',
    'assign_terms' => 'edit_products',
),

سپس هنگام فعال‌سازی افزونه، این capabilityها را به نقش‌های موردنظر اضافه کنید:

function my_plugin_add_caps() {
    $admin = get_role( 'administrator' );
    $admin->add_cap( 'manage_brands' );
}
register_activation_hook( __FILE__, 'my_plugin_add_caps' );

راهنمای نقش‌ها و دسترسی‌ها در توابع نقش و دسترسی و افزونه‌های مدیریت کاربران.

term meta و دادهٔ اضافه

از وردپرس ۴.۴ به بعد، هر ترم می‌تواند متادیتای اختصاصی داشته باشد. مثال‌های کاربردی: تصویر برند، رنگ دسته، توضیح اضافی، شماره تماس. ثبت متادیتای ترم با هوک brand_edit_form_fields و ذخیره با edited_term:

// نمایش فیلد در فرم ویرایش ترم
function my_plugin_brand_logo_field( $term ) {
    $logo_id = get_term_meta( $term->term_id, '_brand_logo_id', true );
    $logo_url = $logo_id ? wp_get_attachment_image_url( $logo_id, 'medium' ) : '';
    ?>
    <tr class="form-field">
        <th scope="row"><label for="brand_logo_id">لوگوی برند</label></th>
        <td>
            <input type="hidden" name="brand_logo_id" id="brand_logo_id" value="<?php echo esc_attr( $logo_id ); ?>" />
            <button type="button" class="button my-select-logo">انتخاب لوگو</button>
            <?php if ( $logo_url ) : ?>
                <img src="<?php echo esc_url( $logo_url ); ?>" style="max-width:120px;display:block;margin-top:8px;" />
            <?php endif; ?>
        </td>
    </tr>
    <?php
}
add_action( 'brand_edit_form_fields', 'my_plugin_brand_logo_field' );

// ذخیره
function my_plugin_save_brand_logo( $term_id ) {
    if ( isset( $_POST['brand_logo_id'] ) ) {
        update_term_meta( $term_id, '_brand_logo_id', absint( $_POST['brand_logo_id'] ) );
    }
}
add_action( 'edited_term', 'my_plugin_save_brand_logo' );

نکته: برای تاکسونومی با نام متفاوت، هوک‌ها با پیشوند نام تاکسونومی تغییر می‌کنند: {taxonomy}_edit_form_fields، {taxonomy}_add_form_fields، edited_{taxonomy}، created_{taxonomy}. راهنمای کامل فیلد تصویر در کار با متاباکس‌ها. یک نکتهٔ امنیتی: همیشه از absint یا sanitize_text_field روی متادیتای ترم استفاده کنید. راهنما در پاک‌سازی داده‌ها و PHP امن در وردپرس.

کوئری بر اساس تاکسونومی

سه روش کوئری بر اساس تاکسونومی: یک — WP_Query با tax_query: انعطاف کامل:

$args = array(
    'post_type'      => 'product',
    'posts_per_page' => 12,
    'tax_query'      => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'brand',
            'field'    => 'slug',
            'terms'    => 'apple',
        ),
        array(
            'taxonomy' => 'product_cat',
            'field'    => 'term_id',
            'terms'    => array( 5, 12 ),
            'operator' => 'IN',
        ),
    ),
);
$query = new WP_Query( $args );

پارامتر relation می‌تواند AND یا OR باشد. برای شرط‌های پیچیده‌تر، از تودرتویی tax_query استفاده کنید. دو — get_posts: برای کوئری‌های ساده. سه — کوئری ترم: با get_terms:

$terms = get_terms( array(
    'taxonomy'   => 'brand',
    'hide_empty' => true,
    'parent'     => 0,
    'orderby'    => 'name',
    'order'      => 'ASC',
) );

if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
    foreach ( $terms as $term ) {
        printf(
            '<a href="%s">%s (%d)</a>',
            esc_url( get_term_link( $term ) ),
            esc_html( $term->name ),
            (int) $term->count
        );
    }
}

نکته: همیشه is_wp_error را چک کنید. راهنمای کامل در توابع کوئری سفارشی، بهینه‌سازی کوئری‌ها، و بهینه‌سازی کد. یک نکتهٔ مهم در بهینه‌سازی: کوئری‌های tax_query روی تاکسونومی‌های پرترافیک، در سایت‌های بزرگ می‌توانند سنگین باشند. در این حالت، از transients برای کش نتایج استفاده کنید. راهنما در ترنزینت‌ها در وردپرس.

نمایش تاکسونومی در قالب

نمایش ترم‌های یک محتوا در قالب:

$brands = get_the_terms( get_the_ID(), 'brand' );
if ( ! empty( $brands ) && ! is_wp_error( $brands ) ) {
    echo '<div class="post-brands">';
    foreach ( $brands as $brand ) {
        printf(
            '<a href="%s" class="brand-tag">%s</a>',
            esc_url( get_term_link( $brand ) ),
            esc_html( $brand->name )
        );
    }
    echo '</div>';
}

برای صفحهٔ آرشیو تاکسونومی، دو فایل template در اولویت قرار می‌گیرند:

taxonomy-brand.php        # آرشیو تاکسونومی «برند»
taxonomy-brand-apple.php  # آرشیو ترم «apple»
taxonomy.php              # fallback عمومی

الگوی تفکیک در ساختار فایل‌های قالب استاندارد. در فایل آرشیو تاکسونومی، به متادیتای ترم دسترسی دارید:

$term = get_queried_object();
if ( $term && ! empty( $term->term_id ) ) {
    $logo_id = get_term_meta( $term->term_id, '_brand_logo_id', true );
    if ( $logo_id ) {
        echo wp_get_attachment_image( $logo_id, 'medium' );
    }
}

یک قاعده در نمایش: صفحهٔ آرشیو تاکسونومی، صفحهٔ ایندکس‌شده است. اگر تعداد ترم‌ها زیاد است، صفحه‌بندی را جدی بگیرید. راهنمای صفحه‌بندی در ساختار URL و سئو.

سفارشی‌سازی پیشخوان تاکسونومی

چهار سفارشی‌سازی پرکاربرد در پیشخوان تاکسونومی: یک — ستون اختصاصی در لیست ترم‌ها:

add_filter( 'manage_edit-brand_columns', function( $cols ) {
    $cols['brand_logo'] = 'لوگو';
    return $cols;
} );

add_filter( 'manage_brand_custom_column', function( $content, $column, $term_id ) {
    if ( $column === 'brand_logo' ) {
        $logo_id = get_term_meta( $term_id, '_brand_logo_id', true );
        if ( $logo_id ) {
            echo wp_get_attachment_image( $logo_id, 'thumbnail' );
        }
    }
    return $content;
}, 10, 3 );

دو — فیلتر dropdown در بالای لیست. سه — فیلد اضافی در فرم افزودن ترم: با هوک brand_add_form_fields. چهار — ستون‌بندی سفارشی. راهنمای مشابه در کار با متاباکس‌ها و ساخت منوی مدیریتی. یک نکته: در فرم «افزودن ترم جدید»، فیلد فایل آپلود به‌طور پیش‌فرض وجود ندارد. برای اضافه‌کردن، نیاز به enqueue اسکریپت wp.media در پیشخوان با admin_enqueue_scripts دارید.

ساختار کلاس‌محور تاکسونومی

در افزونه‌های جدی، تاکسونومی در یک کلاس اختصاصی تعریف می‌شود:

class My_Plugin_Brand_Taxonomy {
    const TAXONOMY = 'brand';
    const POST_TYPES = array( 'product', 'accessory' );

    public static function init() {
        add_action( 'init', array( __CLASS__, 'register' ), 0 );
        add_action( self::TAXONOMY . '_edit_form_fields', array( __CLASS__, 'render_logo_field' ) );
        add_action( 'edited_' . self::TAXONOMY, array( __CLASS__, 'save_logo' ) );
        add_filter( 'manage_edit-' . self::TAXONOMY . '_columns', array( __CLASS__, 'columns' ) );
    }

    public static function register() {
        register_taxonomy( self::TAXONOMY, self::POST_TYPES, self::get_args() );
    }

    public static function render_logo_field( $term ) {
        $logo_id = get_term_meta( $term->term_id, '_brand_logo_id', true );
        include plugin_dir_path( __FILE__ ) . '../admin/views/brand-logo-field.php';
    }

    public static function save_logo( $term_id ) {
        if ( isset( $_POST['brand_logo_id'] ) ) {
            update_term_meta(
                $term_id,
                '_brand_logo_id',
                absint( $_POST['brand_logo_id'] )
            );
        }
    }

    public static function columns( $cols ) {
        $cols['brand_logo'] = 'لوگو';
        return $cols;
    }

    private static function get_args() {
        return array(
            'hierarchical'      => true,
            'public'            => true,
            'show_admin_column' => true,
            'show_in_rest'      => true,
            'rewrite'           => array( 'slug' => 'brand', 'with_front' => false ),
            'labels'            => self::get_labels(),
        );
    }

    private static function get_labels() {
        return array(
            'name'          => __( 'برندها', 'my-plugin' ),
            'singular_name' => __( 'برند', 'my-plugin' ),
            'menu_name'     => __( 'برندها', 'my-plugin' ),
        );
    }
}
My_Plugin_Brand_Taxonomy::init();

مزیت این ساختار: تمام کد تاکسونومی در یک نقطه، بدون تعارض، قابل انتقال. الگوهای مشابه در کدنویسی اختصاصی افزونه، ساختار فایل‌های افزونهٔ استاندارد، و استانداردهای کدنویسی وردپرس. در پروژه‌ای با چند تاکسونومی، این الگو، تفاوت بین فایل اصلی ۱۵۰ خطی و فایل اصلی ۶۰۰ خطی را می‌سازد. یک نکتهٔ ساختاری: منطق داده (query، ذخیره، به‌روزرسانی) را از لایهٔ نمایش (admin، front-end) جدا کنید. این جداسازی، در آپدیت‌های بعدی، نگهداری را چند برابر ساده می‌کند.

اشتباهات رایج

  • ثبت تاکسونومی روی هوک اشتباه: باید init باشد، نه after_setup_theme یا هوک دیگر. هوک‌های وردپرس.
  • نبود اولویت ۰ در هوک init: تاکسونومی قبل از CPT ثبت می‌شود و اتصال برقرار نمی‌شود. ساخت CPT.
  • نام تاکسونومی با حروف بزرگ یا خط تیره: فقط حروف کوچک لاتین و زیرخط.
  • انتخاب اشتباه hierarchical از ابتدا: مهاجرت بعدی گران. ساخت تاکسونومی.
  • نبود flush_rewrite_rules: صفحه‌های تاکسونومی ۴۰۴ می‌شوند.
  • نبود show_in_rest: گوتنبرگ و REST API کار نمی‌کنند.
  • نبود show_admin_column: ستون ترم در لیست CPT نمایش داده نمی‌شود.
  • نبود is_wp_error در get_the_terms: خطای PHP در صورت نبود تاکسونومی. اعتبارسنجی داده‌ها.
  • نبود nonce و check_user_can در فرم‌های ترم: خطر CSRF. نانس وردپرس.
  • نبود sanitize روی term meta: خطر XSS. پاک‌سازی داده‌ها.
  • نبود ساختار کلاس‌محور در پروژه‌های بزرگ: نگهداری سخت. کدنویسی اختصاصی افزونه.
  • حذف تاکسونومی بدون پاک‌سازی داده: ردیف‌های بی‌استفاده در wp_termmeta و wp_term_relationships. پاک‌سازی دیتابیس.
  • نمایش تمام ترم‌ها در یک صفحه بدون صفحه‌بندی: کندی و UX ضعیف. بهینه‌سازی کد.

کار حرفه‌ای با تاکسونومی سفارشی، مجموعه‌ای از تصمیم‌های درست در طول پروژه است: ثبت با پارامترهای دقیق، برچسب‌های ترجمه‌پذیر، انتخاب صحیح hierarchical، rewrite مناسب، capability اختصاصی، term meta، کوئری بهینه، نمایش در template، سفارشی‌سازی پیشخوان، و ساختار کلاس‌محور. اگر امروز یک کار در این مسیر انجام می‌دهید: یکی از تاکسونومی‌های فعلی پروژهٔ خود را باز کنید و ببینید کدام‌یک از این پارامترها را نادیده گرفته‌اید. همان بازبینی کوچک، در پروژه‌های بعدی تبدیل به الگوی ذهنی می‌شود. اگر تجربه‌ای از یک تاکسونومی سفارشی دارید که در بلندمدت مفید یا پرمشکل بوده — به‌ویژه در سایت‌های پرمحتوا — در دیدگاه‌ها بنویسید؛ همان گزارش‌های واقعی، این راهنما را دقیق‌تر می‌کند. 🏷️