# Menus WordPress en headless : les exposer à un front découplé

> L'API REST de WordPress n'expose pas les menus par défaut. Voici comment les récupérer proprement pour construire la navigation d'un front découplé.

- Auteur : WordPress Développement
- Publié le : 2020-04-02
- Mis à jour le : 2026-09-30
- Catégorie : Headless &amp; API
- URL : https://www.wpmoderne.fr/headless/menus-wordpress-headless-exposer-front-decouple/

## L’essentiel

- Menus absents de l'API REST native
- Deux solutions : extension ou route maison
- Structure hiérarchique à reconstruire côté front

Un client m'a demandé, il y a quelques semaines, pourquoi son menu de navigation n'apparaissait nulle part dans les réponses de l'API REST de son site WordPress. La réponse est simple : contrairement aux articles, aux pages ou aux catégories, les menus de navigation ne font partie d'aucune route native. WordPress 5.4 gère les menus comme un objet interne à l'administration, pensé pour `wp_nav_menu()` et le thème, pas pour une consommation externe.

Sur un projet headless, ce trou est bloquant dès la page d'accueil. Un front découplé a besoin de connaître la structure du menu principal, ses libellés, ses liens et sa hiérarchie éventuelle (sous-menus). Deux approches s'offrent à vous : passer par une extension existante, ou écrire votre propre route REST. Je détaille les deux, avec leurs compromis respectifs.

## Pourquoi les menus ne sont pas exposés nativement

Techniquement, un menu WordPress repose sur une taxonomie interne (`nav_menu`) et un type de contenu (`nav_menu_item`), tous deux marqués `public` à `false` et `show_in_rest` à `false` dans le cœur. Ce choix n'est pas un oubli : les éléments de menu contiennent des méta-données complexes (profondeur, parent, type de cible — page, article, lien personnalisé, catégorie) que l'équipe cœur n'a jamais jugé prioritaire de normaliser pour une API publique.

Résultat concret : interroger `/wp-json/wp/v2/menu-items` renvoie une erreur 404 sur une installation par défaut. Il faut soit modifier la visibilité de ces objets, soit construire une route qui les traduit en JSON exploitable.

## Solution 1 : l'extension WP REST API Menus

La solution la plus rapide consiste à installer une extension dédiée, comme WP REST API Menus. Une fois activée, elle ajoute une route du type `/wp-json/menus/v1/menus/{slug}` qui renvoie l'arborescence complète du menu demandé, avec pour chaque élément son titre, son URL, son identifiant de parent et ses enfants imbriqués.

> L'essentiel à retenir : Menus absents de l'API REST native ; Deux solutions : extension ou route maison ; Structure hiérarchique à reconstruire côté front

Cette approche convient très bien pour un site vitrine ou un blog dont le menu ne change pas souvent. Elle a toutefois deux limites que j'ai rencontrées en production : la structure de réponse dépend entièrement de l'extension (donc de sa maintenance), et il faut connaître à l'avance le `slug` du menu enregistré via `register_nav_menu()`.

### Exemple de réponse

```
{
  "ID": 12,
  "items": [
    {
      "title": "Accueil",
      "url": "https://exemple.fr/",
      "object_id": "2",
      "object": "page",
      "child_items": []
    },
    {
      "title": "Blog",
      "url": "https://exemple.fr/blog/",
      "object_id": "8",
      "object": "page",
      "child_items": []
    }
  ]
}
```

## Solution 2 : une route REST personnalisée avec register_rest_route

Sur les projets où je maîtrise le thème, je préfère écrire ma propre route plutôt que d'ajouter une dépendance externe. La fonction `wp_get_nav_menu_items()` retourne les éléments bruts d'un menu à partir de son identifiant ou de son emplacement enregistré ; il suffit de les reformater.

Voici une première version de la route. Elle se déclare sur le crochet `rest_api_init`, accepte un paramètre `slug` dans l'URL et renvoie la liste à plat des éléments du menu, sans aucune reconstruction de hiérarchie : ce travail est laissé au front, comme l'annonce le troisième point clé de cet article.

```
add_action( 'rest_api_init', function () {
    register_rest_route( 'wpmoderne/v1', '/menus/(?P<slug>[a-z0-9_-]+)', array(
        'methods'             => 'GET',
        'callback'            => 'wpmoderne_menu_rest',
        'permission_callback' => '__return_true',
    ) );
} );

function wpmoderne_menu_rest( WP_REST_Request $request ) {
    $items = wp_get_nav_menu_items( $request['slug'] );

    if ( false === $items ) {
        return new WP_Error(
            'menu_introuvable',
            'Aucun menu ne correspond à cet identifiant.',
            array( 'status' => 404 )
        );
    }

    $resultat = array();
    foreach ( $items as $item ) {
        $resultat[] = array(
            'id'     => (int) $item->ID,
            'parent' => (int) $item->menu_item_parent,
            'ordre'  => (int) $item->menu_order,
            'titre'  => $item->title,
            'url'    => $item->url,
            'type'   => $item->object,
            'cible'  => (int) $item->object_id,
        );
    }

    return rest_ensure_response( $resultat );
}
```

Quelques précisions sur ce code. `wp_get_nav_menu_items()` renvoie `false` lorsque le menu n'existe pas, et un tableau vide lorsqu'il existe mais ne contient rien : les deux cas doivent être distingués, d'où l'erreur 404 explicite. Le champ `menu_item_parent` est une chaîne de caractères ; le convertir en entier évite de mauvaises surprises lors des comparaisons strictes en JavaScript. Enfin, `permission_callback` vaut ici `__return_true` parce qu'un menu de navigation est par nature une information publique : le déclarer explicitement documente ce choix au lieu de le laisser implicite.

