Many clients host their PHP application on a subdomain like blog.example.com, but later decide that they want the same site to appear under a subdirectory such as:
And they want:
- NO redirect
- URL must stay example.com/blog
- Content must come from blog.example.com
- All internal links must continue to work
This can only be done using a Reverse Proxy.
On an IIS server, the correct way to do this is using:
- Application Request Routing (ARR)
- URL Rewrite
This combination allows IIS to forward requests internally to the subdomain and rewrite outbound links so the user never sees the subdomain.
✔ Why Point 4 (Inbound Rewrite Only) Is NOT Enough
Many developers try only the following inbound rule:
<rule name="Blog Reverse Proxy" stopProcessing="true">
<match url="^blog/(.*)" />
<action type="Rewrite" url="http://blog.example.com/{R:1}" />
</rule>
This will load the PHP site from the subdomain, but it causes major problems:
- CSS, JS, and images will still load from blog.example.com
- Internal links redirect to blog.example.com
- Forms submit to the subdomain
- Login redirects break (WordPress, Laravel, etc.)
- The illusion of
/blogis lost
This happens because the backend continues to generate absolute URLs.
So inbound only = proxy works, but site breaks.
✔ What You Actually Need
To make the site fully functional under example.com/blog, you need two rules:
1. Inbound Rewrite (Reverse Proxy Request Forwarding)
This forwards all /blog/... requests to the PHP site.
<rule name="Blog Reverse Proxy" stopProcessing="true">
<match url="^blog/(.*)" />
<action type="Rewrite" url="http://blog.example.com/{R:1}" />
<serverVariables>
<set name="HTTP_X_Forwarded_For" value="{REMOTE_ADDR}" />
<set name="HTTP_X_Forwarded_Proto" value="https" />
<set name="HTTP_HOST" value="blog.example.com" />
</serverVariables>
</rule>
2. Outbound Rewrite (Fix HTML Links, Images, CSS, JS)
This replaces URLs inside HTML output so browsers never see the subdomain.
<outboundRules>
<rule name="Fix absolute URLs" preCondition="IsHTML">
<match filterByTags="A, Form, Img, Script, Link"
pattern="http://blog\.example\.com/(.*)" />
<action type="Rewrite" value="/blog/{R:1}" />
</rule>
<preConditions>
<preCondition name="IsHTML">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
</preCondition>
</preConditions>
</outboundRules>
Now all links such as:
http://blog.example.com/category
http://blog.example.com/wp-content/themes/style.css
automatically become:
/blog/category
/blog/wp-content/themes/style.css
This keeps users inside /blog forever.
🧩 Final Combined Working web.config
Put this inside the main site (example.com) web.config:
<configuration>
<system.webServer>
<rewrite>
<!-- Inbound reverse proxy rule -->
<rules>
<rule name="Blog Reverse Proxy" stopProcessing="true">
<match url="^blog/(.*)" />
<action type="Rewrite" url="http://blog.example.com/{R:1}" />
<serverVariables>
<set name="HTTP_X_Forwarded_For" value="{REMOTE_ADDR}" />
<set name="HTTP_X_Forwarded_Proto" value="https" />
<set name="HTTP_HOST" value="blog.example.com" />
</serverVariables>
</rule>
</rules>
<!-- Outbound rules to fix internal links -->
<outboundRules>
<rule name="Fix absolute URLs" preCondition="IsHTML">
<match filterByTags="A, Form, Img, Script, Link"
pattern="http://blog\.example\.com/(.*)" />
<action type="Rewrite" value="/blog/{R:1}" />
</rule>
<preConditions>
<preCondition name="IsHTML">
<add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>
🎉 Result
After applying both rules:
✔ Visiting example.com/blog will load the PHP site
✔ URL will NOT redirect
✔ All CSS, JS, and images will load correctly
✔ Internal site links stay under /blog
✔ Backend (PHP) keeps running normally on blog.example.com
This is the correct and complete reverse proxy setup for IIS.