by ryou

Rewrite Modに関して覚書

参考サイト

Apacheのmod_rewriteモジュールの使い方を徹底的に解説

ログの出し方

httpd.confに下記内容を追記

RewriteLog /var/log/httpd/rewrite.log
RewriteLogLevel 9

ログがないと中で何が起こっているか意味不明なので、検証時はログを吐き出すように。

実例

実例1

今まで、「http://www.hoge.com/blog/」配下にブログコンテンツを配置していたが、ホームページリニューアルに伴い、コンテンツ内容はそのままで、URLを「http://www.hoge.com/staff/blog/」へ移行することとなった。

SEO効果を保ったままコンテンツを移行したいため、以前のURLに関しては301リダイレクトを行いたい。この場合どのようにmod_rewriteを設定すればよいか。

解答例

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteCond %{REQUEST_URI} ^/blog/(.*)$
  RewriteRule ^blog/(.*)$ /staff/blog/$1 [R=301,L]
</IfModule>

ログ

「http://www.hoge.com/blog/index.php」にアクセスがあった際のログ例は以下のとおり。(余計な情報は省いています)

各処理に関してコメントで説明を記載しておきます。

# まず、リクエストURIを元に求められているファイルを導きます。
# DocumentRootは「/var/www/html/public/」という設定とします。
# 結果、/var/www/html/public/blog/index.phpが求められていることになります。
# その上でここでは、mod_rewriteのコンテキスト(.htaccessのディレクトリ)を
# 上記パスから間引く処理を行っています。
strip per-dir prefix: /var/www/html/public/blog/index.php -> blog/index.php

# RewriteCondより先に、RewriteRuleの左側のパターンと前述のパスとのマッチングを行います。
# ここでマッチしなかった場合はRewriteの処理がスルーされます。
# 今回は「^blog/(.*)$」と「blog/index.php」でマッチしたためRewriteCondの処理に入ります。
applying pattern '^blog/(.*)$' to uri 'blog/index.php'

# ここでのinputは「REQUEST_URI」のことです。
# REQUEST_URIは先頭に「/」が付くので注意して下さい。
# ここでもパターンがマッチするため、実際の書き換え処理に移ります。
RewriteCond: input='/blog/index.php' pattern='^/blog/(.*)$' => matched

# 2つのパターンマッチを通過したため、実際の書き換え処理が行われます。
rewrite 'blog/index.php' -> '/staff/blog/index.php'

# RewriteRuleにリダイレクトオプションを付けていたため、リダイレクト処理に移ります。
# この時点ではまだリダイレクトは実行されていません。
explicitly forcing redirect with http://www.hoge.com/staff/blog/index.php

# RewriteBaseに「/」が設定されているため、mod_rewriteのコンテキストが「/」に
# 置き換えられます。が、今回は特に必要のない処理となっています。
trying to replace prefix /var/www/html/public/ with /

# 下記処理で実際にリダイレクトが実行されます。
escaping http://www.hoge.com/staff/blog/index.php for redirect
redirect to http://www.hoge.com/staff/blog/index.php [REDIRECT/301]

# リダイレクトされて、今度は「http://www.hoge.com/staff/blog/index.php」へ
# アクセスされます。その処理になります。
# この処理でやっていることはこのログの一番上でやっていることと同じ処理です。
strip per-dir prefix: /var/www/html/public/staff/blog/index.php -> staff/blog/index.php

# 前回と同様に、RewriteRuleの左側の式と、上の処理で算出したパスとでマッチングを行います。
# 今回は「^blog/(.*)$」と「staff/blog/index.php」のマッチングのため、マッチせず
# pass throughとなり書き換え処理がスルーされます。
applying pattern '^blog/(.*)$' to uri 'staff/blog/index.php'
pass through /var/www/html/public/staff/blog/index.php

# 最終的に、「/var/www/html/public/staff/blog/index.php」というファイルの処理が
# 実行されます。

簡易版

実はこの例は以下のとおりにもっと短く書くことが出来る。

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteRule ^blog/(.*)$ /staff/blog/$1 [R=301,L]
</IfModule>

変更点は、RewriteBaseが不要だったので削除した点と、RewriteCondも削除した点。
前述のとおり、RewriteRuleの左側の式でパターンマッチをした後にRewriteCondでパターンマッチを
しているのだが、両者は同じマッチングをしていたのでRewriteCondを削除しても問題はない(はず)。

この時のログは以下のとおり

