When .htaccess rules run, the URL is already a file path. mod_rewrite strips the directory's path, trailing slash included, matches the rest, and adds the prefix back to a relative substitution. A ^/ pattern therefore never matches in .htaccess, and a rule moved into a virtual host needs the slash added.
RewriteEngine On
RewriteRule ^post/(\d+)$ show.php?id=$1 [L]
RewriteRule ^old-post$ new-post [R=301,L]$ t /blog/post/5 /blog/old-post
/blog/post/5 200 {"id":"5"}
/blog/old-post 301 http://example.com/var/www/example/blog/new-postThe trace of the first request shows the prefix going out and coming back:
trace3 initial strip per-dir prefix: /var/www/example/blog/post/5 -> post/5 trace3 initial applying pattern '^post/(\\d+)$' to uri 'post/5' trace3 initial add per-dir prefix: show.php?id=5 -> /var/www/example/blog/show.php?id=5 trace1 initial internal redirect with /blog/show.php [INTERNAL REDIRECT]
The redirect is the classic failure: the prefix added back is a filesystem path, and the redirect sent it to the browser, leaking the server's layout. RewriteBase /blog/ after RewriteEngine On tells mod_rewrite which URL path the directory answers to, and the redirect then goes to http://example.com/blog/new-post. Since 2.4.16, internal rewrites in a directory reached through Alias work without it. Add RewriteBase wherever rules redirect to relative targets, or write those targets as absolute paths.