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

CPT در وردپرس: بازنگری کوتاه

CPT یک نوع محتوای مستقل از نوشته و برگه است که ساختار و ظاهر اختصاصی خودش را دارد. سه نشانهٔ روشن برای نیاز به CPT: یک — محتوایی که ساختار متفاوت از نوشته و برگه دارد. مثال: پروژه‌ها با فیلد «سال اجرا» یا محصولات با «گارانتی». دو — نیاز به URL اختصاصی برای آن نوع محتوا. مثال: /project/example-name/. سه — نیاز به رابط کاربری جداگانه در پیشخوان. مثال: منوی «پروژه‌ها» جدا از منوی «نوشته‌ها». ساخت اولیهٔ CPT در ساخت CPT توضیح داده شده. این مقاله، نکات حرفه‌ای و الگوهایی را پوشش می‌دهد که در پروژه‌های واقعی به‌کار می‌آید.

CPT یک تصمیم معماری داده است، نه فقط یک تیک در پنل وردپرس. اشتباه در انتخاب ساختار، در ماه سوم هزینه‌اش را نشان می‌دهد.

ثبت CPT با پارامترهای درست

اسکلت حرفه‌ای ثبت CPT:

function my_plugin_register_project_cpt() {
    $labels = array(
        'name'               => 'پروژه‌ها',
        'singular_name'      => 'پروژه',
        'add_new'            => 'افزودن پروژه',
        'add_new_item'       => 'افزودن پروژهٔ جدید',
        'edit_item'          => 'ویرایش پروژه',
        'new_item'           => 'پروژهٔ جدید',
        'view_item'          => 'مشاهدهٔ پروژه',
        'all_items'          => 'همهٔ پروژه‌ها',
        'search_items'       => 'جستجوی پروژه‌ها',
        'not_found'          => 'پروژه‌ای یافت نشد',
        'not_found_in_trash' => 'در زباله‌دان پروژه‌ای نیست',
        'menu_name'          => 'پروژه‌ها',
    );

    $args = array(
        'labels'          => $labels,
        'public'          => true,
        'has_archive'     => true,
        'menu_icon'       => 'dashicons-portfolio',
        'menu_position'   => 26,
        'supports'        => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
        'rewrite'         => array( 'slug' => 'project', 'with_front' => false ),
        'show_in_rest'    => true,
        'hierarchical'    => false,
        'capability_type' => 'post',
    );

    register_post_type( 'project', $args );
}
add_action( 'init', 'my_plugin_register_project_cpt' );

پارامترهای کلیدی: یک — public: اگر false، CPT از فرانت‌اند مخفی می‌شود و فقط در پیشخوان مدیریت می‌شود. برای داده‌های داخلی. دو — has_archive: اگر true، صفحهٔ آرشیو ساخته می‌شود. سه — show_in_rest: برای دسترسی REST و سازگاری با گوتنبرگ. چهار — with_front: اگر false، پیشوند نامک سایت به URL اضافه نمی‌شود. راهنمای کامل هوک init در هوک‌های وردپرس. انتخاب درست hierarchical از ابتدا مهم است: اگر در ابتدا false بگذارید و بعداً بخواهید سلسله‌مراتبی شود، مهاجرت داده گران است.

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

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

$labels = array(
    'name'               => _x( 'پروژه‌ها', 'Post type general name', 'my-plugin' ),
    'singular_name'      => _x( 'پروژه', 'Post type singular name', 'my-plugin' ),
    'menu_name'          => _x( 'پروژه‌ها', 'Admin Menu text', 'my-plugin' ),
    'add_new'            => __( 'افزودن پروژه', 'my-plugin' ),
    'add_new_item'       => __( 'افزودن پروژهٔ جدید', 'my-plugin' ),
    'edit_item'          => __( 'ویرایش پروژه', 'my-plugin' ),
    'new_item'           => __( 'پروژهٔ جدید', 'my-plugin' ),
    'view_item'          => __( 'مشاهدهٔ پروژه', 'my-plugin' ),
    'search_items'       => __( 'جستجوی پروژه‌ها', 'my-plugin' ),
    'not_found'          => __( 'پروژه‌ای یافت نشد', 'my-plugin' ),
    'not_found_in_trash' => __( 'در زباله‌دان پروژه‌ای نیست', 'my-plugin' ),
);

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

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

انتخاب supports مناسب

