“Extending” Wordpress to offer bootstrap 5, vertical, accordion menu that is parent/child aware to depth 2
Wordpress comes with wp_get_nav_menu_items function that suffers from two major limitations:
- It is limited to depth 1 in determining the parent/child relationship
- It does not -directly- provide the information if a submenu item is a parent to another (sub) menu item
To overcome this shortcoming, the WP community has created various menu “walkers” that allow their adopters to extend and/or override the default WP menu behaviour.
However, these attempts suffer from the inherent shortcoming of the WP menu that is not fully Bootstrap 5 compatible. For example they rely on the <ul><li></li></ul>
I spent a couple of frustrating days in trying to re-create the functionality of the Metronic vertical, accordeon, fully Bootstrap5 menu in Wordpress trying to make use the documentation and code sample in WP’s site, that led me to the usual places for support that in turn pointed to the various ‘menu walkers’ that are circulating around.
Me thinks that spending two days to fully customise a menu is too much in this day and age — no wonder there are companies that make a decent living out of the wordpress community offering various “mega menu” plugins
Therefore, I decided to spend a few more minutes to “open source” my approach.
You can find it here: https://github.com/exonianp/wp-bootstrap5-accordeon-vertical-menu
It so happens that it is my first personal open project on github, if that is to say something about my frustration.
This initial file produces the functionality of displaying fully compatible with Metronic 8, bootstrap 5, vertical (accordion) menu.
1. It goes to depth two (i.e. main item, submenu item and children of the submenu).
2. It does contain SQL code to determine if a main menu item is a parent to a child
3. It does contain SQL code to determine if a submenu has a child
There are many walkers available but they are based on the core functionality of the WP menu (which is rather limited as it only goes to depth one by default) and add quite a lot of overhead, with limited control to the actual output of the code. Then the output has to be overwritten with various techniques.
Finding if a submenu item has a child and getting all the details is achieved with the following SQL:
SELECT e.id, e.post_title, CASE WHEN c.meta_value=’custom’ THEN ( d.meta_value) ELSE concat (‘/’, e.post_name ,’/’) END as linkurl, b.post_id
FROM wp_postmeta a FORCE INDEX (post_id)
JOIN wp_postmeta b FORCE INDEX (post_id) ON a.post_id=b.post_id
JOIN wp_postmeta c FORCE INDEX (post_id) ON c.post_id=b.post_id
JOIN wp_postmeta d FORCE INDEX (post_id) ON d.post_id=b.post_id
JOIN wp_posts e ON e.id=b.meta_value
WHERE d.meta_key=’_menu_item_url’ AND c.meta_key=’_menu_item_object’ AND b.meta_key=’_menu_item_object_id’
AND a.meta_key=’_menu_item_menu_item_parent’ AND a.meta_value=’” . $submenu->ID . "' ORDER BY a.post_id DESC";
We are performing four JOINS in the wp_postmeta table in order to get the following information:
1. The list of the children of the parent via the _menu_item_menu_item_parent
2. The type of the link _menu_item_object of each child
3. Determination of the _menu_item_url that provides the URL in a custom link (i.e. not page or post)
4. Determination of the _menu_item_object_id that provides the actual post id of the page/post referenced in the menu
The final, fifth, JOIN provides us the required information from the wp_posts table for the referenced post.
The link, in the correct format, is produced in SQL in order to avoid creating if/else conditions in php:
CASE WHEN c.meta_value=’custom’ THEN ( d.meta_value) ELSE concat (‘/’, e.post_name ,’/’) END as linkurl
This is because if the menu item has a custom link, the target url is saved (as entered) in the meta_value column of the wp_postmeta table where meta_key=_menu_item_url, whereas the links to pages and posts are derived by the post_name field in the wp_posts table.
As there is no menu ordering information for items beyond depth 1, I order the results in decending order with respect to the sequence of creation of the menu item. So, the menu items of a submenu appear in a LIFO manner. To order them in the way that you like just put first the one that you want to appear last in the submenu.
The FORCE INDEX statements multiply the performance of the query (depending on the actual tuning of your MariaDB/mySQL server.
Since most of us cannot control the tuning of the servers that host our applications is best practice to include the directive notwithstanding the fact that the (post_id) index comes with Wordpress installation).
A word about performance:
Somehow, there is this a-priori belief that as long as one uses Wordpress’s own functions instead of SQL there is a performance benefit. I doubt it. For example, the various menu walkers that are used to provide the “infinite depth” parent/child relationships, do so by running multiple functions operating in all the items/values of the objects that their creator deemed suitable and then operating on much more complex arrays.
In my case, the walker alternative (that was unable to provide the exact aesthetic/functional result I was after), was about an order of magnitude slower than the SQL Version.
Relying in Wordpress’s own functions is a lovely way to show off your array manipulation skills in php but it is definitely NOT the optimal way, performance wise.
It will be great if the Wordpress community decides to include more than depth 1 in the wp_get_nav_menu_items as well as provide methods to easily determine if a node has a child or belongs to a parent (or not).
Navigation is one of the most crucial parts of any website and the wordpress core team has to up its game in this domain.