Come Registrare i Menu di Navigazione in un Tema WordPress: Guida con Codice

By Alberta Trentino

Se stai sviluppando un tema WordPress da zero (o stai mettendo mano a un tema classico ereditato da un altro sviluppatore), prima o poi ti imbatti nella funzione register_nav_menus. È il punto di partenza per dare al cliente un pannello Aspetto > Menu funzionante e per stampare la navigazione nel template senza hardcodare link nell’header.

In questa guida di Mumble Studio vediamo il flusso completo: dichiarazione delle posizioni in functions.php, output con wp_nav_menu(), gestione delle classi CSS generate automaticamente da WordPress, un menu responsive accessibile e infine un Walker personalizzato per controllare l’HTML riga per riga. Tutti gli snippet sono pronti da copiare e adattare.

Cosa fa esattamente register_nav_menus

register_nav_menus() non crea un menu: registra una o più posizioni (theme locations) a cui l’amministratore potrà poi assegnare un menu creato dal backend. In altre parole, il tema dichiara i “contenitori”, il contenuto lo decide chi gestisce il sito. sejuku.net entra nei numeri.

La funzione, inoltre, aggiunge automaticamente il supporto add_theme_support( 'menus' ): non serve dichiararlo separatamente.

Funzione Quando usarla Parametro
register_nav_menus() Registra più posizioni in una sola chiamata (il caso più comune) Array associativo slug => etichetta
register_nav_menu() Registra una singola posizione $location, $description
unregister_nav_menu() Rimuove una posizione di un tema padre da un child theme $location
has_nav_menu() Verifica se a una posizione è stato assegnato un menu $location
wordpress menu code

Passo 1: dichiarare i menu in functions.php

La registrazione va sempre agganciata all’hook after_setup_theme. Farlo fuori da un hook funziona quasi sempre, ma è una pessima pratica: in un child theme rischi problemi di ordine di caricamento e di traduzione delle stringhe.

<?php
/**
 * Registra le posizioni dei menu del tema.
 */
function mumble_register_menus() {

    register_nav_menus(
        array(
            'primary' => esc_html__( 'Menu principale', 'mumble' ),
            'footer'  => esc_html__( 'Menu footer', 'mumble' ),
            'legal'   => esc_html__( 'Menu legale (privacy, cookie)', 'mumble' ),
            'social'  => esc_html__( 'Menu social', 'mumble' ),
        )
    );
}
add_action( 'after_setup_theme', 'mumble_register_menus' );

Regole pratiche sugli slug:

  • usa solo lettere minuscole, numeri e trattini: primary, footer-1, menu-servizi;
  • non cambiarli dopo la messa online: se rinomini uno slug, l’associazione salvata nel database si perde e il menu “sparisce” dal front end;
  • l’etichetta (il secondo valore) è quella che vede il cliente in Aspetto > Menu: scrivila in modo comprensibile, non tecnico.

Registrare una sola posizione

add_action( 'after_setup_theme', function() {
    register_nav_menu( 'primary', esc_html__( 'Menu principale', 'mumble' ) );
} );

Rimuovere una posizione dal tema padre

add_action( 'after_setup_theme', 'mumble_child_unregister', 11 );
function mumble_child_unregister() {
    unregister_nav_menu( 'social' );
}

Nota la priorità 11: deve girare dopo la registrazione del tema padre.

Passo 2: assegnare il menu dal backend

Dopo aver salvato functions.php, il cliente (o tu) deve completare l’operazione lato amministrazione:

  1. Vai su Aspetto > Menu.
  2. Clicca su Crea un nuovo menu, assegna un nome (es. “Navigazione sito”) e salva.
  3. Aggiungi pagine, articoli, categorie o link personalizzati.
  4. Nel box Impostazioni menu spunta la posizione registrata (es. “Menu principale”).
  5. Salva il menu.

Se il box delle posizioni è vuoto, la registrazione non è andata a buon fine: controlla di aver modificato il functions.php del tema attivo. C’è altro in The WordPress Navigation Menu: How It Works.

wordpress menu code

Passo 3: stampare il menu nel template con wp_nav_menu

Nel file header.php (o in un template part) richiami la posizione tramite l’argomento theme_location:

<?php if ( has_nav_menu( 'primary' ) ) : ?>

    <nav class="main-nav" aria-label="<?php esc_attr_e( 'Navigazione principale', 'mumble' ); ?>">
        <?php
        wp_nav_menu(
            array(
                'theme_location' => 'primary',
                'container'      => false,
                'menu_id'        => 'primary-menu',
                'menu_class'     => 'nav-list',
                'depth'          => 2,
                'fallback_cb'    => false,
            )
        );
        ?>
    </nav>