پارامتر supports، تعیین می‌کند کدام قابلیت‌های ویرایشگر برای CPT فعال باشند. گزینه‌های پرکاربرد: title: عنوان. editor: ویرایشگر محتوا. thumbnail: تصویر شاخص. excerpt: خلاصه. custom-fields: باکس متادیتای پیش‌فرض. comments: دیدگاه‌ها. revisions: نسخه‌ها. author: نویسنده. page-attributes: ترتیب و سلسله‌مراتب (فقط اگر hierarchical = true باشد). نکته: برای CPTهای محتوایی، title، editor، thumbnail، excerpt معمولاً لازم‌اند. برای CPTهای داده‌ای که از فیلد سفارشی استفاده می‌کنند، ممکن است editor غیرفعال باشد. الگوی دقیق در ساخت CPT. انتخاب اشتباه در supports، در UX پیشخوان هزینه دارد: اگر title را غیرفعال کنید ولی CPT نیاز به عنوان داشته باشد، کاربر باید عنوان را در فیلد سفارشی وارد کند که تجربهٔ بدی است.

rewrite و ساختار URL

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

'rewrite' => array(
    'slug'       => 'project',
    'with_front' => false,
    'feeds'      => true,
    'pages'      => true,
),

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

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

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

capability و نقش‌ها

برای کنترل دقیق دسترسی، از capability_type و map_meta_cap استفاده کنید:

'capability_type' => 'project',
'map_meta_cap'    => true,

با این تنظیم، وردپرس capabilityهای اختصاصی مثل edit_project، edit_projects، edit_others_projects، publish_projects، delete_project را می‌سازد. سپس هنگام فعال‌سازی افزونه، این capabilityها را به نقش‌های موردنظر اضافه کنید:

function my_plugin_activate_caps() {
    $admin = get_role( 'administrator' );
    $editor = get_role( 'editor' );

    $caps = array(
        'edit_project', 'read_project', 'delete_project',
        'edit_projects', 'edit_others_projects', 'publish_projects',
        'read_private_projects', 'delete_projects', 'delete_private_projects',
        'delete_published_projects', 'delete_others_projects',
        'edit_private_projects', 'edit_published_projects',
    );

    foreach ( $caps as $cap ) {
        $admin->add_cap( $cap );
        $editor->add_cap( $cap );
    }
}
register_activation_hook( __FILE__, 'my_plugin_activate_caps' );

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

اتصال به تاکسونومی سفارشی

برای CPT «پروژه»، معمولاً یک تاکسونومی «دستهٔ پروژه» لازم است. ثبت:

function my_plugin_register_project_taxonomy() {
    register_taxonomy( 'project_cat', 'project', array(
        'labels' => array(
            'name'          => 'دسته‌های پروژه',
            'singular_name' => 'دستهٔ پروژه',
            'search_items'  => 'جستجوی دسته‌ها',
            'all_items'     => 'همهٔ دسته‌ها',
            'edit_item'     => 'ویرایش دسته',
            'add_new_item'  => 'افزودن دستهٔ جدید',
        ),
        'hierarchical'      => true,
        'public'            => true,
        'show_admin_column' => true,
        'show_in_rest'      => true,
        'rewrite'           => array( 'slug' => 'project-category' ),
    ) );
}
add_action( 'init', 'my_plugin_register_project_taxonomy', 0 );

نکته: اولویت 0 در هوک init تا تاکسونومی قبل از CPT ثبت شود. راهنمای کامل در ساخت تاکسونومی سفارشی.

کوئری سفارشی CPT

نمایش CPT در صفحات سفارشی، با WP_Query. الگوی حرفه‌ای با بهینه‌سازی:

$args = array(
    'post_type'              => 'project',
    'posts_per_page'         => 12,
    'no_found_rows'          => true,
    'update_post_meta_cache' => false,
    'update_post_term_cache' => false,
    'tax_query'              => array(
        array(
            'taxonomy' => 'project_cat',
            'field'    => 'slug',
            'terms'    => 'web-design',
        ),
    ),
    'meta_query'             => array(
        array(
            'key'     => '_project_year',
            'value'   => 2024,
            'compare' => '>=',
            'type'    => 'NUMERIC',
        ),
    ),
);
$query = new WP_Query( $args );

if ( $query->have_posts() ) :
    while ( $query->have_posts() ) : $query->the_post();
        ?>
        <article class="project-item">
            <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
            <?php the_excerpt(); ?>
        </article>
        <?php
    endwhile;
    wp_reset_postdata();
endif;

نکات بهینه‌سازی: یک — no_found_rows => true: اگر نیازی به صفحه‌بندی نیست، کوئری شمارش را حذف می‌کند. دو — update_post_meta_cache و update_post_term_cache: اگر از متا یا ترم استفاده نمی‌کنید، این دو را غیرفعال کنید. سه — wp_reset_postdata: پس از حلقه، متغیر جهانی $post را بازمی‌گرداند. راهنمای کامل در توابع کوئری سفارشی، بهینه‌سازی کوئری‌های وردپرس، و بهینه‌سازی کد.

نمایش CPT در قالب

وردپرس به‌طور خودکار فایل‌های template زیر را برای CPT جستجو می‌کند:

single-project.php          # تک‌پروژه
archive-project.php         # آرشیو پروژه‌ها
taxonomy-project_cat.php    # آرشیو دستهٔ پروژه

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

