本文へジャンプ

WordPressのカスタムメニューをカスタマイズして出し入れ&並び替え自由なUIを作る

Posted by mio.satoh

世界のトップ100万サイトの内、22%はWordPressで出来ている!(W3Techs調べ)

『汎用CMS』用途においては、一時はその勢いに陰りが見えた(かに思えた)WordPressですが、ここに来てまた盛り返しているような気がします。

「WordPressってもともとブログ用CMSでしょ? 汎用的に使うなら汎用CMSを使った方が効率的じゃないの?」といった意見が強まり、「WordPressの天下が終わるのも時間の問題か...」なんて空気が漂っていた時期もありました。
ところがWordPress(WP)に替わるだけの完成度と機能性を持ったオープンソースCMSは結局現れず。WPが天下を譲らなかったのはこの後継者不足問題が一番大きいと思うのですが、やっぱりなんだかんだでWordPressはよく出来てます。

コードがレガシー? 確かにね。でもその反面可読性が高かったりバージョンアップ毎の学習コストが低かったりするんです。

ロードが遅い? もちろん静的なHTMLよりは遅いしサーバ負荷も高いけど、工夫によってかなり改善できる部分です。今ならリバースプロキシなんかも使えるし。

また、かつてのWPの管理画面はいかにも海外製という感じで、(1バイト文字に最適化されているために)文字は小さいしフレンドリーさに欠けてましたが)、今の管理画面はスッキリ見やすいし日本語のバランスもいいんですよね。

WordPressのダッシュボード

そんな進化し続けるWordPress。MONSTER DIVEでもプロジェクトでCMSとして導入する機会が増えております。
今回は(Laravelの連載をいったん脇に置いて)WordPressの"カスタムメニュー"をカスタマイズするお話です。

やりたいこと

受託開発のプロジェクトでCMSを導入するとき、重要になってくるのは「クライアントさん側でどこまで更新作業を行うのか」という部分です(当然のことながら)。
記事を入力して新着順に表示するだけなら単純ですが、それ以上の要望を頂くことも稀によくあります。

  • お知らせ記事一覧を自由に並び替えたい?
  • え、しかもドラッグ&ドロップがいいんですか?
  • さらに記事を選択するときに検索機能が欲しいと...。

そんな要望を頂いても大丈夫。そう、カスタムメニューならね。
WPの管理画面上で、表示するコンテンツの選択と並び替えを自由に操作できます。それもGUIで!

WordPressの「カスタムメニュー」機能って?

「メニュー」という名前がついているように、本来はメニュー的なナビゲーション要素の内容をカスタマイズできる機能です。

カスタムメニュー設定画面

左カラムでコンテンツを選び、「メニューに追加」をクリックすると右のメニュー枠に追加されます。並び替えはドラッグ&ドロップで出来るし、数を増やしたり減らしたりも自由です。Ajaxでサクッと記事検索する機能もついてます。

さっそく導入しよう

カスタムメニュー機能を導入する

前提として、WPの執筆時点での最新版(4.1.1)を新規インストールし、オリジナルテーマを作成するものとします。
メニューの登録・表示方法は何通りかあるのですが、今回は、

  • メニュー機能を使用する場所は決まっている
  • 表示するソースコードをカスタマイズしたい

という要件として、いちばん簡単と思われる方法で解説します。

まず、カスタムメニュー機能はデフォルトでOFFになっているので、functions.phpに以下の1行を追加してONにしましょう。

add_theme_support('menus');

これで管理画面の「外観」の配下に「メニュー」というリンクが追加されたはずです。

メニューはいくつも登録できますが、今はとりあえず"メインメニュー"というメニューを登録してみます。
「メニューの名前」欄に"test_menu"と入力して保存してみましょう。
テンプレート側からメニューを指定して呼び出すときに使う名前なので、分かりやすい名前を付けておきましょう。できれば英数で区別がつきやすいものがいいと思います。

"test_menu"メニューを追加

これで"test_menu"というメニューが登録されました。

登録したメニューを表示する

単純に表示するだけならとっても簡単。
トップページに表示させたいなら、テーマファイル内index.phpの表示させる場所に以下のように書くだけです。

<?php wp_nav_menu( array('menu' => 'test_menu' )); ?>

ここで吐き出されるタグはこんな感じになります。

<div class="menu-test_menu-container">
  <ul id="menu-test_menu" class="menu">
  <li id="menu-item-8" class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-8"><a href="http://foo.com/">ホーム</a></li>
  <li id="menu-item-9" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-9"><a href="http://foo.com/?page_id=2">サンプルページ</a></li>
  </ul>
</div>

選んだコンテンツがちゃんと表示されました。
けど、見ての通り、余計なタグがいっぱいです。
外側のdivは要らないとか、各liの中にサムネイル画像を追加したいとか、色々欲をかきたくなります。

メニューの表示内容をカスタマイズする

メニューの仕組みをざっくり把握する

