fredag den 23. januar 2015

An improved, multisite aware alias solution

Lets face it - the builtin alias functionality in Sitecore isn't really that great.
It does not support multiple sites with the same alias, and only supports replacing the Sitecore.Context.Item with the target item, not redirecting.

That is why I have created another solution, that is way more flexible, supports the following things:
  • Being multisite aware (every alias is defined for the site it is related to).
  • Handling incoming URL's that is no at the root of the site (like: "/test/test2").
  • Supporting different extensions (like .php ) as long as they are mapped to the ASP.NET ISAPI module.
  • Setting the context item (like the current alias does).
  • Redirect (both temporary and permanent) to the new target item.
All in all, it has alot of different features.

Now, lets take a look at how it is implemented - first lets talk a little bit about making multisite solutions in Sitecore.
The way we do, is creating a folder in /Sitecore/Content called Websites .
Inside here, we create a folder for each website, and in there, we create two items - an item called Global, that contains all the metadata for the site, and an item called Home, which is the frontpage for the website (with all the content pages being descendants from here).

Then for each site, we create a folder called Aliases under the Global item.

First, we have to create some dummy items, to be able to select from a dropdown list, to indicate which redirect mode we should use - so create a folder here: /sitecore/system/modules/AliasRedirectModes .

In this folder, create 3 items, based on the Standard Fields template (it has no custom fields, which is perfect for this case) :

200 - Rewrite internal URL
301 - Moved Permanently
302 - Moved Temporarily

Then create a template somewhere, for you alias, with the following fields:

Name / Type / Source
Incoming Link / Single-Line Text /
Outgoing Link / General Link /
Redirect Mode / Droplist / /sitecore/system/modules/AliasRedirectModes

Now, make sure this template is added to the insert options for the Aliases folder create under Global for your sites.

And then for some code :-)

Create a class in a Sitecore project somewhere, and paste the following code:

public class AliasItemResolver : HttpRequestProcessor
{
    public override void Process(HttpRequestArgs args)
    {
        // If the current item is found, there is no reason to go on, so we return.
        if (Sitecore.Context.Item != null)
        {
            return;
        }

        // If there is no context, we are not in a request from a web browser.
        if (args == null || args.Context == null)
        {
            // So we return.
            return;
        }

        // We have to make sure we don't run for the website to make tasks working without failing.
        if (Sitecore.Context.Site.Name == "website")
        {
            return;
        }

        // If not running on a site under /sitecore/content/websites, just return.
        if (!Sitecore.Context.Site.StartPath.StartsWith("/sitecore/content/websites", StringComparison.InvariantCultureIgnoreCase) || Sitecore.Context.Site.Name == "shell")
        {
            return;
        }

        // Get the requested URL.
        Uri url = args.Context.Request.Url;

        // We find the aliases folder for this site.
        Item aliasesFolderItem = GetAliasesFolder(Sitecore.Context.Site);

        // And then we find the right alias item.
        Item alias = (from aliasItem in aliasesFolderItem.Axes.GetDescendants()
                        where aliasItem.TemplateName == "Alias" && aliasItem["Incoming Link"].Equals(HttpUtility.UrlDecode(url.AbsolutePath), StringComparison.InvariantCultureIgnoreCase)
                        select aliasItem).FirstOrDefault();

        // If we didn't find an alias, we return.
        if (alias == null)
        {
            return;
        }

        // Get the link, and return if we fail.
        string link = GetLinkFromAlias(alias);
        if (link == null)
        {
            return;
        }

        // And then we see what kind of redirect is needed.
        switch (alias["Redirect Mode"])
        {
            case "200 - Rewrite internal URL":
                Sitecore.Context.Item = ((LinkField)alias.Fields["Outgoing Link"]).TargetItem;
                return;
            case "301 - Moved Permanently":
                // If it is 301, we redirects the client with the Moved Permanently message.
                args.Context.Response.Clear();
                args.Context.Response.Headers.Add("Location", link);
                args.Context.Response.Status = "301 - Moved Permanently";
                args.Context.Response.StatusCode = 301;
                args.Context.Response.End();
                args.AbortPipeline();
                return;
            case "302 - Moved Temporarily":
                // If it is 302, we redirects the client with the Moved Temporarily message.
                args.Context.Response.Clear();
                args.Context.Response.Headers.Add("Location", link);
                args.Context.Response.Status = "302 - Moved Temporarily";
                args.Context.Response.StatusCode = 302;
                args.Context.Response.End();
                args.AbortPipeline();
                return;
            default:
                // Unknown value, so we can't do anything.
                return;
        }
    }

    private static string GetLinkFromAlias(Item alias)
    {
        // We need to disable security, in case the item we link to is behind a login wall.
        // You might want to make sure the user is redirected to a login page, if the item being targeted is behind a login wall.
        using (new SecurityDisabler())
        {
            // TODO: HERE YOU HAVE TO FIGURE OUT HOW TO GET THE LINK FROM THE FIELD, DEPENDING ON WHAT TYPE IT IS :-)
        }
    }

    private static Item GetAliasesFolder(SiteContext site)
    {
        using (new SecurityDisabler())
        {
            // If the site is null, something is broken, and we just return.
            if (site == null)
            {
                return null;
            }

            // Then get the global folder for that site.
            Item globalFolderItem = // TODO: HERE YOU HAVE TO FIND A GOOD WAY TO FIND THE GLOBAL FOLDER ITEM IN YOUR SOLUTION :-)
            if (globalFolderItem == null)
            {
                // We couldn't find the global folder item, so we return;
                return null;
            }

            // Then we see if we can get the alias folder item, and return the folder.
            return globalFolderItem.Children["Aliases"];
        }
    }
}

I have left two small assignments in the code for you to do yourself ;-)
Now save the code, and build the project.

Then create the following include file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="Namespace.Classname, Assembly" patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"/>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Now you are good to go - once the user creates a request, if the item is not found, it looks to see if there is an alias, and does the right action based on the found alias.