$year = get_post_meta( get_the_ID(), '_project_year', true );
if ( ! empty( $year ) ) {
    printf( '<p>سال اجرا: %s</p>', esc_html( $year ) );
}

برای دسته‌های پروژه:

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

نکته: همیشه is_wp_error را چک کنید تا در صورت نبود ترم یا خطای query، خطای PHP رخ ندهد.

سفارشی‌سازی پیشخوان CPT

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

add_filter( 'manage_project_posts_columns', function( $cols ) {
    $cols['project_year'] = 'سال اجرا';
    return $cols;
} );

add_action( 'manage_project_posts_custom_column', function( $col, $post_id ) {
    if ( $col === 'project_year' ) {
        echo esc_html( get_post_meta( $post_id, '_project_year', true ) );
    }
}, 10, 2 );

دو — فیلتر در بالای لیست CPT. با restrict_manage_posts می‌توان dropdown اضافه کرد. سه — مرتب‌سازی بر اساس متادیتا. با فیلتر manage_edit-project_sortable_columns و pre_get_posts. چهار — پیش‌فرض‌های پس از انتشار. در save_post، می‌توانید بعد از ذخیره، اقدامات خودکار انجام دهید. راهنمای دقیق این چهار الگو در ساخت منوی مدیریتی و کار با متاباکس‌ها.

ساختار کلاس‌محور CPT

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

class My_Plugin_Project_CPT {
    const POST_TYPE = 'project';
    const TAXONOMY  = 'project_cat';

    public static function init() {
        add_action( 'init', array( __CLASS__, 'register' ), 0 );
        add_action( 'init', array( __CLASS__, 'register_taxonomy' ), 0 );
        add_filter( 'manage_project_posts_columns', array( __CLASS__, 'columns' ) );
    }

    public static function register() {
        register_post_type( self::POST_TYPE, self::get_args() );
    }

    public static function register_taxonomy() {
        register_taxonomy( self::TAXONOMY, self::POST_TYPE, array(
            'hierarchical'      => true,
            'public'            => true,
            'show_admin_column' => true,
            'show_in_rest'      => true,
        ) );
    }

    public static function columns( $cols ) {
        $cols['project_year'] = 'سال اجرا';
        return $cols;
    }

    private static function get_args() {
        return array(
            'public'       => true,
            'has_archive'  => true,
            'supports'     => array( 'title', 'editor', 'thumbnail' ),
            'show_in_rest' => true,
            'rewrite'      => array( 'slug' => 'project' ),
        );
    }
}
My_Plugin_Project_CPT::init();

مزیت این ساختار: تمام کد CPT در یک نقطه، بدون تعارض، قابل انتقال. الگوهای مشابه در کدنویسی اختصاصی افزونه، ساختار فایل‌های افزونهٔ استاندارد، و استانداردهای کدنویسی وردپرس. در پروژه‌ای با سه CPT، این الگو تفاوت بین فایل اصلی ۲۰۰ خطی و فایل اصلی ۸۰۰ خطی را می‌سازد. نکتهٔ ساختاری: منطق داده (query، ذخیره، به‌روزرسانی) را از لایهٔ نمایش (template، admin) جدا کنید. این جداسازی، در آپدیت‌های بعدی، نگهداری را چند برابر ساده می‌کند.

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

  • ثبت CPT روی هوک اشتباه: باید init باشد، نه after_setup_theme یا init با اولویت نامناسب. هوک‌های وردپرس.
  • نام CPT با حروف بزرگ یا خط تیره: فقط حروف کوچک لاتین و زیرخط. ساخت CPT.
  • نبود flush_rewrite_rules پس از ثبت: صفحه‌های CPT ۴۰۴ می‌شوند.
  • ثبت CPT در فایل قالب والد: با تغییر قالب از دست می‌رود. چایلد تم.
  • انتخاب اشتباه hierarchical از ابتدا: مهاجرت بعدی گران. ساخت CPT.
  • نبود show_in_rest: گوتنبرگ و REST API کار نمی‌کنند.
  • تاکسونومی متصل به CPT اشتباه: در پیشخوان ظاهر نمی‌شود. تاکسونومی سفارشی.
  • کوئری بدون بهینه‌سازی: کندی سایت. بهینه‌سازی کوئری.
  • نبود wp_reset_postdata پس از کوئری سفارشی: تأثیر روی حلقه‌های بعدی.
  • نبود برچسب فارسی: تجربهٔ کاربری غیرفنی ضعیف. آماده‌سازی فارسی.
  • نبود capability اختصاصی: دسترسی همهٔ ادمین‌ها به داده‌های حساس. نقش و دسترسی.
  • نادیده‌گرفتن پاک‌سازی داده هنگام حذف CPT: ردیف‌های بی‌استفاده در دیتابیس. پاک‌سازی دیتابیس.

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