AjaxでWordPressのページをシームレスに遷移させるコードをおおよそ作成した
WordPressのページをAjaxを利用してシームレス(継ぎ目なし)に遷移したいという話があった。
話が出たとき、「ああ、ついにこういう系のやつが来たか」と思ってげんなり。
まぁ自作のURLを処理するフレームワークとかに実装するなら別に大した問題でもない。
が、WordPressみたいに複雑にパラメータを処理してページを表示するタイプのCMSには、自分が預かり知らぬところの表示処理も考慮して実装するのは案外難しかったりする。
とりあえず、無理なら無理でもいいということだったので、それを行うために山積みの問題と格闘してそこそこ時間を取って作成してみた。
まぁ、時間を取った割に、いろいろな制約が重なって残念ながらお蔵入りになったけど。
制約を守って使う分には使えそうなので、掲載しようと思う。
実際のところで、このAjaxによるページ遷移・・・今的な言い方をするとPjaxとでもいうべきなんだろうか。
これ自体は、URLの形式に拘らなければ意外とシンプルに実装できたりする。拘らなければ、だが。
それを相手方に伝えたところ、まぁ開口一番拒否されましたが。
「URLの形式はこの形でないといけない!」とのこと。どないせいっちゅーねん。
実に感覚的な問題である。見た目の美しさのためには、それを得るための犠牲は付きもんだというのに・・・。
で、指定のURLの形式でパラメータの処理をさぁどうするかとかなんやらで初めから問題はクライマックス。
前にこちらの記事でURLでのパラメータ処理について書いていたが、実はこの問題に対応するためだった。
(このときは実際にページ間をAjaxでやり取りする際に、使えるかどうかも不明瞭な状態だったのでお茶を濁していたけど)
パラメータの処理はおおよそこれをベースに使うことにして、今回のコードを作成。
コード掲載の前に(当該の記事の内容と被るけど)動作要件、そして、冒頭で書いていたコードの制約を合わせて書いておく。
動作要件はこんな感じ。
- URLの形式は「サイトURL/%year%/%monthnum%/%day%/%post_id%/」。
- または、「サイトURL/%year%/%monthnum%/%day%/%post_name%/」。
で、制約はこんな感じ。
- 動作要件で書いたURL形式以外は対象外。
- フォームの処理は割愛(POSTで飛んでくるデータを全てAjax側に書いて処理しないといけないため、非常に面倒くさい。まぁPjaxでも同じみたいだしここについては別にいいや。)
- ショートコードの登録タイミングがWordPress本体のAjaxの読み込みより遅いものは、そのショートコードの実行ができない。(自分の場合はMW WP Formで確認、というか実行タイミング100000とか正直きついッス・・・。Contact Form 7とかは特に問題ない。)
以上を踏まえて作成したコードを掲載していく。
コードそのものは自分が読みやすくするために改行をそこそこ入れているとはいえ、結構長いので注意。
一部設定を書き換えなければならない部分はあるけど、基本的にはコピペで使える。多分。
記事の最後にファイルのダウンロード付けておきます。
読むのが面倒な方は記事の末端まで飛んで下さい。
まずはディレクトリの構成から。こんな形で。
ファイル構成
your_theme_name - lib - class-pagetransition.php - class-pagetransition-settings.php - functions-pagetransition.php - jquery.ajax.pagetransition.js - pagetransition.php - content (Dir) - content.php - content-single.php - include-content.php - include-single.php - etc... - index.php - single.php - etc...
スクリプトファイルは以下の様な感じ。
ほぼほぼWPのプレーンなAjax処理なので、このあたりはあまり難しくはない。
jquery.ajax.pagetransition.js
(function($){ $.fn.Pagetransition = function(options){ var option = { anchor: 'a[href]', delay: 0, }; var static = { link: 'pagetransition-link', }; var config = $.extend(option, options); var inc = $(this), st = null; var Pagetransition = (function(){ return{ init: function(){ Pagetransition.reverse(); Pagetransition.clip(); }, clip: function(){ $(config.anchor + ':not([target])').not('#wpadminbar a').addClass(static.link); $('.' + static.link).bind('click', function(e){ e.preventDefault(); if( $(this).attr('href') !== '' ){ var post_url = $(this).attr('href'); Pagetransition.proc(post_url); } }); }, reverse: function(){ $(window).bind('popstate', function(e) { Pagetransition.post(location.href); }); }, proc: function(post_url){ history.pushState(null, null, post_url); // Closing effect st = setTimeout(function(){ // Opening effect Pagetransition.post(post_url); }, config.delay); }, post: function(post_url){ $.ajax({ type: 'POST', url: pagetransition_ajax.url, data: { action: 'site_pagetranstion', url: post_url, }, success: function(result){ $('html,body').animate({scrollTop: 0}, 0); inc.html(result); $('.' + static.link).unbind('click'); Pagetransition.clip(); }, error: function(e){ console.log(e); }, }); } }; })(); Pagetransition.init(); return(this); }; })(jQuery);
以下は包括用のPHPファイル。
設定用・コア的な処理・フロントエンドで使う部分とおおよその用途に分けたかったので3分割。
pagetransition.php
<?php require_once( dirname( __FILE__ ) . '/class-pagetransition-settings.php' ); require_once( dirname( __FILE__ ) . '/class-pagetransition.php' ); require_once( dirname( __FILE__ ) . '/functions-pagetransition.php' ); $Pagetransition = new Pagetransition();
以下はコア的なPHPファイル。
手続き式でやると、ただでさえ雑多なコードが余計雑多なコードになるのでプラグインよろしく色んな所をクラスとメソッドで作成している。
class-pagetransition.php
<?php Class Pagetransition { private $PagetransitionSettings; function __construct() { $this->PagetransitionSettings = new PagetransitionSettings(); add_filter( 'rewrite_rules_array', array( $this, 'rewrite_rules' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'wp_head', array( $this, 'load_scripts' ) ); add_action( 'wp_ajax_site_pagetranstion', array( $this, 'branch_templates' ) ); add_action( 'wp_ajax_nopriv_site_pagetranstion', array( $this, 'branch_templates' ) ); } public function rewrite_rules( $_rules ) { $_add_rules = $this->PagetransitionSettings->rules(); $_rules = array_merge( $_add_rules, $_rules ); return $_rules; } public function enqueue_scripts() { wp_enqueue_script( 'js-pagetransition', get_template_directory_uri() . '/lib/pagetransition/jquery.ajax.pagetransition.js', array( 'jquery' ) ); } public function load_scripts() { $_params = array( 'ajax_url' => admin_url( 'admin-ajax.php' ), ); $_scripts = <<<EOT var pagetransition_ajax = { url: '{$_params['ajax_url']}', }; EOT; echo preg_replace( '~\r\n|\r|\n|\t~', '', '<script type="text/javascript">' . $_scripts . '</script>' ) . "\n"; } private function decrypt_queries( $_post_url = '' ) { $_entity = ''; $_matches = array(); $_rewrite_rules = get_option( 'rewrite_rules' ); $_host_url = ( empty( $_SERVER['HTTPS'] ) ? 'http://' : 'https://' ) . $_SERVER['HTTP_HOST']; $_home_dir = preg_replace( '~' . $_host_url . '~', '', get_home_url() ); $_request = '' !== $_post_url ? esc_url( $_post_url ) : esc_url( get_home_url() ); $_request = preg_replace( '~' . $_host_url . '|/$~', '', $_request ); $_request = str_replace( $_home_dir, '', $_request ); foreach ( $_rewrite_rules as $_key => $_val ) { if ( preg_match( '~' . $_key . '~', $_request, $_matches ) ) { $_entity = $_val; break; } } if ( ! empty( $_matches ) ) { unset( $_matches[0] ); foreach ( $_matches as $_key => $_val ) { $_entity = preg_replace( '~\$matches\[' . $_key . '\]~', $_val, $_entity ); } //$_entity = preg_replace( '~&page=\$matches\[[0-9]{1,}\]~', '', str_replace( '=/', '=', $_entity ) ); //$_entity = str_replace( 'index.php?', '', preg_replace( '~&page=\$matches\[[0-9]{1,}\]~', '', $_entity ) ); $_entity = str_replace( 'index.php?', '', $_entity ); $_entity = preg_replace( '~^\&~', '', $_entity ); $_entity = preg_replace( '~#.+?$~', '', $_entity ); } return $_entity; } public function branch_templates() { global $wp_rewrite; $_url = $_POST['url']; $_title = ''; $_SERVER['REQUEST_URI'] = str_replace( ( empty( $_SERVER['HTTPS'] ) ? 'http://' : 'https://' ) . $_SERVER['HTTP_HOST'], '', $_url ); $_query_char = $this->decrypt_queries( $_url ); var_dump($_query_char); if ( false === strpos( $_query_char, 'post_type' ) ) { $_post_types = $this->PagetransitionSettings->post_types_query(); foreach ( $_post_types as $_val ) { if ( preg_match( '~^' . $_val . '~', $_query_char ) ) { $_query_char = $_query_char . '&post_type=' . $_val . '&name=' . str_replace( $_val . '=', '', $_query_char ); break; } } } query_posts( $_query_char ); $_title = wp_title( '', false ); if ( '' === $_title ){ $_title = $this->PagetransitionSettings->pagetransition_after_titles(); $_title = $_title['home']; } else { $_title = $this->PagetransitionSettings->pagetransition_after_titles(); $_title = $_title['every']; } $_scripts = <<<EOT (function($){ var pagetransition_title = '{$_title}'; document.title = pagetransition_title; })(); EOT; echo preg_replace( '~\r\n|\r|\n|\t~', '', '<script type="text/javascript">' . $_scripts . '</script>' ) . "\n"; $this->PagetransitionSettings->includes(); wp_reset_query(); die(); } } Class PagetransitionCall { public function pagenation( $_pagenation = true, $_prev_string = '<', $_next_string = '>' ){ global $wp_query; $_base = $this->replace_pagenation_base(); $_arguments = array( 'base' => $_base, 'format' => '?paged=%#%', 'prev_next' => true, 'prev_text' => '<span class="prev-string">' . $_prev_string . '</span>', 'next_text' => '<span class="next-string">' . $_next_string . '</span>', 'current' => max( 1, get_query_var('paged') ), 'type' => 'plain', 'total' => $wp_query->max_num_pages, ); $_pager = paginate_links( $_arguments ); if ( isset( $_pager ) ) { $_pager = preg_replace( '~\t~', '', $_pager ); $_pager = preg_replace( '~\r\n|\n|\r~', '', $_pager ); if ( false === $_pagenation ) { $_pager = preg_replace( '~<a class=\'page-numbers.+?</a>|<span class="page-numbers.+?</span>|<span aria-current=.+?</span>~', '', $_pager ); $_pager = preg_replace( '~ page-numbers|page-numbers |page-numbers~', '', $_pager ); $_pager = '<nav class="pager">' . $_pager . '</nav>'; } else { $_pager = '<nav class="pagenation">' . $_pager . '</nav>'; } } echo $_pager; } private function replace_pagenation_base() { $_url = parse_url( ( empty( $_SERVER['HTTPS'] ) ? 'http://' : 'https://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], PHP_URL_PATH ); if ( preg_match ( '~page/~', $_url ) ) { $_base = preg_replace( '~page/([0-9]{1,})/~', '', $_url ); $_base = preg_replace( '~^(.+?)$~', '\1/page/%#%/', $_base ); } else { $_base = preg_replace( '~^(.+?)$~', '\1/page/%#%/', $_url ); } $_base = preg_replace( '~//page/~', '/page/', $_base ); return $_base; } }
以下はページ読み込みのための各種設定用のPHPファイル。
設定用とは言ってもテーマとサイトに合わせてきちんと設定しなければならぬです。
class-pagetransition-settings.php
<?php Class PagetransitionSettings { public function includes() { /* * Theme Path Example : * * your_theme_name * - content (Dir) * - - include-content.php (Template) * - - include-single.php (Template) * - - etc... * - index.php (Template) * - single.php (Template) * - - etc... * */ if ( is_page() ) { // Page get_template_part( 'content/include', 'page' ); } elseif ( is_attachment() ) { // Attachment get_template_part( 'content/include', 'attachment' ); } elseif ( is_singular( 'news' ) ) { // Custom post of 'news' get_template_part( 'content/include', 'single-news' ); } elseif ( is_single() ) { // Single get_template_part( 'content/include', 'single' ); } elseif ( is_category() ) { // Category get_template_part( 'content/include', 'content' ); } elseif ( is_tag() ) { // Tags get_template_part( 'content/include', 'content' ); } elseif ( is_tax( 'category_news' ) ) { // Custom taxonomy of 'news' get_template_part( 'content/include', 'news' ); } elseif ( is_date() ) { // Date get_template_part( 'content/include', 'content' ); } elseif ( is_search() ) { // Search get_template_part( 'content/include', 'search' ); } elseif ( is_author() ) { // Author get_template_part( 'content/include', 'content' ); } elseif ( is_post_type_archive( 'news' ) ) { // custom post archive of 'news' get_template_part( 'content/include', 'news' ); } elseif ( is_archive() ) { // Other archive get_template_part( 'content/include', 'content' ); } else { // Front page get_template_part( 'content/include', 'frontpage' ); } } public function rules() { $_rules = array( // Search 'page/([0-9]{1,})/\?s=(.*)$' => 'index.php?paged=$matches[1]&s=$matches[2]', 'page/([0-9]{1,})/\?s$' => 'index.php?paged=$matches[1]&s=', '\?s=(.*)$' => 'index.php?s=$matches[1]', '\?s$' => 'index.php?s=$matches[1]', 'search/(.+)/page/?([0-9]{1,})/?$' => 'index.php?s=$matches[1]&paged=$matches[2]', 'search/(.+)/?$' => 'index.php?s=$matches[1]', // Date Archives '([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/page/?([0-9]{1,})/?$' => 'index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&paged=$matches[4]', '([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/?$' => 'index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]', '([0-9]{4})/([0-9]{1,2})/page/?([0-9]{1,})/?$' => 'index.php?year=$matches[1]&monthnum=$matches[2]&paged=$matches[3]', '([0-9]{4})/([0-9]{1,2})/?$' => 'index.php?year=$matches[1]&monthnum=$matches[2]', '([0-9]{4})/page/?([0-9]{1,})/?$' => 'index.php?year=$matches[1]&paged=$matches[2]', '([0-9]{4})/?$' => 'index.php?year=$matches[1]', // When the static page show the blog 'post' articles. 'topics/page/([0-9]{1,})/?$' => 'index.php?pagename=topics&paged=$matches[1]', ); return $_rules; } public function post_types_query() { $_post_types = array( // Specify the registered post_type. 'news', ); return $_post_types; } public function pagetransition_after_titles() { $_titles = array( // The title on home and other everything pages. 'home' => esc_html( get_bloginfo( 'name' ) ) . ' - ' . esc_html( get_bloginfo( 'description' ) ), 'every' => esc_html( wp_title( '', false ) ) . ' - ' . esc_html( get_bloginfo( 'name' ) ), ); return $_titles; } }
以下はテーマでの実行用関数をまとめたPHPファイル。
コアで専用のページネーションを作っているので、テーマで使うためにクラスとメソッドの呼び出しを関数化。
functions-pagetransition.php
<?php function pagetransition_pagenation( $_prev_string = '<', $_next_string = '>' ) { $call = new PagetransitionCall(); $call->pagenation( true, $_prev_string, $_next_string ); } /* * Template Function - Entries Pager */ function pagetransition_pager( $_prev_string = '<', $_next_string = '>' ) { $call = new PagetransitionCall(); $call->pagenation( false, $_prev_string, $_next_string ); }
うん、長いね。
いつぞや書いたRailsのメモとかにも匹敵する長さ。
とりあえず、作成したコードの内容に関してはこんなところ。
使い方としては以下のような形になる。
ファイル構成を参考にテーマの中にテンプレートファイルを作成する。
テンプレートファイルは以下の様な例で使用するテンプレート分だけ、各種設置する。
例:your_theme_name/index.php
普通なら「あれ?」となる構成だが、遷移のためにループファイルのインクルードを行うように指定する。
<?php get_header(); get_template_part( 'content/include', 'frontpage' ); get_footer(); ?>
例:your_theme_name/content/include-frontpage.php
通常ならindex.php等に記載する内容。
これがclass-pagetransition-settings.phpで指定するAjaxの読み込み用の実体テンプレートとなる。
<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); get_template_part( 'content/content', get_post_format() ); endwhile; pagetransition_pager(); pagetransition_pagenation(); endif; ?>
pagetransition_pager()、及び、pagetransition_pagenation()はページ送りとページネーション。
わざわざこうしてページ送り用の関数を作っているのは、Ajaxでテンプレートを読み込むと、pagenate_links()やprevious_posts_link()内で処理されるリクエストURIは、Ajaxファイルそのものになるため。
例:your_theme_name/content/content.php
これは普通にテーマを作るときと同じようにする。
<article id="entry-<?php the_ID(); ?>" class="entry-content"> <header class="header"> <time class="post-date"><?php the_date(); ?></time> <h1 class="post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h1> <?php edit_post_link(''); ?> </header> <section class="content"> <?php the_excerpt(); ?> </section> </article>
テンプレートファイルが適宜揃ったら、テーマ側のfunctionsにファイルのインクルードを行って、起動コードを設置する。
your_theme_name/functions.php
require( dirname( __FILE__ ) . '/lib/pagetransition.php' ); function theme_read_scripts() { $string = <<<EOT <script type="text/javascript"> (function($){ $(document).ready(function(){ $('#contents').Pagetransition(); // ←注:Ajaxで読み込むテンプレートを表示するブロックを指定する。 }); })(jQuery); </script> EOT; echo preg_replace( '/\r\n|\n|\r|\t/', '', $string ); } add_action('wp_head', 'theme_read_scripts');
以上で、設置は終わり。
見るのも嫌になりそうな長ったらしいものをここまで読んでくれてありがとう。
お疲れ様です。
ページ遷移先のタイトルは申し訳程度に入れてあるけど、class-pagetransition-setting.phpのpagetransition_after_titles()の設定を弄る必要がある。ついでにリライト条件の優先度とかも申し訳程度。
テンプレートの読み込み条件は適宜追加して下さい。
細かい不備はあるので突っ込みだすとキリがないけどとりあえずざっくりとはこんな感じ。
仕事で作っていたテーマで動作を確認していたが、制約を守って普通に使う分だけには問題はない・・・はず。
後々修正するかもしれない。というか、多分する。
ちなみに、Ajaxのページ遷移の際のアナリティクスなどのデータ送信については、自分は調べてないのでここで見るよりも他サイト様のほうが詳しいと思います。
多分、jQueryの.ajaxComplete()あたりでga関数を飛ばしてあげるんだと思う。この辺は未検証。
最後になったけど、一応今回作ったファイルをダウンロードできるようにしておきます。
ダウンロードはこちらからどうぞ。