<?php endif; ?>

Gli argomenti che servono davvero

Argomento Default A cosa serve
theme_location vuoto Slug registrato con register_nav_menus
menu vuoto Forza un menu specifico per ID, slug o nome (utile per output condizionali)
container div Tag wrapper. Metti false se il <nav> lo scrivi a mano
menu_class / menu_id menu Classe e id applicati all’<ul>
depth 0 Livelli stampati. 1 disabilita i sottomenu
fallback_cb wp_page_menu Cosa mostrare se nessun menu è assegnato. Con false non stampa nulla
walker Walker_Nav_Menu Classe che genera il markup (vedi sotto)
echo true Con false restituisce la stringa invece di stamparla

Le classi CSS generate da WordPress

Ogni <li> riceve un set di classi automatiche. Conoscerle evita di scrivere CSS a tentativi:

Classe Significato
menu-item Presente su tutte le voci
menu-item-123 ID univoco della voce di menu
menu-item-type-post_type Tipo di oggetto collegato (post_type, taxonomy, custom)
menu-item-object-page Oggetto specifico (page, post, category…)
menu-item-has-children La voce ha un sottomenu: fondamentale per i dropdown
current-menu-item Pagina attualmente visualizzata
current-menu-parent / current-menu-ancestor Genitore o antenato della pagina corrente
menu-item-home Voce che punta alla home page

Ripulire le classi con il filtro nav_menu_css_class

Sulle strutture grandi WordPress stampa 6-7 classi per voce, con un HTML pesantissimo. Se lavori con una metodologia BEM o utility-first, puoi filtrare l’output:

add_filter( 'nav_menu_css_class', 'mumble_clean_menu_classes', 10, 3 );
function mumble_clean_menu_classes( $classes, $item, $args ) {

    if ( empty( $args->theme_location ) || 'primary' !== $args->theme_location ) {
        return $classes;
    }

    $allowed = array(
        'menu-item-has-children',
        'current-menu-item',
        'current-menu-ancestor',
    );

    $classes = array_intersect( $classes, $allowed );
    $classes[] = 'nav-list__item';

    return $classes;
}

Filtri gemelli utili: nav_menu_item_id (rimuove gli id menu-item-123), nav_menu_link_attributes (aggiunge attributi al tag <a>) e nav_menu_submenu_css_class (classi dell’<ul> figlio).

add_filter( 'nav_menu_link_attributes', function( $atts, $item, $args, $depth ) {
    $atts['class'] = 'nav-link nav-link--level-' . $depth;

    if ( in_array( 'current-menu-item', (array) $item->classes, true ) ) {
        $atts['aria-current'] = 'page';
    }

    return $atts;
}, 10, 4 );

Menu responsive: markup, CSS e toggle accessibile

Il pattern che usiamo più spesso nei progetti: un bottone hamburger con aria-expanded, il menu che diventa off-canvas sotto i 992px e i sottomenu che si aprono a click su mobile e a hover su desktop.

1. Il markup nell’header

<header class="site-header">

    <button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
        <span class="menu-toggle__bar"></span>
        <span class="screen-reader-text"><?php esc_html_e( 'Apri il menu', 'mumble' ); ?></span>
    </button>

    <nav id="site-navigation" class="main-nav" aria-label="<?php esc_attr_e( 'Principale', 'mumble' ); ?>">
        <?php
        wp_nav_menu(
            array(
                'theme_location' => 'primary',
                'container'      => false,
                'menu_id'        => 'primary-menu',
                'menu_class'     => 'nav-list',
                'depth'          => 2,
                'fallback_cb'    => false,
            )
        );
        ?>
    </nav>

</header>

2. Il CSS essenziale

.menu-toggle { display: block; }

.nav-list {
    display: none;
    list-style: none;
    margin: 0;
    padding: 0;
}

.main-nav.is-open .nav-list { display: block; }

.nav-list .sub-menu {
    display: none;
    list-style: none;
    padding-left: 1rem;
}

.nav-list .menu-item-has-children.is-open > .sub-menu { display: block; }

@media (min-width: 992px) {

    .menu-toggle { display: none; }

    .nav-list {
        display: flex;
        gap: 1.5rem;
    }

    .nav-list .sub-menu {
        position: absolute;
        min-width: 220px;
        padding: .5rem 0;
        background: #fff;
        box-shadow: 0 8px 24px rgba(0,0,0,.12);
    }

    .nav-list .menu-item-has-children:hover > .sub-menu,
    .nav-list .menu-item-has-children:focus-within > .sub-menu { display: block; }
}

3. Il JavaScript (vanilla, senza dipendenze)

document.addEventListener( 'DOMContentLoaded', function () {

    var toggle = document.querySelector( '.menu-toggle' );
    var nav    = document.querySelector( '.main-nav' );

    if ( toggle && nav ) {
        toggle.addEventListener( 'click', function () {
            var open = toggle.getAttribute( 'aria-expanded' ) === 'true';
            toggle.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
            nav.classList.toggle( 'is-open' );
        } );
    }

    document.querySelectorAll( '.submenu-toggle' ).forEach( function ( btn ) {
        btn.addEventListener( 'click', function () {
            var parent = btn.closest( '.menu-item-has-children' );
            var open   = btn.getAttribute( 'aria-expanded' ) === 'true';
            btn.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
            parent.classList.toggle( 'is-open' );
        } );
    } );
} );

Ricorda di accodare lo script correttamente:

add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script(
        'mumble-navigation',
        get_theme_file_uri( '/assets/js/navigation.js' ),
        array(),
        wp_get_theme()->get( 'Version' ),
        true
    );
} );
wordpress menu code

Walker personalizzato: controllo totale sull’HTML

I filtri coprono l’80% dei casi. Quando serve una struttura diversa (bottoni di apertura sottomenu, icone, descrizioni delle voci, wrapper interni al link) si estende la classe Walker_Nav_Menu.

<?php
/**
 * Walker personalizzato per il menu principale.
 * File: /inc/class-mumble-walker-nav-menu.php
 */
class Mumble_Walker_Nav_Menu extends Walker_Nav_Menu {

    /* Apertura del sottomenu */
    public function start_lvl( &$output, $depth = 0, $args = null ) {
        $output .= '<ul class="sub-menu sub-menu--level-' . ( $depth + 1 ) . '">';
    }

    /* Chiusura del sottomenu */
    public function end_lvl( &$output, $depth = 0, $args = null ) {
        $output .= '</ul>';
    }

    /* Apertura della singola voce */
    public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {

        $classes   = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'nav-list__item';

        $has_children = in_array( 'menu-item-has-children', $classes, true );
        $is_current   = in_array( 'current-menu-item', $classes, true );

        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

        $output .= '<li' . $class_names . '>';

        $atts = array(
            'href'         => ! empty( $item->url ) ? $item->url : '#',
            'title'        => ! empty( $item->attr_title ) ? $item->attr_title : '',
            'target'       => ! empty( $item->target ) ? $item->target : '',
            'rel'          => ! empty( $item->xfn ) ? $item->xfn : '',
            'class'        => 'nav-link nav-link--depth-' . $depth,
            'aria-current' => $is_current ? 'page' : '',
        );

        $atts       = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
        $attributes = '';

        foreach ( $atts as $attr => $value ) {
            if ( '' !== $value && false !== $value ) {
                $attributes .= ' ' . $attr . '="' . esc_attr( $value ) . '"';
            }
        }

        $title = apply_filters( 'the_title', $item->title, $item->ID );

        $item_output  = '<a' . $attributes . '><span class="nav-link__label">' . $title . '</span>';

        if ( ! empty( $item->description ) && 0 === $depth ) {
            $item_output .= '<span class="nav-link__desc">' . esc_html( $item->description ) . '</span>';
        }

        $item_output .= '</a>';

        /* Bottone accessibile per aprire il sottomenu su mobile */
        if ( $has_children ) {
            $item_output .= '<button type="button" class="submenu-toggle" aria-expanded="false">'
                . '<span class="screen-reader-text">' . esc_html__( 'Apri sottomenu', 'mumble' ) . '</span>'
                . '</button>';
        }

        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

    /* Chiusura della singola voce */
    public function end_el( &$output, $item, $depth = 0, $args = null ) {
        $output .= '</li>';
    }
}

Includi il file e passa la classe a wp_nav_menu():

require_once get_theme_file_path( '/inc/class-mumble-walker-nav-menu.php' );

wp_nav_menu(
    array(
        'theme_location' => 'primary',
        'container'      => false,
        'menu_class'     => 'nav-list',
        'depth'          => 2,
        'fallback_cb'    => false,
        'walker'         => new Mumble_Walker_Nav_Menu(),
    )
);

Attenzione: le descrizioni delle voci sono visibili nel backend solo attivando la casella “Descrizione” dal pannello Opzioni schermata, in alto a destra nella schermata dei menu.

Bonus: recuperare gli elementi del menu senza wp_nav_menu

Se devi costruire una navigazione dentro un componente custom, un blocco o una risposta REST, ti serve l’array grezzo delle voci:

$locations = get_nav_menu_locations();

if ( isset( $locations['primary'] ) ) {

    $menu_obj = wp_get_nav_menu_object( $locations['primary'] );
    $items    = wp_get_nav_menu_items( $menu_obj->term_id );

    foreach ( $items as $item ) {
        // $item->title, $item->url, $item->menu_item_parent, $item->object_id
    }
}

// Nome leggibile del menu assegnato a una posizione
$nome = wp_get_nav_menu_name( 'primary' );

Per recuperare un menu tramite il suo nome invece che tramite posizione basta passare la stringa: wp_get_nav_menu_items( 'Navigazione sito' ).

wordpress menu code

Problemi frequenti e come risolverli

Sintomo Causa tipica Soluzione
La posizione non appare in Aspetto > Menu Codice nel tema sbagliato o hook mancante Verifica il tema attivo e usa after_setup_theme
Vengono stampate tutte le pagine del sito Nessun menu assegnato, entra il fallback wp_page_menu Imposta 'fallback_cb' => false o assegna il menu
Il menu è sparito dopo un aggiornamento del tema Slug della location rinominato Ripristina lo slug originale o riassegna il menu
Errore fatale sul Walker Firma dei metodi non compatibile con il core Mantieni parametri e riferimenti &$output identici alla classe padre
I sottomenu non si aprono su desktop depth impostato a 1 Porta depth a 2 o 0

E nei temi a blocchi (Full Site Editing)?

Nei temi a blocchi la navigazione è gestita dal blocco Navigazione e i menu classici non vengono usati per l’header. Tuttavia register_nav_menus resta indispensabile in tre scenari molto concreti:

  • temi classici su misura, ancora la scelta più solida per progetti con logiche PHP complesse;
  • temi ibridi, dove header e footer restano in PHP mentre i contenuti usano i blocchi;
  • plugin e blocchi custom che devono leggere una struttura di menu gestibile dal cliente senza toccare l’editor.

Se stai migrando un tema classico verso i blocchi, tieni presente che il blocco Navigazione può importare un menu esistente, quindi le voci create in Aspetto > Menu non vanno perse. Vedi https://wpmudev.com.

Checklist finale

  1. Registra le posizioni con register_nav_menus() dentro after_setup_theme.
  2. Usa slug stabili e etichette comprensibili per il cliente.
  3. Proteggi l’output con has_nav_menu() e disattiva il fallback.
  4. Sfrutta menu-item-has-children e current-menu-item per il CSS invece di inventare classi.
  5. Prima prova con i filtri, poi (solo se serve) scrivi un Walker.
  6. Aggiungi sempre aria-expanded, aria-label e aria-current: un menu non accessibile è un menu incompleto.

FAQ

Qual è la differenza tra register_nav_menu e register_nav_menus?

La prima registra una sola posizione e accetta due parametri (slug e descrizione), la seconda ne registra molte in una volta tramite un array associativo. A livello di funzionamento sono identiche: register_nav_menus() internamente cicla sull’array.

Devo aggiungere anche add_theme_support( ‘menus’ )?

No. Sia register_nav_menu() sia register_nav_menus() attivano automaticamente il supporto ai menu per il tema.

Come faccio a mostrare un menu diverso in base alla pagina?

Puoi registrare più posizioni e scegliere quale stampare con una condizione PHP, oppure usare l’argomento menu di wp_nav_menu() passando ID, slug o nome del menu desiderato.

Perché WordPress aggiunge tante classi ai miei li?

Servono a identificare tipo di contenuto e stato della voce (corrente, genitore, con figli). Se ti pesano, filtrale con nav_menu_css_class mantenendo almeno menu-item-has-children e current-menu-item.

Posso creare un menu via codice, senza passare dal backend?

Sì, con wp_create_nav_menu(), wp_update_nav_menu_item() e set_theme_mod( 'nav_menu_locations', ... ). È una soluzione tipica degli script di setup iniziale, ma va eseguita una sola volta con un controllo su wp_get_nav_menu_object() per evitare duplicati a ogni caricamento.

Il Walker rallenta il sito?

No, l’impatto è trascurabile: la generazione del menu avviene in memoria dopo una query molto leggera. Se hai centinaia di voci, valuta piuttosto una cache dell’output con 'echo' => false e un transient.


Hai bisogno di un tema WordPress su misura, veloce e mantenibile? In Mumble Studio sviluppiamo temi custom con architetture pulite, navigazioni accessibili e codice documentato. Parliamo del tuo progetto.

Leave a Comment