## Retrouver un menu par son emplacement plutôt que par son identifiant court

Le `slug` d'un menu se choisit à la main dans l'administration et peut changer si un éditeur le renomme. Pour un front découplé, il est plus stable de s'appuyer sur l'emplacement déclaré par le thème avec `register_nav_menus()` (par exemple `principal` ou `pied-de-page`). La fonction `get_nav_menu_locations()` renvoie un tableau associant chaque emplacement à l'identifiant du menu qui lui est affecté.

```
function wpmoderne_menu_par_emplacement( $emplacement ) {
    $emplacements = get_nav_menu_locations();

    if ( empty( $emplacements[ $emplacement ] ) ) {
        return false;
    }

    return wp_get_nav_menu_items( (int) $emplacements[ $emplacement ] );
}
```

Dans la route, il suffit alors d'appeler cette fonction avec `principal` : le front demande « le menu principal » sans connaître le nom que les éditeurs lui ont donné. Si aucun menu n'est affecté à l'emplacement, la fonction renvoie `false` et la route peut répondre par une erreur ou par une liste vide, selon le comportement souhaité côté interface.

## Reconstruire la hiérarchie côté front

La route renvoie une liste plate, avec pour chaque élément l'identifiant de son parent. Transformer cette liste en arbre tient en quelques lignes de JavaScript : on indexe les éléments par identifiant, puis on rattache chacun à son parent.

```
function construireArbre( elements ) {
  const parIdentifiant = new Map();
  elements.forEach( ( e ) => parIdentifiant.set( e.id, { ...e, enfants: [] } ) );

  const racines = [];
  parIdentifiant.forEach( ( noeud ) => {
    const parent = parIdentifiant.get( noeud.parent );
    if ( parent ) {
      parent.enfants.push( noeud );
    } else {
      racines.push( noeud );
    }
  } );

  return racines;
}

fetch( 'https://exemple.fr/wp-json/wpmoderne/v1/menus/principal' )
  .then( ( reponse ) => reponse.json() )
  .then( ( elements ) => console.log( construireArbre( elements ) ) );
```

Comme `wp_get_nav_menu_items()` trie les éléments selon leur ordre dans le menu, l'ordre relatif des enfants est conservé sans traitement supplémentaire. Un élément dont le parent est absent de la liste devient une racine, ce qui évite de perdre un lien si la structure est incohérente.

## Extension ou route maison : le comparatif

Les deux approches se valent techniquement ; ce sont le contexte du projet et la capacité de l'équipe à maintenir du code qui tranchent.

- **Extension :** mise en place en quelques minutes, réponse déjà hiérarchisée, mais format imposé, dépendance à sa maintenance et à ses mises à jour.
- **Route maison :** format de réponse entièrement maîtrisé, aucune dépendance supplémentaire, possibilité d'ajouter des champs (classes CSS, cible du lien, champs personnalisés), mais du code à tester et à faire évoluer.
- **Critère de choix :** un site vitrine au menu stable s'accommode très bien de l'extension ; un projet au front exigeant, ou une agence qui reproduit le même schéma sur plusieurs sites, gagne à posséder sa route.

## Un cas concret : menu principal et pied de page

Sur un site dont le front est construit avec un générateur de pages statiques, la navigation est récupérée une seule fois au moment de la construction du site, puis figée dans le code généré. Le menu principal et le menu du pied de page passent par la même route, avec deux emplacements différents. Cette organisation évite d'écrire deux routes et garantit que les éditeurs continuent de gérer leurs menus depuis l'écran habituel de l'administration, sans jamais toucher au code du front.

> Un menu, c'est de la donnée éditoriale : tant que l'éditeur ne peut pas le modifier sans développeur, le découplage n'a rien simplifié.

## Les pièges à éviter

**Les URL absolues.** Les liens renvoyés par WordPress pointent vers le domaine de l'administration, par exemple `https://admin.exemple.fr/blog/`. Si le front est servi depuis un autre domaine, il faut les convertir en chemins relatifs avant de les afficher, sous peine d'envoyer les visiteurs vers le site d'administration. Un appel à `wp_parse_url( $item->url, PHP_URL_PATH )` dans la route règle le problème à la source.

**Le cache.** Reconstruire la réponse à chaque requête est inutile pour un menu qui change rarement. Un transitoire (`set_transient()`) qui conserve le résultat, supprimé par `delete_transient()` sur le crochet `wp_update_nav_menu`, évite des requêtes en base superflues tout en reflétant immédiatement une modification de l'éditeur.

**Les règles CORS.** Si le front interroge la route depuis le navigateur et non depuis un serveur, le domaine du front doit être autorisé à lire la réponse. À défaut, la requête échoue avec une erreur qui n'a aucun rapport apparent avec le menu.

**Les éléments particuliers.** Un lien personnalisé n'a pas d'objet cible, et une catégorie ne se traduit pas en route du front de la même façon qu'une page. Prévoyez dans le front une table de correspondance entre le champ `type` de la réponse et la route réellement affichée.

## En résumé

Les menus WordPress ne sont pas exposés par l'API REST native, et ce n'est pas un défaut à contourner avec de la ruse : c'est un manque à combler explicitement. Une extension dédiée le comble en quelques minutes ; une route écrite avec `register_rest_route()` et `wp_get_nav_menu_items()` le comble avec un format que vous maîtrisez. Dans les deux cas, la hiérarchie se reconstruit côté front à partir de l'identifiant du parent. Choisissez l'extension pour la rapidité, la route maison pour la durée, et mettez le résultat en cache dès que le site prend de l'ampleur.
