Introduction
Les Custom Post Types (CPT) et les taxonomies personnalisées constituent le fondement d’une architecture de contenu flexible et scalable dans WordPress. En 2025, avec l’évolution du Full Site Editing et l’importance croissante du SEO structuré, maîtriser ces outils est essentiel pour créer des sites WordPress performants et bien organisés. Ce guide vous accompagnera dans la création, l’optimisation et la gestion avancée des CPT et taxonomies.
Comprendre les Custom Post Types
Quand utiliser un Custom Post Type
Les Custom Post Types sont appropriés lorsque vous avez besoin de :
- Séparer différents types de contenu (produits, témoignages, portfolios)
- Appliquer des workflows de publication différents
- Créer des templates spécifiques
- Organiser du contenu avec des métadonnées distinctes
- Améliorer le SEO avec des structures d’URL personnalisées
- Site immobilier :
property(propriété) - Site événementiel :
event(événement) - Site portfolio :
project(projet) - Site e-learning :
course(cours)
Exemples concrets :
Créer un Custom Post Type complet
Voici un exemple professionnel et testé pour un CPT « Portfolio » :
<?php
/**
* Custom Post Type : Portfolio
*
* @package MonTheme
*/
namespace MonThemePostTypes;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Portfolio {
/**
* Slug du post type
*/
private $post_type = 'portfolio';
/**
* Constructeur
*/
public function **construct() {
add_action( 'init', array( $this, 'register_post_type' ) );
add_action( 'init', array( $this, 'register_taxonomies' ) );
add_filter( 'enter_title_here', array( $this, 'change_title_placeholder' ), 10, 2 );
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
add_action( 'save_post', array( $this, 'save_meta_boxes' ), 10, 2 );
add_filter( 'manage*' . $this->post_type . '*posts_columns', array( $this, 'custom_columns' ) );
add_action( 'manage*' . $this->post_type . '*posts_custom_column', array( $this, 'custom_column_content' ), 10, 2 );
add_filter( 'manage_edit-' . $this->post_type . '*sortable_columns', array( $this, 'sortable_columns' ) );
}
/**
* Enregistrer le Custom Post Type
*/
public function register_post_type() {
$labels = array(
'name' => *x( 'Projets Portfolio', 'Post type general name', 'mon-theme' ),
'singular_name' => *x( 'Projet', 'Post type singular name', 'mon-theme' ),
'menu_name' => *x( 'Portfolio', 'Admin Menu text', 'mon-theme' ),
'name_admin_bar' => *x( 'Projet', 'Add New on Toolbar', 'mon-theme' ),
'add_new' => **( 'Ajouter un projet', 'mon-theme' ),
'add_new_item' => **( 'Ajouter un nouveau projet', 'mon-theme' ),
'new_item' => **( 'Nouveau projet', 'mon-theme' ),
'edit_item' => **( 'Modifier le projet', 'mon-theme' ),
'view_item' => **( 'Voir le projet', 'mon-theme' ),
'all_items' => **( 'Tous les projets', 'mon-theme' ),
'search_items' => **( 'Rechercher des projets', 'mon-theme' ),
'parent_item_colon' => **( 'Projet parent:', 'mon-theme' ),
'not_found' => **( 'Aucun projet trouvé.', 'mon-theme' ),
'not_found_in_trash' => **( 'Aucun projet trouvé dans la corbeille.', 'mon-theme' ),
'featured_image' => *x( 'Image du projet', 'Overrides the "Featured Image" phrase', 'mon-theme' ),
'set_featured_image' => *x( 'Définir l'image du projet', 'Overrides the "Set featured image" phrase', 'mon-theme' ),
'remove_featured_image' => *x( 'Retirer l'image du projet', 'Overrides the "Remove featured image" phrase', 'mon-theme' ),
'use_featured_image' => *x( 'Utiliser comme image du projet', 'Overrides the "Use as featured image" phrase', 'mon-theme' ),
'archives' => *x( 'Archives des projets', 'The post type archive label used in nav menus', 'mon-theme' ),
'insert_into_item' => *x( 'Insérer dans le projet', 'Overrides the "Insert into post" phrase', 'mon-theme' ),
'uploaded_to_this_item' => *x( 'Téléversé vers ce projet', 'Overrides the "Uploaded to this post" phrase', 'mon-theme' ),
'filter_items_list' => *x( 'Filtrer la liste des projets', 'Screen reader text for the filter links', 'mon-theme' ),
'items_list_navigation' => *x( 'Navigation de la liste des projets', 'Screen reader text for the pagination', 'mon-theme' ),
'items_list' => *x( 'Liste des projets', 'Screen reader text for the items list', 'mon-theme' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array(
'slug' => 'portfolio',
'with_front' => false,
'feeds' => true,
'pages' => true,
),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'menu_icon' => 'dashicons-portfolio',
'supports' => array(
'title',
'editor',
'thumbnail',
'excerpt',
'custom-fields',
'revisions',
'author',
'page-attributes',
),
'show_in_rest' => true,
'rest_base' => 'portfolio',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'template' => array(
array( 'core/image', array() ),
array( 'core/paragraph', array(
'placeholder' => **( 'Décrivez votre projet...', 'mon-theme' ),
) ),
),
'template_lock' => false,
);
register_post_type( $this->post_type, $args );
}
/**
* Enregistrer les taxonomies
*/
public function register_taxonomies() {
// Taxonomie : Catégories de portfolio (hiérarchique)
$category_labels = array(
'name' => *x( 'Catégories', 'taxonomy general name', 'mon-theme' ),
'singular_name' => *x( 'Catégorie', 'taxonomy singular name', 'mon-theme' ),
'search_items' => **( 'Rechercher des catégories', 'mon-theme' ),
'all_items' => **( 'Toutes les catégories', 'mon-theme' ),
'parent_item' => **( 'Catégorie parente', 'mon-theme' ),
'parent_item_colon' => **( 'Catégorie parente:', 'mon-theme' ),
'edit_item' => **( 'Modifier la catégorie', 'mon-theme' ),
'update_item' => **( 'Mettre à jour la catégorie', 'mon-theme' ),
'add_new_item' => **( 'Ajouter une nouvelle catégorie', 'mon-theme' ),
'new_item_name' => **( 'Nom de la nouvelle catégorie', 'mon-theme' ),
'menu_name' => **( 'Catégories', 'mon-theme' ),
);
$category_args = array(
'hierarchical' => true,
'labels' => $category_labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array(
'slug' => 'categorie-portfolio',
'with_front' => false,
'hierarchical' => true,
),
'show_in_rest' => true,
'rest_base' => 'portfolio-categories',
);
register_taxonomy( 'portfolio_category', array( $this->post_type ), $category_args );
// Taxonomie : Tags de portfolio (non hiérarchique)
$tag_labels = array(
'name' => *x( 'Tags', 'taxonomy general name', 'mon-theme' ),
'singular_name' => *x( 'Tag', 'taxonomy singular name', 'mon-theme' ),
'search_items' => **( 'Rechercher des tags', 'mon-theme' ),
'popular_items' => **( 'Tags populaires', 'mon-theme' ),
'all_items' => **( 'Tous les tags', 'mon-theme' ),
'edit_item' => **( 'Modifier le tag', 'mon-theme' ),
'update_item' => **( 'Mettre à jour le tag', 'mon-theme' ),
'add_new_item' => **( 'Ajouter un nouveau tag', 'mon-theme' ),
'new_item_name' => **( 'Nom du nouveau tag', 'mon-theme' ),
'separate_items_with_commas' => **( 'Séparer les tags par des virgules', 'mon-theme' ),
'add_or_remove_items' => **( 'Ajouter ou retirer des tags', 'mon-theme' ),
'choose_from_most_used' => **( 'Choisir parmi les tags les plus utilisés', 'mon-theme' ),
'menu_name' => **( 'Tags', 'mon-theme' ),
);
$tag_args = array(
'hierarchical' => false,
'labels' => $tag_labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array(
'slug' => 'tag-portfolio',
),
'show_in_rest' => true,
'rest_base' => 'portfolio-tags',
);
register_taxonomy( 'portfolio_tag', array( $this->post_type ), $tag_args );
}
/**
* Changer le placeholder du titre
*/
public function change_title_placeholder( $title, $post ) {
if ( $post->post_type === $this->post_type ) {
$title = **( 'Entrez le nom du projet', 'mon-theme' );
}
return $title;
}
/**
* Ajouter des meta boxes
*/
public function add_meta_boxes() {
add_meta_box(
'portfolio_details',
**( 'Détails du projet', 'mon-theme' ),
array( $this, 'render_details_meta_box' ),
$this->post_type,
'normal',
'high'
);
add_meta_box(
'portfolio_client',
**( 'Informations client', 'mon-theme' ),
array( $this, 'render_client_meta_box' ),
$this->post_type,
'side',
'default'
);
}
/**
* Render meta box : Détails du projet
*/
public function render_details_meta_box( $post ) {
// Nonce pour la sécurité
wp_nonce_field( 'portfolio_details_nonce', 'portfolio_details_nonce' );
// Récupérer les valeurs existantes
$project_url = get_post_meta( $post->ID, '*portfolio_project_url', true );
$project_date = get_post_meta( $post->ID, '*portfolio_project_date', true );
$technologies = get_post_meta( $post->ID, '*portfolio_technologies', true );
$github_url = get_post_meta( $post->ID, '*portfolio_github_url', true );
?>
<div class="portfolio-meta-box">
<p>
<label for="portfolio_project_url">
**<?php esc_html_e( 'URL du projet', 'mon-theme' ); ?>**
</label>
<input
type="url"
id="portfolio_project_url"
name="portfolio_project_url"
value="<?php echo esc_url( $project_url ); ?>"
class="widefat"
placeholder="https://exemple.com"
/>
</p>
<p>
<label for="portfolio_project_date">
**<?php esc_html_e( 'Date de réalisation', 'mon-theme' ); ?>**
</label>
<input
type="date"
id="portfolio_project_date"
name="portfolio_project_date"
value="<?php echo esc_attr( $project_date ); ?>"
class="widefat"
/>
</p>
<p>
<label for="portfolio_technologies">
**<?php esc_html_e( 'Technologies utilisées', 'mon-theme' ); ?>**
</label>
<input
type="text"
id="portfolio_technologies"
name="portfolio_technologies"
value="<?php echo esc_attr( $technologies ); ?>"
class="widefat"
placeholder="WordPress, React, PHP, etc."
/>
<span class="description">
<?php esc_html_e( 'Séparez les technologies par des virgules', 'mon-theme' ); ?>
</span>
</p>
<p>
<label for="portfolio_github_url">
**<?php esc_html_e( 'URL GitHub', 'mon-theme' ); ?>**
</label>
<input
type="url"
id="portfolio_github_url"
name="portfolio_github_url"
value="<?php echo esc_url( $github_url ); ?>"
class="widefat"
placeholder="https://github.com/username/repo"
/>
</p>
</div>
<?php
}
/**
* Render meta box : Informations client
*/
public function render_client_meta_box( $post ) {
wp_nonce_field( 'portfolio_client_nonce', 'portfolio_client_nonce' );
$client_name = get_post_meta( $post->ID, '*portfolio_client_name', true );
$client_website = get_post_meta( $post->ID, '*portfolio_client_website', true );
$is_featured = get_post_meta( $post->ID, '*portfolio_is_featured', true );
?>
<div class="portfolio-client-meta">
<p>
<label for="portfolio_client_name">
**<?php esc_html_e( 'Nom du client', 'mon-theme' ); ?>**
</label>
<input
type="text"
id="portfolio_client_name"
name="portfolio_client_name"
value="<?php echo esc_attr( $client_name ); ?>"
class="widefat"
/>
</p>
<p>
<label for="portfolio_client_website">
**<?php esc_html_e( 'Site web du client', 'mon-theme' ); ?>**
</label>
<input
type="url"
id="portfolio_client_website"
name="portfolio_client_website"
value="<?php echo esc_url( $client_website ); ?>"
class="widefat"
/>
</p>
<p>
<label>
<input
type="checkbox"
name="portfolio_is_featured"
value="1"
<?php checked( $is_featured, '1' ); ?>
/>
<?php esc_html_e( 'Projet vedette', 'mon-theme' ); ?>
</label>
</p>
</div>
<?php
}
/**
* Sauvegarder les meta boxes
*/
public function save_meta_boxes( $post_id, $post ) {
// Vérifier le post type
if ( $post->post_type !== $this->post_type ) {
return;
}
// Vérifier l'autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Vérifier les permissions
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Sauvegarder les détails du projet
if ( isset( $*POST['portfolio_details_nonce'] ) &&
wp_verify_nonce( $*POST['portfolio_details_nonce'], 'portfolio_details_nonce' ) ) {
if ( isset( $*POST['portfolio_project_url'] ) ) {
update_post_meta( $post_id, '*portfolio_project_url', esc_url_raw( $*POST['portfolio_project_url'] ) );
}
if ( isset( $*POST['portfolio_project_date'] ) ) {
update_post_meta( $post_id, '*portfolio_project_date', sanitize_text_field( $*POST['portfolio_project_date'] ) );
}
if ( isset( $*POST['portfolio_technologies'] ) ) {
update_post_meta( $post_id, '*portfolio_technologies', sanitize_text_field( $*POST['portfolio_technologies'] ) );
}
if ( isset( $*POST['portfolio_github_url'] ) ) {
update_post_meta( $post_id, '*portfolio_github_url', esc_url_raw( $*POST['portfolio_github_url'] ) );
}
}
// Sauvegarder les informations client
if ( isset( $*POST['portfolio_client_nonce'] ) &&
wp_verify_nonce( $*POST['portfolio_client_nonce'], 'portfolio_client_nonce' ) ) {
if ( isset( $*POST['portfolio_client_name'] ) ) {
update_post_meta( $post_id, '*portfolio_client_name', sanitize_text_field( $*POST['portfolio_client_name'] ) );
}
if ( isset( $*POST['portfolio_client_website'] ) ) {
update_post_meta( $post_id, '*portfolio_client_website', esc_url_raw( $*POST['portfolio_client_website'] ) );
}
$is_featured = isset( $*POST['portfolio_is_featured'] ) ? '1' : '0';
update_post_meta( $post_id, '*portfolio_is_featured', $is_featured );
}
}
/**
* Colonnes personnalisées dans l'admin
*/
public function custom_columns( $columns ) {
// Retirer la colonne date
unset( $columns['date'] );
// Ajouter nos colonnes personnalisées
$new_columns = array(
'thumbnail' => **( 'Image', 'mon-theme' ),
'client' => **( 'Client', 'mon-theme' ),
'technologies' => **( 'Technologies', 'mon-theme' ),
'project_date' => **( 'Date projet', 'mon-theme' ),
'is_featured' => **( 'Vedette', 'mon-theme' ),
);
// Fusionner avec les colonnes existantes
return array_merge( $columns, $new_columns );
}
/**
* Contenu des colonnes personnalisées
*/
public function custom_column_content( $column, $post_id ) {
switch ( $column ) {
case 'thumbnail':
if ( has_post_thumbnail( $post_id ) ) {
echo get_the_post_thumbnail( $post_id, array( 60, 60 ) );
} else {
echo '<span class="dashicons dashicons-format-image" style="font-size: 40px; color: #ddd;"></span>';
}
break;
case 'client':
$client_name = get_post_meta( $post_id, '*portfolio_client_name', true );
echo $client_name ? esc_html( $client_name ) : '-';
break;
case 'technologies':
$technologies = get_post_meta( $post_id, '*portfolio_technologies', true );
if ( $technologies ) {
$tech_array = array_map( 'trim', explode( ',', $technologies ) );
echo '<div class="portfolio-technologies">';
foreach ( $tech_array as $tech ) {
echo '<span class="tech-badge">' . esc_html( $tech ) . '</span> ';
}
echo '</div>';
} else {
echo '-';
}
break;
case 'project_date':
$project_date = get_post_meta( $post_id, '*portfolio_project_date', true );
if ( $project_date ) {
echo esc_html( mysql2date( 'd/m/Y', $project_date ) );
} else {
echo '-';
}
break;
case 'is_featured':
$is_featured = get_post_meta( $post_id, '*portfolio_is_featured', true );
if ( $is_featured === '1' ) {
echo '<span class="dashicons dashicons-star-filled" style="color: #ffa500;"></span>';
} else {
echo '<span class="dashicons dashicons-star-empty" style="color: #ccc;"></span>';
}
break;
}
}
/**
* Colonnes triables
*/
public function sortable_columns( $columns ) {
$columns['project_date'] = 'project_date';
$columns['client'] = 'client';
return $columns;
}
}
// Initialiser le CPT
new Portfolio();
Requêtes avancées avec WP_Query
Récupérer des projets portfolio avec filtres
<?php
/**
* Exemples de requêtes avancées pour CPT
*/
// Récupérer les 6 projets vedettes les plus récents
$featured_projects = new WP_Query( array(
'post_type' => 'portfolio',
'posts_per_page' => 6,
'meta_query' => array(
array(
'key' => '*portfolio_is_featured',
'value' => '1',
'compare' => '=',
),
),
'orderby' => 'date',
'order' => 'DESC',
) );
if ( $featured_projects->have_posts() ) {
while ( $featured_projects->have_posts() ) {
$featured_projects->the_post();
// Afficher le projet
get_template_part( 'template-parts/content', 'portfolio' );
}
wp_reset_postdata();
}
// Récupérer les projets par catégorie et tag
$category_projects = new WP_Query( array(
'post_type' => 'portfolio',
'posts_per_page' => 10,
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'portfolio_category',
'field' => 'slug',
'terms' => 'web-design',
),
array(
'taxonomy' => 'portfolio_tag',
'field' => 'slug',
'terms' => array( 'wordpress', 'responsive' ),
'operator' => 'IN',
),
),
) );
// Récupérer les projets d'un client spécifique
$client_projects = new WP_Query( array(
'post_type' => 'portfolio',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '*portfolio_client_name',
'value' => 'ACME Corporation',
'compare' => '=',
),
),
) );
// Requête complexe : projets avec technologies et date
$complex_query = new WP_Query( array(
'post_type' => 'portfolio',
'posts_per_page' => 12,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '*portfolio_technologies',
'value' => 'React',
'compare' => 'LIKE',
),
array(
'key' => '*portfolio_project_date',
'value' => '2024-01-01',
'compare' => '>=',
'type' => 'DATE',
),
),
'tax_query' => array(
array(
'taxonomy' => 'portfolio_category',
'field' => 'slug',
'terms' => 'web-application',
),
),
'orderby' => array(
'meta_value' => 'DESC',
'date' => 'DESC',
),
'meta_key' => '*portfolio_project_date',
) );
Optimisation des performances
Indexation de la base de données
Pour améliorer les performances des requêtes sur les meta données :
<?php
/**
* Ajouter des index à la base de données pour les meta_key fréquemment utilisés
*/
function montheme_add_portfolio_indexes() {
global $wpdb;
// Vérifier si l'index existe déjà
$index_exists = $wpdb->get_var(
"SHOW INDEX FROM {$wpdb->postmeta}
WHERE Key_name = 'meta_key_portfolio_featured'"
);
if ( ! $index_exists ) {
$wpdb->query(
"CREATE INDEX meta_key_portfolio_featured
ON {$wpdb->postmeta} (meta_key(191), meta_value(50))
WHERE meta_key = '*portfolio_is_featured'"
);
}
// Index pour les dates de projet
$date_index_exists = $wpdb->get_var(
"SHOW INDEX FROM {$wpdb->postmeta}
WHERE Key_name = 'meta_key_portfolio_date'"
);
if ( ! $date_index_exists ) {
$wpdb->query(
"CREATE INDEX meta_key_portfolio_date
ON {$wpdb->postmeta} (meta_key(191), meta_value(20))
WHERE meta_key = '*portfolio_project_date'"
);
}
}
add_action( 'after_switch_theme', 'montheme_add_portfolio_indexes' );
Mise en cache des requêtes
<?php
/**
* Système de cache pour les requêtes CPT
*/
class PortfolioCacheManager {
private $cache_group = 'portfolio_queries';
private $cache_duration = 3600; // 1 heure
/**
* Récupérer les projets vedettes avec cache
*/
public function get_featured_projects( $limit = 6 ) {
$cache_key = 'featured_projects*' . $limit;
$projects = wp_cache_get( $cache_key, $this->cache_group );
if ( false === $projects ) {
$query = new WP_Query( array(
'post_type' => 'portfolio',
'posts_per_page' => $limit,
'meta_query' => array(
array(
'key' => '*portfolio_is_featured',
'value' => '1',
'compare' => '=',
),
),
'no_found_rows' => true, // Optimisation : pas de pagination
'update_post_meta_cache' => true,
'update_post_term_cache' => true,
) );
$projects = $query->posts;
wp_cache_set( $cache_key, $projects, $this->cache_group, $this->cache_duration );
}
return $projects;
}
/**
* Invalider le cache lors de la sauvegarde
*/
public function invalidate_cache( $post_id, $post ) {
if ( $post->post_type !== 'portfolio' ) {
return;
}
// Supprimer tous les caches du groupe
wp_cache_flush_group( $this->cache_group );
}
/**
* Initialiser les hooks
*/
public function init() {
add_action( 'save_post_portfolio', array( $this, 'invalidate_cache' ), 10, 2 );
add_action( 'delete_post', array( $this, 'invalidate_cache_on_delete' ) );
}
/**
* Invalider le cache lors de la suppression
*/
public function invalidate_cache_on_delete( $post_id ) {
$post = get_post( $post_id );
if ( $post && $post->post_type === 'portfolio' ) {
wp_cache_flush_group( $this->cache_group );
}
}
}
$portfolio_cache = new PortfolioCacheManager();
$portfolio_cache->init();
Intégration avec Gutenberg
Créer un bloc personnalisé pour afficher les projets
<?php
/**
* Enregistrer un bloc Gutenberg pour le portfolio
*/
function montheme_register_portfolio_block() {
// Vérifier si Gutenberg est actif
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
wp_register_script(
'montheme-portfolio-block',
get_template_directory_uri() . '/assets/js/blocks/portfolio.js',
array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-editor' ),
filemtime( get_template_directory() . '/assets/js/blocks/portfolio.js' )
);
wp_register_style(
'montheme-portfolio-block-editor',
get_template_directory_uri() . '/assets/css/blocks/portfolio-editor.css',
array( 'wp-edit-blocks' ),
filemtime( get_template_directory() . '/assets/css/blocks/portfolio-editor.css' )
);
wp_register_style(
'montheme-portfolio-block',
get_template_directory_uri() . '/assets/css/blocks/portfolio.css',
array(),
filemtime( get_template_directory() . '/assets/css/blocks/portfolio.css' )
);
register_block_type( 'montheme/portfolio-grid', array(
'editor_script' => 'montheme-portfolio-block',
'editor_style' => 'montheme-portfolio-block-editor',
'style' => 'montheme-portfolio-block',
'render_callback' => 'montheme_render_portfolio_block',
'attributes' => array(
'postsToShow' => array(
'type' => 'number',
'default' => 6,
),
'category' => array(
'type' => 'string',
'default' => '',
),
'featuredOnly' => array(
'type' => 'boolean',
'default' => false,
),
'columns' => array(
'type' => 'number',
'default' => 3,
),
),
) );
}
add_action( 'init', 'montheme_register_portfolio_block' );
/**
* Render du bloc portfolio
*/
function montheme_render_portfolio_block( $attributes ) {
$posts_to_show = isset( $attributes['postsToShow'] ) ? absint( $attributes['postsToShow'] ) : 6;
$category = isset( $attributes['category'] ) ? sanitize_text_field( $attributes['category'] ) : '';
$featured_only = isset( $attributes['featuredOnly'] ) ? (bool) $attributes['featuredOnly'] : false;
$columns = isset( $attributes['columns'] ) ? absint( $attributes['columns'] ) : 3;
$args = array(
'post_type' => 'portfolio',
'posts_per_page' => $posts_to_show,
'no_found_rows' => true,
);
if ( $category ) {
$args['tax_query'] = array(
array(
'taxonomy' => 'portfolio_category',
'field' => 'slug',
'terms' => $category,
),
);
}
if ( $featured_only ) {
$args['meta_query'] = array(
array(
'key' => '*portfolio_is_featured',
'value' => '1',
'compare' => '=',
),
);
}
$query = new WP_Query( $args );
if ( ! $query->have_posts() ) {
return '<p>' . esc_html**( 'Aucun projet trouvé.', 'mon-theme' ) . '</p>';
}
ob_start();
?>
<div class="portfolio-grid portfolio-grid--columns-<?php echo esc_attr( $columns ); ?>">
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<article class="portfolio-item">
<?php if ( has_post_thumbnail() ) : ?>
<div class="portfolio-item__thumbnail">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'montheme-featured' ); ?>
</a>
</div>
<?php endif; ?>
<div class="portfolio-item__content">
<h3 class="portfolio-item__title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h3>
<?php
$technologies = get_post_meta( get_the_ID(), '*portfolio_technologies', true );
if ( $technologies ) :
$tech_array = array_map( 'trim', explode( ',', $technologies ) );
?>
<div class="portfolio-item__technologies">
<?php foreach ( $tech_array as $tech ) : ?>
<span class="tech-badge"><?php echo esc_html( $tech ); ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ( has_excerpt() ) : ?>
<div class="portfolio-item__excerpt">
<?php the_excerpt(); ?>
</div>
<?php endif; ?>
<a href="<?php the_permalink(); ?>" class="portfolio-item__link">
<?php esc_html_e( 'Voir le projet', 'mon-theme' ); ?>
</a>
</div>
</article>
<?php endwhile; ?>
</div>
<?php
wp_reset_postdata();
return ob_get_clean();
}
SEO et structured data
Ajouter des données structurées Schema.org
<?php
/**
* Ajouter des données structurées pour les projets portfolio
*/
function montheme_portfolio_structured_data() {
if ( ! is_singular( 'portfolio' ) ) {
return;
}
$post_id = get_the_ID();
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'CreativeWork',
'name' => get_the_title(),
'description' => get_the_excerpt(),
'url' => get_permalink(),
'dateCreated' => get_the_date( 'c' ),
'dateModified' => get_the_modified_date( 'c' ),
'author' => array(
'@type' => 'Person',
'name' => get_the_author(),
),
);
// Ajouter l'image
if ( has_post_thumbnail() ) {
$image_id = get_post_thumbnail_id();
$image_data = wp_get_attachment_image_src( $image_id, 'full' );
if ( $image_data ) {
$schema['image'] = array(
'@type' => 'ImageObject',
'url' => $image_data[0],
'width' => $image_data[1],
'height' => $image_data[2],
);
}
}
// Ajouter les métadonnées personnalisées
$project_url = get_post_meta( $post_id, '*portfolio_project_url', true );
if ( $project_url ) {
$schema['workExample'] = $project_url;
}
$project_date = get_post_meta( $post_id, '*portfolio_project_date', true );
if ( $project_date ) {
$schema['datePublished'] = mysql2date( 'c', $project_date );
}
$technologies = get_post_meta( $post_id, '*portfolio_technologies', true );
if ( $technologies ) {
$tech_array = array_map( 'trim', explode( ',', $technologies ) );
$schema['keywords'] = implode( ', ', $tech_array );
}
$client_name = get_post_meta( $post_id, '*portfolio_client_name', true );
if ( $client_name ) {
$schema['client'] = array(
'@type' => 'Organization',
'name' => $client_name,
);
$client_website = get_post_meta( $post_id, '*portfolio_client_website', true );
if ( $client_website ) {
$schema['client']['url'] = $client_website;
}
}
// Sortir le JSON-LD
echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ) . '</script>' . "n";
}
add_action( 'wp_head', 'montheme_portfolio_structured_data' );
REST API pour CPT
Ajouter des champs personnalisés à l’API REST
<?php
/**
* Étendre l'API REST pour les projets portfolio
*/
function montheme_register_portfolio_rest_fields() {
// Enregistrer le champ "featured"
register_rest_field( 'portfolio', 'is_featured', array(
'get_callback' => function( $post ) {
return get_post_meta( $post['id'], '*portfolio_is_featured', true ) === '1';
},
'update_callback' => function( $value, $post ) {
return update_post_meta( $post->ID, '*portfolio_is_featured', $value ? '1' : '0' );
},
'schema' => array(
'description' => **( 'Si le projet est vedette', 'mon-theme' ),
'type' => 'boolean',
),
) );
// Enregistrer tous les champs méta en une seule fois
register_rest_field( 'portfolio', 'project_meta', array(
'get_callback' => function( $post ) {
return array(
'project_url' => get_post_meta( $post['id'], '*portfolio_project_url', true ),
'project_date' => get_post_meta( $post['id'], '*portfolio_project_date', true ),
'technologies' => get_post_meta( $post['id'], '*portfolio_technologies', true ),
'github_url' => get_post_meta( $post['id'], '*portfolio_github_url', true ),
'client_name' => get_post_meta( $post['id'], '*portfolio_client_name', true ),
'client_website' => get_post_meta( $post['id'], '*portfolio_client_website', true ),
);
},
'schema' => array(
'description' => **( 'Métadonnées du projet', 'mon-theme' ),
'type' => 'object',
'properties' => array(
'project_url' => array( 'type' => 'string', 'format' => 'uri' ),
'project_date' => array( 'type' => 'string', 'format' => 'date' ),
'technologies' => array( 'type' => 'string' ),
'github_url' => array( 'type' => 'string', 'format' => 'uri' ),
'client_name' => array( 'type' => 'string' ),
'client_website' => array( 'type' => 'string', 'format' => 'uri' ),
),
),
) );
}
add_action( 'rest_api_init', 'montheme_register_portfolio_rest_fields' );
Erreurs courantes à éviter
1. Oublier de flush les rewrite rules
PROBLÈME : URLs 404 après création du CPT
SOLUTION :
// Lors de l'activation du thème ou plugin
function montheme_flush_rewrite_rules() {
// Enregistrer le CPT
montheme_register_portfolio_cpt();
// Flush
flush_rewrite_rules();
}
register_activation_hook( **FILE__, 'montheme_flush_rewrite_rules' );
2. Utiliser un slug trop générique
MAUVAIS :
'rewrite' => array( 'slug' => 'item' )
BON :
'rewrite' => array( 'slug' => 'portfolio-projet' )
3. Ne pas penser au SEO
MAUVAIS : Pas de has_archive, pas de métadonnées
BON : Archive activée, structured data, sitemap XML
Conclusion
La maîtrise des Custom Post Types et des taxonomies est essentielle pour créer des sites WordPress structurés et performants en 2025. En combinant une architecture solide, des optimisations de performance et une intégration SEO appropriée, vous créerez des solutions évolutives et maintenables.
Points clés :
Ressources supplémentaires
Mots-clés: Custom Post Types WordPress, CPT WordPress, taxonomies WordPress, WP_Query avancé, post types personnalisés, WordPress metadata, structured data WordPress, WordPress SEO, portfolio WordPress, CPT performance