Les Custom Post Types : structurer le contenu au-delà des articles et pages

WordPress propose nativement deux types de contenu : les articles (posts) et les pages. Les Custom Post Types (CPT) permettent de créer des types de contenu supplémentaires adaptés à la nature spécifique des données d'un projet. Un site portfolio aura un type "Projet", un site immobilier un type "Bien", un site e-commerce un type "Produit".

Chaque CPT dispose de son propre espace dans le back-office WordPress, de ses propres taxonomies et de ses propres champs personnalisés. En architecture headless, les CPT sont exposés via l'API REST ou WPGraphQL, ce qui permet au frontend de les consommer comme n'importe quelle autre ressource.

Enregistrer un Custom Post Type compatible headless

L'enregistrement d'un CPT se fait via la fonction register_post_type() dans le fichier functions.php du thème ou dans un plugin personnalisé. Pour qu'un CPT soit accessible via l'API REST, le paramètre show_in_rest doit être défini à true.

// functions.php ou plugin personnalisé
add_action('init', function () {
  register_post_type('projet', [
    'labels' => [
      'name'               => 'Projets',
      'singular_name'      => 'Projet',
      'add_new'            => 'Ajouter un projet',
      'add_new_item'       => 'Ajouter un nouveau projet',
      'edit_item'          => 'Modifier le projet',
      'view_item'          => 'Voir le projet',
      'all_items'          => 'Tous les projets',
      'search_items'       => 'Rechercher des projets',
      'not_found'          => 'Aucun projet trouvé',
    ],
    'public'       => true,
    'has_archive'  => true,
    'menu_icon'    => 'dashicons-portfolio',
    'supports'     => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],

    // Paramètres essentiels pour le headless
    'show_in_rest'          => true,      // Expose le CPT dans l'API REST
    'rest_base'             => 'projets', // Personnalise le slug de l'endpoint REST
    'show_in_graphql'       => true,      // Expose le CPT dans WPGraphQL
    'graphql_single_name'   => 'projet',  // Nom singulier pour le schéma GraphQL
    'graphql_plural_name'   => 'projets', // Nom pluriel pour le schéma GraphQL
  ]);
});

show_in_rest est obligatoire

Sans show_in_rest => true, le CPT n'apparaît pas dans l'API REST et l'éditeur Gutenberg n'est pas disponible pour ce type de contenu. Ce paramètre est indispensable en architecture headless. Pour WPGraphQL, ajoutez également show_in_graphql => true.

Une fois enregistré, le CPT est accessible :

  • Via REST : GET /wp-json/wp/v2/projets
  • Via GraphQL : query { projets { nodes { title slug } } }

Taxonomies personnalisées

Les taxonomies permettent de classer le contenu. WordPress fournit les catégories et les étiquettes pour les articles. Pour les CPT, des taxonomies personnalisées sont souvent nécessaires.

add_action('init', function () {
  register_taxonomy('technologie', ['projet'], [
    'labels' => [
      'name'          => 'Technologies',
      'singular_name' => 'Technologie',
      'add_new_item'  => 'Ajouter une technologie',
    ],
    'hierarchical'          => false, // false = étiquettes, true = catégories
    'show_in_rest'          => true,
    'rest_base'             => 'technologies',
    'show_in_graphql'       => true,
    'graphql_single_name'   => 'technologie',
    'graphql_plural_name'   => 'technologies',
  ]);

  register_taxonomy('type_projet', ['projet'], [
    'labels' => [
      'name'          => 'Types de projet',
      'singular_name' => 'Type de projet',
    ],
    'hierarchical'          => true,
    'show_in_rest'          => true,
    'show_in_graphql'       => true,
    'graphql_single_name'   => 'typeProjet',
    'graphql_plural_name'   => 'typeProjets',
  ]);
});

Advanced Custom Fields (ACF) : enrichir la structure de contenu

ACF permet d'ajouter des champs personnalisés à n'importe quel type de contenu WordPress. En mode headless, ces champs structurent les données de manière précise et typée, ce qui facilite leur consommation par le frontend.

Types de champs ACF courants

Créer un groupe de champs pour le CPT Projet

Dans le back-office WordPress, accédez à ACF > Groupes de champs > Ajouter. Créez un groupe "Détails du projet" avec les champs suivants :

| Nom du champ | Clé | Type | Description | |---|---|---|---| | Client | client | Text | Nom du client ou de l'entreprise | | Année | annee | Number | Année de réalisation | | URL du projet | url_projet | URL | Lien vers le site en production | | Technologies | technologies_utilisees | Checkbox | Liste des technologies utilisées | | Description détaillée | description_detaillee | Wysiwyg | Description longue du projet | | Galerie | galerie | Gallery | Captures d'écran du projet | | Témoignage client | temoignage | Group | Groupe contenant texte + nom + poste |

Assignez ce groupe au type de contenu "Projet" via la règle de localisation d'ACF.

Exposer ACF dans l'API REST

Par défaut, les champs ACF ne sont pas inclus dans les réponses de l'API REST. Deux méthodes permettent de les exposer.

Méthode 1 : option native ACF (ACF PRO 5.11+)

Dans les réglages du groupe de champs ACF, activez l'option "Show in REST API". Les champs apparaissent alors dans un objet acf dans la réponse JSON.