ここからはちょっと応用編。
出力されるタグの中身をゴリゴリとカスタマイズする方法です。

メニューを出力する仕組みは、/wp-includes/nav-menu-template.phpという深いところに書かれています。つまり直接は弄れないということです。
でもご安心を。出力内容をある程度カスタマイズするため、"ガワだけ着せ替え"できるようになっているのです。

以降、少しばかりプログラミングの話が出てきてしまいます。
まずはその仕組みを簡単にでも把握するために、nav-menu-template.phpを開いてみてください。
wp_nav_menuを定義している箇所を見ると、最初の方にこんなことが書かれています。

function wp_nav_menu( $args = array() ) {
    static $menu_id_slugs = array();

    $defaults = array( 'menu' => '', 'container' => 'div', 'container_class' => '', 'container_id' => '', 'menu_class' => 'menu', 'menu_id' => '',
    'echo' => true, 'fallback_cb' => 'wp_page_menu', 'before' => '', 'after' => '', 'link_before' => '', 'link_after' => '', 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
    'depth' => 0, 'walker' => '', 'theme_location' => '' );

  // 以下略

$defaultsの配列は、メニューのデフォルト設定です。
ということは、メニューを呼び出すときにこれらの設定を上書きできるってことですね。
付けるクラスや前後の記述など、ある程度のカスタマイズはこのオプションだけでいけそうです。

さらに注目すべきは、この中の'walker'というオプションです。
デフォルトでは空になってますが、実はこのwalkerこそがメニューの出力テンプレートを着せ替えする設定なのです。

walkerを自作する

同じnav-menu-template.phpの冒頭にWalker_Nav_Menuというクラスが定義されています。
その中のstart_el()というメソッドが、「メニューを出力する」処理の本丸です。
つまり、このWalker_Nav_Menuクラスを継承したクラスを作って、start_el()の内容を書き換えてしまえば、出力内容を制御できそうな気がしてきました。
早速、functions.phpにWalker_Nav_Menuを継承(extends)したWalker_Nav_Menu_Testというクラスを作ってみます。 クラスの中身は元のWalker_Nav_Menuクラスのstart_el()をコピペしてきて、必要に応じて書き換えます。

class Walker_Nav_Menu_Test extends Walker_Nav_Menu {
    function start_el(&$output, $item, $depth, $args) {
        $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';

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

        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
        // li要素に任意のクラスosukina_classを足してみた
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . ' osukina_class"' : '';

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

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

        $atts = array();
        $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
        $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
        $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
        $atts['href']   = ! empty( $item->url )        ? $item->url        : '';

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

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

        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        // ここに記事のサムネイルを追加してみた。
        // メニューで表示する記事のIDは$item->IDではなく$item->object_idに入っていることに注意。
        $item_output .= get_the_post_thumbnail($item->object_id, array(100, 100), array('alt' => $item->title));
        $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;

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

ここのカスタマイズはちょっと骨が折れますが、頑張ればサムネイルだけでなく、投稿日を出したり作者を出したりと何でも弄ることができます。

自作walkerを使って出力する

最後に、テーマファイルに書いた出力用コードをこんな感じに変更します。

wp_nav_menu( array(
  'menu' => $nav_menu,
  'walker' => new Walker_Nav_Menu_Test()
));

'walker'のところに先ほど定義したWalker_Nav_Menu_Testクラスをnewします。

これで完了!
出力を確認してみましょう。

<div class="menu-test_menu-container">
  <ul id="menu-test_menu" class="menu">
    <li id="menu-item-8" class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-8 osukina_class"><a href="http://foo.com/">ホーム</a></li>
    <li id="menu-item-9" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-9 osukina_class"><a href="http://foo.com/?page_id=2"><img width="100" height="100" src="http://foo.com/wp-content/uploads/2015/04/2012-05-31-00.44.44-150x150.jpg" class="attachment-100x100 wp-post-image" alt="サンプルページ" />サンプルページ</a></li>
  </ul>
</div>

サムネイルがある記事にはばっちりサムネイルも表示されてるし、osukina_classも追加されてますね。

ちょっと長い道のりでしたが、頑張ればカスタムメニューをごりっとカスタマイズできることが分かっていただけたと思います。

hackのしやすさこそWordPressの魅力

ここで紹介したようなメニューの使い方は、WordPressのオフィシャルドキュメントでは紹介されていません。

でも、メニューの出力部分がこんな風に拡張性を持った形で実装されているということは、カスタマイズしたい人は中身を読んでカスタマイズしてね、と開発者にやさしい設計思想になっているということなんですよね。

こういう拡張性の高さがWordPressの魅力だし、公式Plugin Directoryに何億という数の有志の自作プラグインが投稿されている所以でしょう。

MONSTER DIVE印のオリジナルプラグイン、いつかリリースするかもね!

15.11.27追記

ソースの一部が化けて出力されていたため、修正いたしました。 失礼いたしました!

Recent Entries
MD EVENT REPORT
What's Hot?