strip per-dir prefix: /var/www/html/public/blog/index.php -> blog/index.php
applying pattern '^blog/(.*)$' to uri 'blog/index.php'
rewrite 'blog/index.php' -> '/staff/blog/index.php'
explicitly forcing redirect with http://www.hoge.com/staff/blog/index.php
escaping http://www.hoge.com/staff/blog/index.php for redirect
redirect to http://www.hoge.com/staff/blog/index.php [REDIRECT/301]

# リダイレクト後
strip per-dir prefix: /var/www/html/public/staff/blog/index.php -> staff/blog/index.php
applying pattern '^blog/(.*)$' to uri 'staff/blog/index.php'
pass through /var/www/html/public/staff/blog/index.php

以前あった以下の2行が今回のログでは出力されなくなっている。

RewriteCond: input='/blog/index.php' pattern='^/blog/(.*)$' => matched
trying to replace prefix /var/www/html/public/ with /

実例2

URIのパラメータにより表示内容を変えるタイプのサイト(index.php?category=blog&id=1、みたいな)がある。
このサイトを「http://www.hoge.com/blog/1」といったURLで対象のコンテンツを表示するよう変更したい。

解答例

大体のフレームワークは前述のようなURIを取るようになっている。
その場合のやり方は

<IfModule mod_rewrite.c>
  RewriteEngine On
  # 下2行は、「実在するファイルやディレクトリで無ければルール適用」な意味
  # 例えば、「http://www.hoge.com/img/test.jpg」にアクセスがあって、
  # 実際に画像が「/var/www/html/public/img/test.jpg」に存在していれば
  # その画像を返却する。
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^(.*)$ index.php?/$1 [QSA,L]
</IfModule>

mod_rewriteに関しては上記のように設定。
すると$_SERVER[‘QUERY_STRING’]にパラメータ(/staff/blog/、的な)が入ってくるのでそれを「/」で分割したりすればOK。

ハマリポイント

301転送によるキャッシュ

ブラウザによっては301による転送をキャッシュするものがある(chromeはそうだった)ので、rewrite_modのテストをする時、301による転送を使っているとファイルを書き換えたのに反映されないといったことが起きるので注意。

RewriteRuleによるチェック → RewriteCondによるチェック

RewriteCondによるチェックの前に、RewriteRuleによるチェックが先に行われる。

例えば、

# .htaccessは「/var/www/html/public/.htaccess」に存在するとする
RewriteCond %{REQUEST_URI} ^/blog/(.*)$
RewriteRule ^blog/(.*)1

といったルールがあり、それに対して「http://www.hogehoge.com/staff/blog/index.php」といったリクエストを行うと

http://www.hogehoge.com/staff/blog/index.phpを解釈

/var/www/html/public/staff/blog/index.php というファイルを求めていると解釈できる

mod_rewriteのコンテキスト(この場合は.htaccessのあるディレクトリ)である「/var/www/html/public/」を上記パスから間引く

結果、staff/blog/index.phpとなり、これを元に先にRewriteRuleによるチェック

RewriteRuleの左側式「^blog/(.*)$」と「staff/blog/index.php」をマッチング

not-matchedなのでRewriteCond及び、RewriteRule右側への書き換えはスルー

というフローになる。

この時、以下の様なログが流れる

strip per-dir prefix: /var/www/html/public/staff/blog/index.php -> staff/blog/index.php
applying pattern '^blog/(.*)$' to uri 'staff/blog/index.php'
pass through /var/www/html/public/staff/blog/index.php

REQUEST_URIが返す値は先頭に「/」が付く、RewriteRuleとの比較時には「/」が付かない

タイトルの通り。

前述の通り、mod_rewriteの処理の過程でリクエストされたファイルパスからmod_rewriteのコンテキストを間引く時、結果として返ってくるパスの先頭には「/」が付かない。

これに対し、RewriteCondで使用される「REQUEST_URI」は先頭に「/」が付く、この違いがあるので注意。

RewriteRuleでリダイレクトオプションを付ける場合は、RewriteRuleの右側の先頭に「/」をつけること

そうしないと転送先がエライことになってしまったり。

例:「http://~/blog/index.php」を「http://~/staff/blog/index.php」に飛ばすつもりが、「http://~/var/www/html/public/staff/blog/index.php」に飛ばしてしまったり

理由はある程度わかっているが説明するほどの理解に至ってないのでとりあえずそういうことって覚えとく