Quick ‘n’ Dirty Redirects in DotNetNuke

I have recently been involved in some SEO work on a website written in DotNetNuke. SEO (Search Engine Optimization) is the arcane and mystical art of making your website simultaneously appear interesting/relevant to both users and and search engine bots, most especially the GoogleBot. One of the keys to this it not to have duplicate content on your website, that is to say, pages that appear to be the same that are available via different URLs. If you do this, the GoogleBot will think you are trying to trick it and it devalues the relevance of that content. As a result, if you move content from one URL to another you need to 301 (permanently redirect) the old URL to the new URL.
 
A common way to do this is to buy or write a DNN module that redirects and install it on your site on the pages that should be redirected. This is a pain though because it means that you have to keep those pages in your portal even though you don’t need them any more and each request to them has to load the page and the module before it is redirected.
 
The fact is that DNN is constantly internally redirecting anyway using a technique called URL rewriting. The "real" address of most pages in a DNN portal is /Default.aspx?tabid=1234 where 1234 is a unique identifier for a "page" of content in DNNs database. This rewriting is done by an HttpModule whose configuration is stored in an xml file in the root of the portal (/SiteURLs.config). An example of a URLRewriter rule can be seen here :
 
<RewriterRule>
	<LookFor>.*/MyNiceURL.aspx</LookFor>
	<SendTo>~/Default.aspx?tabid=1234</SendTo>
</RewriterRule>
It occurred to me that it would be easier just to hijack this mechanism and with a very small tweak add permanent (301) and temporary (302) redirects to its features. A new rule would look something like this :
 
<RewriterRule Type="Permanent">
	<LookFor>.*/MyNiceURL.aspx</LookFor>
	<SendTo>~/Default.aspx?tabid=1234</SendTo>
</RewriterRule>
The Type attribute could have values of Permanet, Temporary or Rewrite (the default) or be absent. We then need to make a couple of simple changes to the URLRewriter module which, thanks to open source, we can.
Someone emailed me about this so I thought I’d add a quick clarification. The URLRewriter can only rewrite the path segment of the URL. So if you give it a rule that looks like this :
 
<RewriterRule>
	<LookFor>.*/MyNiceURL.aspx</LookFor>
	<SendTo>http://www.mydomain.com/Default.aspx?tabid=1234</SendTo>
</RewriterRule>
it will instead perform a Response.Redirect. This sends a 302 to the client, not a 301, so without this modification we still have no way of sending a 301 and we can only send a 302 if we include the whole url, i.e. we have limited control over how the rewriter behaves.

First we add an enum to represent our rule type in RewriterRule.vb:
 
Public Enum RewriterRuleType
        Rewrite = 0
        Permanent = 1
        Temporary = 2
End Enum
Then we add the new public property and private field to the RewriterRule class :
 
Private _ruleType As RewriterRuleType = RewriterRuleType.Rewrite

Public Property RuleType() As RewriterRuleType

    Get
        Return _ruleType
    End Get

    Set(ByVal Value As RewriterRuleType)
        _ruleType = Value
    End Set

End Property
We also need to modify the GetConfig method of the RewriteConfiguration class so it knows about our new XML attribute :
 
...

For Each nav As XPathNavigator In doc.CreateNavigator.Select("RewriterConfig/Rules/RewriterRule")

    Dim rule As New RewriterRule()

    rule.LookFor = nav.SelectSingleNode("LookFor").Value

    rule.SendTo = nav.SelectSingleNode("SendTo").Value

    ' >>> begin new code

    Try

        rule.RuleType = CType(System.Enum.Parse(GetType(RewriterRuleType), nav.GetAttribute("Type", ""), False), RewriterRuleType)

    Catch ex As Exception

        ' Do nothing, either there was no attribute or it contained an invalid value
        ' NB : yes this is a dirty hack but this is a blog post not production code

    End Try

    ' >>> end new code

    Config.Rules.Add(rule)

Next

...
Next we need to modify the RewriteURL method of the URLRewriteModule class so it can take different actions based on the newly added Rule Type :
 
(NB : the ruleType variable has been declared at the top of the method and is assigned to as soon as a matching rule is found.)
 
...

If

 intMatch <> -1 Then

    ' >>> begin new code 

    Select Case (ruleType)

        Case Config.RewriterRuleType.Permanent

            RewriterUtils.PermanentlyRedirectURL(app.Context, sendTo)

        Case Config.RewriterRuleType.Rewrite

            ' >>> begin old code 

            If rules(intMatch).SendTo.StartsWith("~") Then

                ' rewrite the URL for internal processing

                RewriterUtils.RewriteUrl(app.Context, sendTo)

            Else

                ' it is not possible to rewrite the domain portion of the URL so redirect to the new URL

                Response.Redirect(sendTo, True)

            End If

            ' >>> end old code 



        Case Config.RewriterRuleType.Temporary

            RewriterUtils.TemporarilyRedirectURL(app.Context, sendTo)

    End Select

    ' >>> end new code 

End If

...
 
Finally, we add these new actions to the RewriterUtils class :
 
Friend

 Shared Sub TemporarilyRedirectURL(ByVal context As HttpContext, ByVal sendToUrl As String)

    context.Response.Clear()

    context.Response.Status = "302 Found"

    context.Response.AddHeader("Location", sendToUrl)

    context.Response.End()

End Sub

 

Friend Shared Sub PermanentlyRedirectURL(ByVal context As HttpContext, ByVal sendToUrl As String)

    context.Response.Clear()

    context.Response.Status = "301 Moved Permanently"

    context.Response.AddHeader("Location", sendToUrl)

    context.Response.End()

End Sub
 
We can then compile the DNN source and replace the DotNetNuke.HttpModules.UrlRewrite.dll in our DNN installation with the newly compiled one.

Advertisements
%d bloggers like this: