Кастомизация меню WordPress

Меню — сущность в WordPress, которая позволяет управлять навигационным меню проекта. Можно создавать много уровней вложенности и множество меню в разных местах страниц сайта.

Многие считают, что оно ограничено только этим, а функцию wp_nav_menu() нельзя кастомизировать.

Я решил развеять этот миф. Рассмотрим, как можно кастомизировать и расширить меню на примере. Для этого мы будем использовать Carbon Fields, о котором я уже писал здесь.

Основную часть кода нужно разместить в файле functions.php.

Важно помнить, что код нужно разделять на смысловые блоки и, в случае с WP, уже подключать их к functions.php. В конце статьи дам ссылку на репозиторий GitHub, где наглядно будет видно, где какой код можно разместить.

Проектирование меню и постановка задачи

Давайте создадим меню современного интернет-магазина. Пусть это будет магазин бытовой техники. Тогда меню будет иметь такую структуру:

  • Каталог
    • Телевизоры
      • Full HD
      • 4K
    • Стиральные машины
      • Фронтальные
      • Вертикальные
    • Плиты
      • Газовые
      • Электрические
    • Микроволновки
      • С разморозкой
      • В грилем
      • Мощность >700 ВТ
    • Утюги
      • С защитой от перегрева
      • Керамическая подошва
    • Пылесосы
      • Классические
      • Вертикальные
      • Роботы-пылесосы
  • О компании
    • Доставка
    • Способы оплаты
    • Обмен, возврат и гарантия
  • Услуги
    • Доставка
    • Монтаж
  • Опт
  • Новости
  • Контакты

Раздел «Каталог» будет формироваться автоматически из категорий каталога WooCommerce. При этом мы сделаем так, чтобы при наведении на категорию, отображались товары из этой категории, которые укажет администратор сайта. И давайте сделаем так, чтобы у обычного меню второго уровня были иконки. Просто потому что мы можем 🙂

Вёрстка

Чтобы не тратить время на вёрстку, воспользуется готовыми решениями Bootstrap 5. Получится вот так:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">

    <link rel="stylesheet" href="style.css">
    <title>Навигационное меню</title>
</head>
<body>
    <div class="container">

        <nav class="navbar navbar-expand-lg bg-light">
            <div class="container-fluid">
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                        <li class="nav-item">

                            <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                Каталог
                            </a>
                            <div class="dropdown-menu dropdown-catalog">
                                <div class="catalog_menu">
                                    <div class="catalog_menu-flexbox">
                                        <div class="catalog_menu-nav">
                                            <ul> 
                                                <li> 
                                                    <a data-category="#category_1" href="#">Телевизоры</a>
                                                    <ul> 
                                                        <li> <a href="#">Full HD</a></li>
                                                        <li> <a href="#">4K</a></li>
                                                    </ul>
                                                </li>
                                                <li> 
                                                    <a data-category="#category_2" href="#">Стиральные машины</a>
                                                    <ul> 
                                                        <li> <a href="#">Фронтальные</a></li>
                                                        <li> <a href="#">Вертикальные</a></li>
                                                    </ul>
                                                </li>
                                                <li> 
                                                    <a data-category="#category_3" href="#">Плиты</a>
                                                    <ul> 
                                                        <li> <a href="#">Газовые</a></li>
                                                        <li> <a href="#">Электрические</a></li>
                                                    </ul>
                                                </li>
                                                <li> 
                                                    <a data-category="#category_4" href="#">Микроволновки</a>
                                                    <ul> 
                                                        <li> <a href="#">С разморозкой</a></li>
                                                        <li> <a href="#">В грилем</a></li>
                                                        <li> <a href="#">Мощность &gt;700 ВТ</a></li>
                                                    </ul>
                                                </li>
                                                <li> 
                                                    <a data-category="#category_5" href="#">Утюги</a>
                                                    <ul> 
                                                        <li> <a href="#">С защитой от перегрева</a></li>
                                                        <li> <a href="#">Керамическая подошва</a></li>
                                                    </ul>
                                                </li>
                                                <li> 
                                                    <a data-category="#category_6" href="#">Пылесосы</a>
                                                    <ul> 
                                                        <li> <a href="#">Классические</a></li>
                                                        <li> <a href="#">Вертикальные</a></li>
                                                        <li> <a href="#">Роботы-пылесосы</a></li>
                                                    </ul>
                                                </li>
                                            </ul>
                                        </div>
                                        <div class="catalog_menu-products">
                                            <!-- Тут уже сверстаны превью-карточки товаров -->
                                            <div class="catalog_menu-category is-active" id="category_1">
                                                <div class="catalog_menu-products-flexbox">
                                                    <div class="catalog_menu-product">
                
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                    <div class="catalog_menu-product">
                            
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                </div>
                                            </div>

                                            <div class="catalog_menu-category" id="category_2">
                                                <div class="catalog_menu-products-flexbox">
                                                    <div class="catalog_menu-product">
                
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 2</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                    <div class="catalog_menu-product">
                            
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 2</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                </div>
                                            </div>

                                            <div class="catalog_menu-category" id="category_3">
                                                <div class="catalog_menu-products-flexbox">
                                                    <div class="catalog_menu-product">
                
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 3</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                    <div class="catalog_menu-product">
                            
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 3</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                </div>
                                            </div>

                                            <div class="catalog_menu-category" id="category_4">
                                                <div class="catalog_menu-products-flexbox">
                                                    <div class="catalog_menu-product">
                
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 4</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                    <div class="catalog_menu-product">
                            
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 4</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                </div>
                                            </div>

                                            <div class="catalog_menu-category" id="category_5">
                                                <div class="catalog_menu-products-flexbox">
                                                    <div class="catalog_menu-product">
                
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 5</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                    <div class="catalog_menu-product">
                            
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 5</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                </div>
                                            </div>

                                            <div class="catalog_menu-category" id="category_6">
                                                <div class="catalog_menu-products-flexbox">
                                                    <div class="catalog_menu-product">
                
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 6</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                    <div class="catalog_menu-product">
                            
                                                        <div class="card">
                                                            <img src="https://images.unsplash.com/photo-1593784991095-a205069470b6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&q=80" class="card-img-top">
                                                            <div class="card-body">
                                                                <h5 class="card-title">Название товара 6</h5>
                                                                <p class="card-text">Некоторое описание</p>
                                                                <div class="btn-group" role="group">
                                                                    <button type="button" class="btn btn-primary">В корзину</button>
                                                                    <a type="button" class="btn btn-outline-primary">Посмотреть</a>
                                                                </div>
                                                            </div>
                                                        </div>
                            
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                
                                </div>
                            </div>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                О компании
                            </a>
                            <ul class="dropdown-menu">
                                <li><a href="#"  class="dropdown-item"><i class="fa fa-star-o"></i> Доставка</a></li>
                                <li><a href="#"  class="dropdown-item"><i class="fa fa-star-o"></i> Способы оплаты</a></li>
                                <li><a href="#"  class="dropdown-item"><i class="fa fa-star-o"></i> Обмен, возврат и гарантия</a></li>
                            </ul>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                Услуги
                            </a>
                            <ul class="dropdown-menu">
                                <li><a href="#" class="dropdown-item"><i class="fa fa-star-o"></i> Доставка</a></li>
                                <li><a href="#" class="dropdown-item"><i class="fa fa-star-o"></i> Монтаж</a></li>
                            </ul>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Опт</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Новости</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Контакты</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>

    </div>
    <!-- JavaScript Bundle with Popper -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
    <script src="main.js"></script>
</body>
</html>

Конечно, такая нагромаждённая вёрстка меню — редкость. Да и товары в меню сейчас мало кто размещает. Но тут смысл в том, чтобы показать, что можно сделать действительно необычные вещи используя API WordPress, не костылируя костыли 🙂

Добавим стили в style.css:

body{
    font-family: Arial;
}

.catalog_menu-category{
    display: none;
}

.catalog_menu-category.is-active{
    display: block;
}

.catalog_menu-flexbox{
    display: flex;
}

.catalog_menu-nav{
    flex: 0 0 40%;
    max-width: 40%;
}
.catalog_menu-products{
    flex: 0 0 60%;
    max-width: 60%;
}

.catalog_menu-products-flexbox{
    display: flex;
    flex-wrap: wrap;
}

.catalog_menu-nav {
    padding: 30px;
}

.catalog_menu-nav > ul {
    display: flex;
    flex-wrap: wrap;
}

.catalog_menu-nav > ul > li{
    margin-right: 50px;
    margin-bottom: 50px;
}

.catalog_menu-nav > ul > li > a{
    font-weight: 700;
    display: block;
    margin-bottom: 10px;
}

.catalog_menu-nav > ul > li > * a:hover{
    text-decoration: underline;
}

.catalog_menu-nav ul{
    list-style-type: none;
    padding: 0;
}

.catalog_menu-nav a{
    color: #000;
    text-decoration: none;
}

.catalog_menu-product {
    margin-right: 30px;
}

И добавим JS, чтобы при наведении на категорию, показывались товары, которые относятся к ней. Файл main.js:

jQuery(function ($) {
    let active_class = 'is-active';

    $('a[data-category]').on('mouseover', function() {
        let id = $(this).attr('data-category');

        if ( !$(id).hasClass( active_class ) ) {
            $('.catalog_menu-category').removeClass( active_class );
            $(id).addClass( active_class );
        }
    }); 
});

Получилась такая штука:

Кастомизация управления меню: BackEnd

Настройки пунктов меню

Было-бы здорово, чтобы админ мог сам выбирать, из какого пункта будет выводиться меню каталога и какие иконки у какого пункта второго уровня будут установлены.

Для этого будем использовать плагин Carbon Fields. Вы можете скачать его здесь. Также советую ознакомиться с моей статьёй о нём.

Официальный сайт плагина — carbonfields.net

Сначала нужно создать кастомные поля ввода. Чтобы так сделать, укажем такой код:

<?php
/**
 * Если Carbon Fields установлен через Composer - убедитесь, что также подключили его ядро.
 * Если как плагин - то все и так будет работать.
 **/

use Carbon_Fields\Container;
use Carbon_Fields\Field;

add_action( 'carbon_fields_register_fields', 'feodoraxis_menu_options' );
function feodoraxis_menu_options() {
    Container::make( 'nav_menu_item', 'Дополнительно' )
        ->add_fields( [
            Field::make( 'checkbox', 'feodoraxis-menu-is_catalog', 'Выводить каталог' ),
            /**
             * Тут я использую Font Awesome в качестве примера.
             * Добавил только некоторые иконки для примера. 
             * Вы можете добавить все те, которые вам нужны из официальной документации
             */
            Field::make( 'select', 'feodoraxis-menu-icon', 'Иконка' )
                ->set_options( [
                    'fa-music' => "Музыка",
                    'fa-search' => "Поиск",
                    'fa-envelope-o' => "Конверт",
                    'fa-heart' => "Сердце",
                    'fa-star' => "Звезда",
                    'fa-user' => "Пользователь",
                    'fa-film' => "Фильм",
                ] ),
        ] );
}

Дополнительные поля ввода появятся после того, как вы сохраните изменения в меню

Настройки каталога

По заданию нам нужно выводить в категориях каталога некоторые товары в этой категории. Мы не уточнили способ выбора товаров для каждой категории в меню. Поэтому можно поступить двумя способами:

  1. Сделать автоматический вывод двух случайных или последних товаров у каждой категории
  2. Дать возможность админу самому выбирать, какие товары нужно выводить в меню

Я считаю, что второй вариант лучше. Всегда лучше дать клиенту точное управление, если в ТЗ не указано иное.

Чтобы дать возможность админу выводить нужные товары, добавим у категорий товаров дополнительное поле, в котором он сможет указывать нужные ему товары.

<?php
/**
 * Если пространства имен (use) уже были прописаны - повторно их указывать не нужно.
 **/
use Carbon_Fields\Container;
use Carbon_Fields\Field;

add_action( 'carbon_fields_register_fields', 'feodoraxis_catalog_categories' );
function feodoraxis_catalog_categories() {
    Container::make( 'term_meta', 'Настройки категорий' )
        /**
         * Я предполагаю, что для ИМ используется WooCommerce
         **/
        ->where( 'term_taxonomy', '=', 'product_cat' )
        ->add_fields( [
            Field::make( 'association', 'feodoraxis-products', 'Товары категории' )
            ->set_types( [
                [
                    'type'      => 'post',
                    'post_type' => 'product',
                ]
            ] )
            ->set_help_text( 'Укажите товары, которые хотите выводить в меню каталога' ) //Укажем подсказку админу
            ->set_max( 2 ) //У нас вёрстка не допускает больше двух товаров
        ] );
}

В итоге у нас получились такие настройки меню и настройки категорий товаров:

Кастомизация вёрстки

Теперь всю информацию нужно вывести используя нашу вёрстку.

Меню WordPress создаётся при помощи функци wp_nav_menu(); Чтобы мы могли кастомизировать HTML, нам потребуется Walker.

Walker — это специальный класс, определяющий внешний вид меню, которое выводит функция wp_nav_menu(). Но мы можем создать дочерний класс и с его помощью изменить внешний вид меню.

Для начала в шапке разместим функцию wp_nav_menu() и настроим её:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">

    <link rel="stylesheet" href="style.css">
    <title>Навигационное меню</title>
</head>
<body>
    <div class="container">

        <nav class="navbar navbar-expand-lg bg-light">
            <div class="container-fluid">
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <?php
                    wp_nav_menu( [
                        'theme_location' => 'primary',
                        'container'      => false,
                        'menu_class'     => 'navbar-nav me-auto mb-2 mb-lg-0',
                        'walker'         => new Feodoraxis_Walker_Nav_Menu(), //Так вместо использования стандартного класса Walker_Nav_Menu(), мы указываем его дочерний класс, который будет кастомизировать
                    ] );
                    ?>
                </div>
            </div>
        </nav>

    </div>
    <!-- JavaScript Bundle with Popper -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
    <script src="main.js"></script>
</body>
</html>

Теперь создадим класс Feodoraxis_Walker_Nav_Menu — это будет дочерний класс, класса Walker_Nav_Menu().

Подключать класс нужно после настроек, которые мы создавали ранее.

<?php
class Feodoraxis_Walker_Nav_Menu extends Walker_Nav_Menu {

}

Теперь кастомизируем класс. Я удалю все комментарии разработчиков WordPress и оставлю свои — чтобы вам было проще ориентироваться в моих изменениях.

<?php
class Feodoraxis_Walker_Nav_Menu extends Walker_Nav_Menu {
    public function start_lvl( &$output, $depth = 0, $args = null ) {
        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
            $t = $n = '';
        } else {
            $t = "\t";
            $n = "\n";
        }
        $indent = str_repeat( $t, $depth );

        /**
         * Заменим .sub-menu на .dropdown-menu
         */
        $classes = array( 'dropdown-menu' );

        $class_names = implode( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

        $output .= "{$n}{$indent}<ul$class_names>{$n}";
    }

