Simple URL keeper for EPiServer

Frederik Vig 16.05.2015 15:14:00

So what we want to do is attach our selves to the MovingContent event, get a hold of the old URL and the content GUID, store that data, and in our custom 404 controller check for it so that we might perform a redirect (otherwise we display our normal 404 page).

using EPiServer.Core;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;

namespace SEO.UrlKeeper
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class InitializationModule : IInitializableModule
    {
        private static bool _initialized;

        public void Initialize(InitializationEngine context)
        {
            if (_initialized)
            {
                return;
            }

            var contentEvents = ServiceLocator.Current.GetInstance();

            contentEvents.MovingContent += contentEvents_MovingContent;

            _initialized = true;
        }

        private void contentEvents_MovingContent(object sender, EPiServer.ContentEventArgs e)
        {
            var urlResolver = ServiceLocator.Current.GetInstance();
            var repository = ServiceLocator.Current.GetInstance();

            var item = new UrlKeeperItem
            {
                ContentGuid = e.Content.ContentGuid,
                OldPath = urlResolver.GetUrl(e.ContentLink)
            };

            repository.Save(item);
        }

        public void Preload(string[] parameters) { }

        public void Uninitialize(InitializationEngine context)
        {
            var contentEvents = ServiceLocator.Current.GetInstance();

            contentEvents.MovingContent -= contentEvents_MovingContent;
        }
    }
}

To persist the data we use EPiServer's Dynamic Data Store. The repository is quite simple.

using System;
using System.Linq;
using EPiServer.Data;
using EPiServer.Data.Dynamic;
using EPiServer.ServiceLocation;

namespace SEO.UrlKeeper
{
    [ServiceConfiguration(typeof(IUrlKeeperRepository))]
    public class UrlKeeperRepository : IUrlKeeperRepository
    {
        private static DynamicDataStore Store
        {
            get
            {
                return typeof(UrlKeeperItem).GetOrCreateStore();
            }
        }

        public UrlKeeperItem GetByOldPath(string path)
        {
            return this.GetAll().FirstOrDefault(item => item.OldPath == path);
        }

        private UrlKeeperItem GetByContentGuid(Guid contentGuid)
        {
            return this.GetAll().FirstOrDefault(item => item.ContentGuid == contentGuid);
        }

        private void Delete(Identity id)
        {
            Store.Delete(id);
        }

        public IQueryable GetAll()
        {
            return Store.Items();
        }

        public Identity Save(UrlKeeperItem item)
        {
            if (item == null || string.IsNullOrEmpty(item.OldPath))
            {
                return null;
            }

            // check if the old path already exists
            var oldPathItem = this.GetByOldPath(item.OldPath);

            // check if contentGuid is already there
            var existingItem = this.GetByContentGuid(item.ContentGuid);

            if (oldPathItem != null)
            {
                // since we don't want more than one item with the same old path.
                this.Delete(oldPathItem.Id);
            }

            if (existingItem != null)
            {
                // let's replace the existing item with our new on (we only want one per contentGuid)
                item.Id = existingItem.Id;
            }

            item.Modified = DateTime.UtcNow;

            return Store.Save(item);
        }
    }
}
using System.Linq;
using EPiServer.Data;

namespace SEO.UrlKeeper
{
    public interface IUrlKeeperRepository
    {
        IQueryable GetAll();

        Identity Save(UrlKeeperItem item);

        UrlKeeperItem GetByOldPath(string path);
    }
}

using System;
using EPiServer.Data;
using EPiServer.Data.Dynamic;

namespace SEO.UrlKeeper
{
    [EPiServerDataStore(AutomaticallyCreateStore = true, AutomaticallyRemapStore = true)]
    public class UrlKeeperItem
    {
        public Identity Id { get; set; }


        [EPiServerDataIndex]
        public Guid ContentGuid { get; set; }


        [EPiServerDataIndex]
        public string OldPath { get; set; }


        public DateTime Modified { get; set; }
    }
}

Our 404 controller is just as simple.

using System.Web.Mvc;
using EPiServer.Web;
using EPiServer.Web.Routing;
using SeoPlayground.SEO.UrlKeeper;

namespace SeoPlayground.Controllers
{
    public class NotFoundController : Controller
    {
        private readonly IUrlKeeperRepository _urlKeeperRepository;
        private readonly UrlResolver _urlResolver;

        public NotFoundController(IUrlKeeperRepository urlKeeperRepository, UrlResolver urlResolver)
        {
            this._urlKeeperRepository = urlKeeperRepository;
            this._urlResolver = urlResolver;
        }

        public ActionResult Index()
        {
            var item = this._urlKeeperRepository.GetByOldPath(Request.RawUrl);

            if (item != null)
            {
                try
                {
                    var redirectUrl = this._urlResolver.GetUrl(PermanentLinkUtility.FindContentReference(item.ContentGuid));

                    if (redirectUrl != null)
                    {
                        Response.RedirectPermanent(redirectUrl, true);
                    }
                }
                catch
                {
                }
            }

            Response.TrySkipIisCustomErrors = true;
            Response.StatusCode = 404;

            return View();
        }
    }
}

Last step is just updating web.config to use our custom 404 controller.

<system.webServer>
<httpErrors errorMode="Custom">
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" prefixLanguageFilePath="" path="/NotFound" responseMode="ExecuteURL" />
</httpErrors>
</system.webServer>

Next time an editor moves a page, Search Crawlers and people who use the old URL will automatically be redirected to the new URL instead of getting a 404 error back.