Ok, so link bait headlines are the order of the day
One tricky issue I had when setting up this website is that the domain itself (knarfalingus.com) is not the main domain of the site. This is a shared hosting account, so on disk, what I will call the ‘primary’ domain points to the root folder ‘’ , and ‘knarfalingus.com’ is a domain pointer that points to the same IP address. I wanted ‘knarfalingus.com’ to point to a subfolder. The way I have it set up, this domain is hosted on disk in a subdirectory of the root, ‘knarfalingus.com’
In order to achieve this, various rewrite rules were put in play. I’ll just refer to the two excellent articles I used when I first set up the site for details, but they should cover most of what is needed to handle the situation. The web.config containing these rules go in the root folder.
For the starting point of this post, this is the web.config for the case when the you have a domain pointer “yourdomain.com” and you want to host it on disk in a same named subfolder of the root i.e. ‘yourdomain.com’
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="yourdomain.com" enabled="true" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" pattern="^/yourdomain.com($|/)" negate="true" /> </conditions> <action type="Rewrite" url="/yourdomain.com/{R:0}" /> </rule> </rules> <outboundRules> <rule name="Outgoing - URL paths" preCondition=".aspx pages only" enabled="true"> <match filterByTags="A" pattern="^(?:yourdomain|(.*//[_a-zA-Z0-9-.]*)?/yourdomain.com)(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <rule name="response_location URL"> <match serverVariable="RESPONSE_LOCATION" pattern="^(?:yourdomain|(.*//[_a-zA-Z0-9-.]*)?/yourdomain.com)(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <rule name="response_location querystring"> <match serverVariable="RESPONSE_LOCATION" pattern="(.*)%2fyourdomain.com(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <preConditions> <preCondition name=".aspx pages only"> <add input="{SCRIPT_NAME}" pattern=".aspx$" /> </preCondition> </preConditions> </outboundRules> </rewrite> </system.webServer> </configuration>
Although this was satisfactory, I recently wanted to add permalinks to my WordPress site. But doing so resulted in nasty 404 errors in IIS. Following the advice here would probably work on most sites but not on mine due to the special setup.
First, I wanted to have a URL pattern that was useful in terms of SEO and also resistant to change. For example, in the WordPress settings, one of the built-in Permalink Types is ‘Day and Name’ which looks like so:
http://www.yourdomain.com/2014/05/26/sample-post/
Where ‘sample post’ is the post slug (which was helpfully hidden for me in WordPress when editing posts, this can be made visible via Screen Options).
To me this not a very useful format. The date is included which might be helpful on a more time sensitive blog like a news site, but to me is meaningless. Worse still the entire format is based on the premise that your title slug never changes – which you may want to change at some later date.
If in the future you change your scheme to
http://www.yourdomain.com/sample-post/2014/05/26/
you can rest assured you have broken all existing links to your posts!
How to solve these issues and improve SEO? For the SEO part, I’d want the title first in the URL, as it is the most specific information. After that I’d want the category for some more keywords. Finally, add the unique post_id to the URL. My URL scheme is therefore as follows
Here are the parts of the scheme (see Permalinks for syntax)
- /blog/ – the reason for this is explained below
- %postname% – this is next – this is the most ‘specific’ information about the post, and I put it first in the URL for that reason.
- %category% – this is next, less specific, but useful keywords. If you use a category hiearchy you will get multiple keywords separated by ‘/’
- %post_id% – this is last. It is meaningless in terms of SEO so it can be last, and as long as I commit to keeping %post_id% last in any URL scheme I come up in the future, any existing links out on other sites should continue to work (EDIT:not with WordPress permalinks but by using IIS Rewrite, see below)
To demonstrate this, examine the following links, they will all lead back to this page, due to the non-varying placement of %post_id%
http://www.knarfalingus.com/blog/hosting-wordpress-on-iis-you-wont-believe-what-you-dont-know-pics-nsfw/wordpress/80/
http://www.knarfalingus.com/blog/still/works/wordpress/80/
http://www.knarfalingus.com/blog/still/works/now/80/
And finally this one here – which won’t work with the WordPress Permalink scheme, which expects three leading directories in the path to the %post_id%. It does work now as I address the issue below when discussing the /blog/ prefix.
http://www.knarfalingus.com/blog/80/
/blog/ prefix
The reason for this is simple, as I stated earlier I have the pre-existing rules which make the domain pointer appear as a standalone domain, but this has a consequence, the rules in the web.config in the root folder actually control the rewrites. In order to squeeze in a rule for the blog without interfering with anything else, I chose to have a rule that handles only URLs with a specific prefix, in this case ‘/blog/’ (actually it matches /blog followed by anything, /blog1/, /blogxyz/ but for my purposes, good enough). This rewrites those URLs to index.php
<rule name="yourdomain.com wordpress" enabled="true" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" matchType="Pattern" pattern="^/blog/.*$" ignoreCase="true" negate="false" /> </conditions> <action type="Rewrite" url="/yourdomain.com/index.php/{R:0}" appendQueryString="false" /> </rule>
Now above I mentioned http://www.knarfalingus.com/blog/80/ doesnt redirect to the page – but we can fix that by changing our newly added rule to:
<rule name="yourdomain.com wordpress" enabled="true" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" matchType="Pattern" pattern="^/blog[w-_/.]*/(d+)/?$" ignoreCase="true" negate="false" /> </conditions> <action type="Rewrite" url="/yourdomain.com/?p={C:1}" appendQueryString="false" /> </rule>
This rule captures all URLs for yourdomain.com that start with /blog and end with a number. That number is rewritten to what is the default PermaLink URL in wordpress (/?p=%post_id%).
Now URL’s like this work, note the ID is at the end of the URL:
I also added a trailing ? to the RegEx to make the trailing slash optional
Wrapping up, many WordPress themes have a 404 page, But I did not see it, I saw an IIS 404 page. The issue is IIS is handling the 404, instead we need to let it flow through to PHP/Wordpress. The solution is to add the following to web.config
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <httpErrors existingResponse="PassThrough" /> .....
Tossing in a little security, we can block others from loading our content in frames (I know, not very common anymore), by using the X-Frame-Options header and specifying SAMEORIGIN.
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <httpProtocol> <customHeaders> <add name="X-Frame-Options" value="sameorigin" /> </customHeaders> </httpProtocol>
The final config is now as follows:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <httpProtocol> <customHeaders> <add name="X-Frame-Options" value="sameorigin" /> </customHeaders> </httpProtocol> <httpErrors existingResponse="PassThrough" /> <rewrite> <rules> <rule name="yourdomain.com wordpress" enabled="true" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" matchType="Pattern" pattern="^/blog[w-_/.]*/(d+)/?$" ignoreCase="true" negate="false" /> </conditions> <action type="Rewrite" url="/yourdomain.com/?p={C:1}" appendQueryString="false" /> </rule> <rule name="yourdomain.com" enabled="true" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" pattern="^/yourdomain.com($|/)" negate="true" /> </conditions> <action type="Rewrite" url="/yourdomain.com/{R:0}" /> </rule> </rules> <outboundRules> <rule name="Outgoing - URL paths" preCondition=".aspx pages only" enabled="true"> <match filterByTags="A" pattern="^(?:yourdomain|(.*//[_a-zA-Z0-9-.]*)?/yourdomain.com)(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <rule name="response_location URL"> <match serverVariable="RESPONSE_LOCATION" pattern="^(?:yourdomain|(.*//[_a-zA-Z0-9-.]*)?/yourdomain.com)(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <rule name="response_location querystring"> <match serverVariable="RESPONSE_LOCATION" pattern="(.*)%2fyourdomain.com(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <preConditions> <preCondition name=".aspx pages only"> <add input="{SCRIPT_NAME}" pattern=".aspx$" /> </preCondition> </preConditions> </outboundRules> </rewrite> </system.webServer> </configuration>
UPDATE : after posting I noticed the categories, date, comments, feed, author links (side menu) and pages didn’t work. This seemed to be the combination of PermaLinks plus the rules I started with, not the recent rule additions. I basically exclude rewriting any subdirectories and any .php files to index.php – this seems to be working. All the permalinks seem to be working as well without any logic for the /blog prefix.
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- {EA385C80-7BD5-46DC-8E47-F992B0514618} --> <system.webServer> <httpProtocol> <customHeaders> <add name="X-Frame-Options" value="sameorigin" /> </customHeaders> </httpProtocol> <httpErrors existingResponse="PassThrough" /> <rewrite> <rules> <!-- permalink = /blog/%postname%/%category%/%post_id%/ --> <clear /> <rule name="wordpress grab trailing post_id" enabled="true" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" matchType="Pattern" pattern="^/blog[w-_/.]*/(d+)/?$" ignoreCase="true" negate="false" /> </conditions> <action type="Rewrite" url="/yourdomain.com/?p={C:1}" appendQueryString="false"/> </rule> <rule name="wordpresss urls" enabled="true" stopProcessing="false"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" pattern="^/wp-includes/.*$" negate="true" /> <add input="{PATH_INFO}" pattern="^/wp-content/.*$" negate="true" /> <add input="{PATH_INFO}" pattern="^/wp-admin/.*$" negate="true" /> <add input="{REQUEST_FILENAME}" pattern="^.*.php$" negate="true" /> </conditions> <action type="Rewrite" url="/index.php/{R:0}" /> </rule> <rule name="yourdomain.com" enabled="true" stopProcessing="false"> <match url=".*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_HOST}" pattern="^(www.)?yourdomain.com$" /> <add input="{PATH_INFO}" pattern="^/yourdomain.com($|/)" negate="true" /> </conditions> <action type="Rewrite" url="/yourdomain.com/{R:0}" /> </rule> </rules> <outboundRules> <rule name="Outgoing - URL paths" preCondition=".aspx pages only" enabled="true"> <match filterByTags="A" pattern="^(?:yourdomain|(.*//[_a-zA-Z0-9-.]*)?/yourdomain.com)(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <rule name="response_location URL"> <match serverVariable="RESPONSE_LOCATION" pattern="^(?:yourdomain|(.*//[_a-zA-Z0-9-.]*)?/yourdomain.com)(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <rule name="response_location querystring"> <match serverVariable="RESPONSE_LOCATION" pattern="(.*)%2fyourdomain.com(.*)" /> <action type="Rewrite" value="{R:1}{R:2}" /> </rule> <preConditions> <preCondition name=".aspx pages only"> <add input="{SCRIPT_NAME}" pattern=".aspx$" /> </preCondition> </preConditions> </outboundRules> </rewrite> </system.webServer> </configuration>
OR
LOGIN OR REGISTER
Registered users with one approved comment can comment without moderation