    public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) {
        $menu_item = $data_object;

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
            $t = '';
            $n = '';
        } else {
            $t = "\t";
            $n = "\n";
        }
        $indent = ( $depth ) ? str_repeat( $t, $depth ) : '';

        $classes   = empty( $menu_item->classes ) ? array() : (array) $menu_item->classes;
        $classes[] = 'menu-item-' . $menu_item->ID;

        /**
         * Добавим Bootstrap-класс для пунктов меню первого уровня
         */
        if ( $depth === 0 ) {
            $classes[] = 'nav-item';
        }

        $args = apply_filters( 'nav_menu_item_args', $args, $menu_item, $depth );

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

        $id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $menu_item->ID, $menu_item, $args, $depth );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

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

        $atts           = array();
        $atts['title']  = ! empty( $menu_item->attr_title ) ? $menu_item->attr_title : '';
        $atts['target'] = ! empty( $menu_item->target ) ? $menu_item->target : '';
        if ( '_blank' === $menu_item->target && empty( $menu_item->xfn ) ) {
            $atts['rel'] = 'noopener';
        } else {
            $atts['rel'] = $menu_item->xfn;
        }
        $atts['href']         = ! empty( $menu_item->url ) ? $menu_item->url : '';
        $atts['aria-current'] = $menu_item->current ? 'page' : '';

        /**
         * Добавим классы ссылкам первого уровня
         */
        if ( $depth === 0 ) {
            $atts['class'] = 'nav-link';

            /**
             * Если есть дочерние пункты меню, или выводится меню каталога - добавим еще атриубты
             */
            if ( in_array('menu-item-has-children', $menu_item->classes ) || carbon_get_nav_menu_item_meta( $menu_item->ID, 'feodoraxis-menu-is_catalog' ) ) {
                $atts['class'] .= 'dropdown-toggle';

                $atts['role'] = 'button';
                $atts['data-bs-toggle'] = 'dropdown';
                $atts['aria-expanded'] = 'false';
            }
        }

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

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

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

        $title = apply_filters( 'nav_menu_item_title', $title, $menu_item, $args, $depth );

        $item_output  = $args->before;
        $item_output .= '<a' . $attributes . '>';

        /**
         * Выведем иконки
         */
        if ( $depth > 0 ) {
            $icon_class = carbon_get_nav_menu_item_meta( $menu_item->ID, 'feodoraxis-menu-icon');
            if ( !empty( $icon_class  ) ) {
                $item_output .= '<i class="fa ' . $icon_class . '"></i> ';
            }
        }

        $item_output .= $args->link_before . $title . $args->link_after;
        $item_output .= '</a>';

        /**
         * Если это меню каталога, то выведем его при помощи дополнительного метода, который создадим прямо в этом классе
         */
        if ( carbon_get_nav_menu_item_meta( $menu_item->ID, 'feodoraxis-menu-is_catalog' ) ) {
            $item_output .= $this->get_catalog();
        }

        $item_output .= $args->after;

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

    /**
     * @return string
     *
     * Метод вывода каталога товаров.
     * Т.к. мы будем использовать функцию wc_get_template_part(), то нам потребуется кеширование данных на вывод
     */
    protected function get_catalog():string {

        global $post;

        /**
         * Сначала подготовим данные
         */
        $categories = get_terms( [
            'taxonomy' => 'product_cat',
            'hide_empty' => false,
            'orderby' => 'parent',
            'exclude' => [15] //Не выводим Uncategorized
        ] );

        $items = $products_categories = [];

        /**
         * Подготовим массив для удобного вывода
         */
        foreach ( $categories as $category ) {
            if ( $category->parent === 0 ) {
                $items[ $category->term_id ] = [
                    'term_id'          => $category->term_id,
                    'name'             => $category->name,
                    'slug'             => $category->slug,
                    'term_group'       => $category->term_group,
                    'term_taxonomy_id' => $category->term_taxonomy_id,
                    'taxonomy'         => $category->taxonomy,
                    'description'      => $category->description,
                    'parent'           => $category->parent,
                    'count'            => $category->count,
                    'filter'           => $category->filter,
                ];

                /**
                 * Сразу получим товары для категорий
                 */
                $products = carbon_get_term_meta( $category->term_id, 'feodoraxis-products' );
                if ( !empty( $products ) ) {
                    foreach ( $products as $product ) {
                        $products_categories[ $category->term_id ][] = $product['id'];
                    }
                }
                unset($products);
            } else {
                $items[ $category->parent ]['child'][ $category->term_id ] = [
                    'term_id'          => $category->term_id,
                    'name'             => $category->name,
                    'slug'             => $category->slug,
                    'term_group'       => $category->term_group,
                    'term_taxonomy_id' => $category->term_taxonomy_id,
                    'taxonomy'         => $category->taxonomy,
                    'description'      => $category->description,
                    'parent'           => $category->parent,
                    'count'            => $category->count,
                    'filter'           => $category->filter,
                ];
            }
        }

        /**
         * Начало буферизации
         */
        ob_start();

        echo '<div class="dropdown-menu dropdown-catalog">
                    <div class="catalog_menu">
                        <div class="catalog_menu-flexbox">';

        /**
         * Выведем категории
         */
        echo '<div class="catalog_menu-nav">
                        <ul>';

        foreach ( $items as $item ) {
            echo '<li>';
            echo '<a href="' . get_term_link( $item['term_id'] ) . '" data-category="#category_' . $item['term_id'] . '">' . $item['name'] . '</a>';

            /**
             * В этом месте больше подошла-бы рекурсия, но мы упрощаем код для более простого понимания
             */
            if ( isset( $item['child'] ) ) {
                echo '<ul>';

                foreach ( $item['child'] as $child ) {
                    echo '<li><a href="' . get_term_link( $child['term_id'] ) . '">' . $child['name'] . '</a></li>';
                }

                echo '</ul>';
            }

            echo '</li>';
        }

        echo '</ul>
            </div>';

        /**
         * Выведем секции с превью-карточкам товаров
         */
        echo '<div class="catalog_menu-products">';

        if ( !empty( $products_categories ) ) {
            foreach ( $products_categories as $key => $products ) {
                echo '<div class="catalog_menu-category" id="category_' . $key . '">
                        <div class="catalog_menu-products-flexbox">';

                /**
                 * Т.к. в стандартном шаблоне WC превью картоки товара выводятся при помощи списка - их нужно завернуть в родительский тег ul
                 */
                echo '<ul>';

                foreach ( $products as $post ) {
                    setup_postdata( $post );

                    echo '<div class="catalog_menu-product">';
                    wc_get_template_part( 'content', 'product' );
                    echo '</div>';
                }

                wp_reset_postdata();

                echo '</ul>
                    </div>
                </div>';
            }
        }
        echo '</div>';

        /**
         * Закрываем каталог меню
         */
        echo '</div>
            </div>
        </div>';

        /**
         * Конец буферизации и вывод разметки
         */
        $output = ob_get_contents();
        ob_end_clean();
        return $output;
    }
}

Отлично! Мы дали все элементы управления и кастомизировали HTML, который выводит функция wp_nav_menu().

Вуаля! У нас получилось кастомизированное меню 🙂

Заключение и финальный код

В результате мы смогли прокачать меню в соответствии с заданием:

  • Пункты меню второго уровня имеют свои иконки, которые может указывать администратор
  • Админ может выбирать, какой пункт меню будет выводить категории меню
  • Также у категорий в меню у нас выводятся товары, которые указывает администратор сайта.

Как по-мне, получилось круто. Вы можете кастомизировать этот код как хотите и использовать у себя 🙂

А посмотреть результат можно в этом репозитории GitHub — https://github.com/feodoraxis/menu-wordpress

Если у вас есть вопросы, или вы хотите покритиковать мою работу — велкам в комментарии!

Также подписывайтесь на мою группу в ВК и на YouTube-канал 🙂

https://vk.com/feodoraxisru

https://www.youtube.com/c/АндрейСмородин