{
  "id": 42,
  "title": { "rendered": "Refonte site e-commerce" },
  "slug": "refonte-site-ecommerce",
  "acf": {
    "client": "Boutique Mode",
    "annee": 2024,
    "url_projet": "https://boutique-mode.com",
    "technologies_utilisees": ["Next.js", "WordPress", "WooCommerce"],
    "description_detaillee": "<p>Refonte complète du site...</p>"
  }
}

Méthode 2 : exposition manuelle via register_rest_field

add_action('rest_api_init', function () {
  register_rest_field('projet', 'projet_meta', [
    'get_callback' => function ($post) {
      return [
        'client'        => get_field('client', $post['id']),
        'annee'         => get_field('annee', $post['id']),
        'url_projet'    => get_field('url_projet', $post['id']),
        'technologies'  => get_field('technologies_utilisees', $post['id']),
      ];
    },
    'schema' => [
      'type'        => 'object',
      'description' => 'Métadonnées du projet',
    ],
  ]);
});

Exposer ACF dans WPGraphQL

Le plugin WPGraphQL for ACF expose automatiquement les champs ACF dans le schéma GraphQL. Après installation et activation, les champs sont immédiatement requêtables.

query ProjetComplet($slug: ID!) {
  projet(id: $slug, idType: SLUG) {
    title
    slug
    featuredImage {
      node {
        sourceUrl
        altText
      }
    }
    projetDetails {
      client
      annee
      urlProjet
      technologiesUtilisees
      descriptionDetaillee
      galerie {
        nodes {
          sourceUrl
          altText
          mediaDetails {
            width
            height
          }
        }
      }
      temoignage {
        texte
        nom
        poste
      }
    }
  }
}

Nommage des champs ACF dans GraphQL

WPGraphQL convertit automatiquement les clés ACF en camelCase. Le champ url_projet devient urlProjet, technologies_utilisees devient technologiesUtilisees. Utilisez des clés ACF en snake_case côté WordPress pour garantir des noms GraphQL lisibles.

Les champs Repeater et Flexible Content

Repeater : listes répétables

Le champ Repeater crée une liste d'éléments ayant la même structure. Exemple : les étapes d'un projet.

// Champ Repeater "etapes" avec sous-champs "titre_etape" et "description_etape"

Requête GraphQL :

query {
  projet(id: "mon-projet", idType: SLUG) {
    projetDetails {
      etapes {
        titreEtape
        descriptionEtape
      }
    }
  }
}

Flexible Content : contenu modulaire

Le champ Flexible Content fonctionne comme un page builder : il définit plusieurs modèles de blocs (layouts) que le rédacteur peut assembler dans l'ordre souhaité. Chaque layout a ses propres sous-champs.

// Flexible Content "sections_page" avec layouts :
// - "hero" (titre, sous_titre, image_fond)
// - "texte_image" (texte, image, position)
// - "grille_stats" (statistiques: repeater de valeur + label)
// - "temoignages" (temoignages: repeater de texte + auteur)

Côté frontend, le rendu s'effectue avec un switch sur le type de layout :

function PageSections({ sections }) {
  return sections.map((section, index) => {
    switch (section.fieldGroupName) {
      case 'Page_Sections_Hero':
        return <HeroSection key={index} {...section} />;
      case 'Page_Sections_TexteImage':
        return <TexteImageSection key={index} {...section} />;
      case 'Page_Sections_GrilleStats':
        return <StatsGrid key={index} {...section} />;
      case 'Page_Sections_Temoignages':
        return <Temoignages key={index} {...section} />;
      default:
        return null;
    }
  });
}

Bonnes pratiques de structuration

Nommer les champs en snake_case

Utilisez des clés explicites en snake_case (date_debut, url_externe, nombre_participants). Ces clés seront automatiquement converties en camelCase par WPGraphQL et resteront lisibles dans l'API REST.

Retourner les ID pour les images

Configurez les champs Image ACF pour retourner l'ID (et non l'URL ou le tableau). Cela permet au frontend de récupérer toutes les tailles disponibles via l'API et d'utiliser le composant next/image avec les dimensions correctes.

Documenter les groupes de champs

Ajoutez des descriptions à chaque champ ACF. Ces descriptions apparaissent dans le schéma GraphQL et servent de documentation pour les développeurs frontend.

Séparer les champs de contenu et de configuration

Créez des groupes de champs distincts pour le contenu éditorial (textes, images) et la configuration technique (options d'affichage, paramètres de mise en page). Cela clarifie le rôle de chaque champ pour les rédacteurs.

Exemple complet : CPT Projet pour un portfolio

Côté Next.js, le typage TypeScript garantit la cohérence entre les données WordPress et le frontend :

interface Projet {
  title: string;
  slug: string;
  featuredImage: {
    node: {
      sourceUrl: string;
      altText: string;
    };
  };
  projetDetails: {
    client: string;
    annee: number;
    urlProjet: string;
    technologiesUtilisees: string[];
    descriptionDetaillee: string;
    galerie: {
      nodes: Array<{
        sourceUrl: string;
        altText: string;
        mediaDetails: { width: number; height: number };
      }>;
    };
  };
  technologies: {
    nodes: Array<{ name: string; slug: string }>;
  };
}